diff --git a/.vscode/launch.json b/.vscode/launch.json index 574ca21..dd83c22 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,13 +1,42 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "C#: Launch Startup Project", - "type": "dotnet", - "request": "launch" - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch SimpleStart", + "type": "dotnet", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/Aquiis.SimpleStart/bin/Debug/net9.0/Aquiis.SimpleStart.dll", + "args": [], + "cwd": "${workspaceFolder}/Aquiis.SimpleStart", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + { + "name": "Launch Professional", + "type": "dotnet", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/Aquiis.Professional/bin/Debug/net9.0/Aquiis.Professional.dll", + "args": [], + "cwd": "${workspaceFolder}/Aquiis.Professional", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + ] +} diff --git a/Aquiis.Professional/Application/Services/ApplicationService.cs b/Aquiis.Professional/Application/Services/ApplicationService.cs new file mode 100644 index 0000000..2e07407 --- /dev/null +++ b/Aquiis.Professional/Application/Services/ApplicationService.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Options; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Constants; + +namespace Aquiis.Professional.Application.Services +{ + public class ApplicationService + { + private readonly ApplicationSettings _settings; + private readonly PaymentService _paymentService; + private readonly LeaseService _leaseService; + + public bool SoftDeleteEnabled { get; } + + public ApplicationService( + IOptions settings, + PaymentService paymentService, + LeaseService leaseService) + { + _settings = settings.Value; + _paymentService = paymentService; + _leaseService = leaseService; + SoftDeleteEnabled = _settings.SoftDeleteEnabled; + } + + public string GetAppInfo() + { + return $"{_settings.AppName} - {_settings.Version}"; + } + + /// + /// Gets the total payments received for a specific date + /// + public async Task GetDailyPaymentTotalAsync(DateTime date) + { + var payments = await _paymentService.GetAllAsync(); + return payments + .Where(p => p.PaidOn.Date == date.Date && !p.IsDeleted) + .Sum(p => p.Amount); + } + + /// + /// Gets the total payments received for today + /// + public async Task GetTodayPaymentTotalAsync() + { + return await GetDailyPaymentTotalAsync(DateTime.Today); + } + + /// + /// Gets the total payments received for a date range + /// + public async Task GetPaymentTotalForRangeAsync(DateTime startDate, DateTime endDate) + { + var payments = await _paymentService.GetAllAsync(); + return payments + .Where(p => p.PaidOn.Date >= startDate.Date && + p.PaidOn.Date <= endDate.Date && + !p.IsDeleted) + .Sum(p => p.Amount); + } + + /// + /// Gets payment statistics for a specific period + /// + public async Task GetPaymentStatisticsAsync(DateTime startDate, DateTime endDate) + { + var payments = await _paymentService.GetAllAsync(); + var periodPayments = payments + .Where(p => p.PaidOn.Date >= startDate.Date && + p.PaidOn.Date <= endDate.Date && + !p.IsDeleted) + .ToList(); + + return new PaymentStatistics + { + StartDate = startDate, + EndDate = endDate, + TotalAmount = periodPayments.Sum(p => p.Amount), + PaymentCount = periodPayments.Count, + AveragePayment = periodPayments.Any() ? periodPayments.Average(p => p.Amount) : 0, + PaymentsByMethod = periodPayments + .GroupBy(p => p.PaymentMethod) + .ToDictionary(g => g.Key, g => g.Sum(p => p.Amount)) + }; + } + + /// + /// Gets leases expiring within the specified number of days + /// + public async Task GetLeasesExpiringCountAsync(int daysAhead) + { + var leases = await _leaseService.GetAllAsync(); + return leases + .Where(l => l.EndDate >= DateTime.Today && + l.EndDate <= DateTime.Today.AddDays(daysAhead) && + !l.IsDeleted) + .Count(); + } + } + + public class PaymentStatistics + { + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public decimal TotalAmount { get; set; } + public int PaymentCount { get; set; } + public decimal AveragePayment { get; set; } + public Dictionary PaymentsByMethod { get; set; } = new(); + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Application/Services/CalendarEventService.cs b/Aquiis.Professional/Application/Services/CalendarEventService.cs new file mode 100644 index 0000000..2199084 --- /dev/null +++ b/Aquiis.Professional/Application/Services/CalendarEventService.cs @@ -0,0 +1,260 @@ +using Microsoft.EntityFrameworkCore; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Interfaces; +using Aquiis.Professional.Shared.Services; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing calendar events and synchronizing with schedulable entities + /// + public class CalendarEventService : ICalendarEventService + { + private readonly ApplicationDbContext _context; + private readonly CalendarSettingsService _settingsService; + private readonly UserContextService _userContextService; + + public CalendarEventService(ApplicationDbContext context, CalendarSettingsService settingsService, UserContextService userContext) + { + _context = context; + _settingsService = settingsService; + _userContextService = userContext; + } + + /// + /// Create or update a calendar event from a schedulable entity + /// + public async Task CreateOrUpdateEventAsync(T entity) + where T : BaseModel, ISchedulableEntity + { + var entityType = entity.GetEventType(); + + // Check if auto-creation is enabled for this entity type + var isEnabled = await _settingsService.IsAutoCreateEnabledAsync( + entity.OrganizationId, + entityType + ); + + if (!isEnabled) + { + // If disabled and event exists, delete it + if (entity.CalendarEventId.HasValue) + { + await DeleteEventAsync(entity.CalendarEventId); + entity.CalendarEventId = null; + await _context.SaveChangesAsync(); + } + return null; + } + + CalendarEvent? calendarEvent; + + if (entity.CalendarEventId.HasValue) + { + // Update existing event + calendarEvent = await _context.CalendarEvents + .FindAsync(entity.CalendarEventId.Value); + + if (calendarEvent != null) + { + UpdateEventFromEntity(calendarEvent, entity); + } + else + { + // Event was deleted, create new one + calendarEvent = CreateEventFromEntity(entity); + _context.CalendarEvents.Add(calendarEvent); + } + } + else + { + // Create new event + calendarEvent = CreateEventFromEntity(entity); + _context.CalendarEvents.Add(calendarEvent); + } + + await _context.SaveChangesAsync(); + + // Link back to entity if not already linked + if (!entity.CalendarEventId.HasValue) + { + entity.CalendarEventId = calendarEvent.Id; + await _context.SaveChangesAsync(); + } + + return calendarEvent; + } + + /// + /// Delete a calendar event + /// + public async Task DeleteEventAsync(Guid? calendarEventId) + { + if (!calendarEventId.HasValue) return; + + var evt = await _context.CalendarEvents.FindAsync(calendarEventId.Value); + if (evt != null) + { + _context.CalendarEvents.Remove(evt); + await _context.SaveChangesAsync(); + } + } + + /// + /// Get calendar events for a date range with optional filtering + /// + public async Task> GetEventsAsync( + DateTime startDate, + DateTime endDate, + List? eventTypes = null) + { + var organizationId = await _userContextService.GetActiveOrganizationIdAsync(); + var query = _context.CalendarEvents + .Include(e => e.Property) + .Where(e => e.OrganizationId == organizationId + && e.StartOn >= startDate + && e.StartOn <= endDate + && !e.IsDeleted); + + if (eventTypes?.Any() == true) + { + query = query.Where(e => eventTypes.Contains(e.EventType)); + } + + return await query.OrderBy(e => e.StartOn).ToListAsync(); + } + + /// + /// Get a specific calendar event by ID + /// + public async Task GetEventByIdAsync(Guid eventId) + { + var organizationId = await _userContextService.GetActiveOrganizationIdAsync(); + return await _context.CalendarEvents + .Include(e => e.Property) + .FirstOrDefaultAsync(e => e.Id == eventId + && e.OrganizationId == organizationId + && !e.IsDeleted); + } + + /// + /// Create a custom calendar event (not linked to a domain entity) + /// + public async Task CreateCustomEventAsync(CalendarEvent calendarEvent) + { + calendarEvent.EventType = CalendarEventTypes.Custom; + calendarEvent.SourceEntityId = null; + calendarEvent.SourceEntityType = null; + calendarEvent.Color = CalendarEventTypes.GetColor(CalendarEventTypes.Custom); + calendarEvent.Icon = CalendarEventTypes.GetIcon(CalendarEventTypes.Custom); + calendarEvent.CreatedOn = DateTime.UtcNow; + + _context.CalendarEvents.Add(calendarEvent); + await _context.SaveChangesAsync(); + + return calendarEvent; + } + + /// + /// Update a custom calendar event + /// + public async Task UpdateCustomEventAsync(CalendarEvent calendarEvent) + { + var existing = await _context.CalendarEvents + .FirstOrDefaultAsync(e => e.Id == calendarEvent.Id + && e.OrganizationId == calendarEvent.OrganizationId + && e.SourceEntityType == null + && !e.IsDeleted); + + if (existing == null) return null; + + existing.Title = calendarEvent.Title; + existing.StartOn = calendarEvent.StartOn; + existing.EndOn = calendarEvent.EndOn; + existing.DurationMinutes = calendarEvent.DurationMinutes; + existing.Description = calendarEvent.Description; + existing.PropertyId = calendarEvent.PropertyId; + existing.Location = calendarEvent.Location; + existing.Status = calendarEvent.Status; + existing.LastModifiedBy = calendarEvent.LastModifiedBy; + existing.LastModifiedOn = calendarEvent.LastModifiedOn; + + await _context.SaveChangesAsync(); + + return existing; + } + + /// + /// Get all calendar events for a specific property + /// + public async Task> GetEventsByPropertyIdAsync(Guid propertyId) + { + var organizationId = await _userContextService.GetActiveOrganizationIdAsync(); + return await _context.CalendarEvents + .Include(e => e.Property) + .Where(e => e.PropertyId == propertyId + && e.OrganizationId == organizationId + && !e.IsDeleted) + .OrderByDescending(e => e.StartOn) + .ToListAsync(); + } + + /// + /// Get upcoming events for the next N days + /// + public async Task> GetUpcomingEventsAsync( + int days = 7, + List? eventTypes = null) + { + var startDate = DateTime.Today; + var endDate = DateTime.Today.AddDays(days); + return await GetEventsAsync(startDate, endDate, eventTypes); + } + + /// + /// Create a CalendarEvent from a schedulable entity + /// + private CalendarEvent CreateEventFromEntity(T entity) + where T : BaseModel, ISchedulableEntity + { + var eventType = entity.GetEventType(); + + return new CalendarEvent + { + Id = Guid.NewGuid(), + Title = entity.GetEventTitle(), + StartOn = entity.GetEventStart(), + DurationMinutes = entity.GetEventDuration(), + EventType = eventType, + Status = entity.GetEventStatus(), + Description = entity.GetEventDescription(), + PropertyId = entity.GetPropertyId(), + Color = CalendarEventTypes.GetColor(eventType), + Icon = CalendarEventTypes.GetIcon(eventType), + SourceEntityId = entity.Id, + SourceEntityType = typeof(T).Name, + OrganizationId = entity.OrganizationId, + CreatedBy = entity.CreatedBy, + CreatedOn = DateTime.UtcNow + }; + } + + /// + /// Update a CalendarEvent from a schedulable entity + /// + private void UpdateEventFromEntity(CalendarEvent evt, T entity) + where T : ISchedulableEntity + { + evt.Title = entity.GetEventTitle(); + evt.StartOn = entity.GetEventStart(); + evt.DurationMinutes = entity.GetEventDuration(); + evt.EventType = entity.GetEventType(); + evt.Status = entity.GetEventStatus(); + evt.Description = entity.GetEventDescription(); + evt.PropertyId = entity.GetPropertyId(); + evt.Color = CalendarEventTypes.GetColor(entity.GetEventType()); + evt.Icon = CalendarEventTypes.GetIcon(entity.GetEventType()); + } + } +} diff --git a/Aquiis.Professional/Application/Services/CalendarSettingsService.cs b/Aquiis.Professional/Application/Services/CalendarSettingsService.cs new file mode 100644 index 0000000..117a626 --- /dev/null +++ b/Aquiis.Professional/Application/Services/CalendarSettingsService.cs @@ -0,0 +1,137 @@ +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Utilities; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Professional.Application.Services; + +public class CalendarSettingsService +{ + private readonly ApplicationDbContext _context; + private readonly UserContextService _userContext; + + public CalendarSettingsService(ApplicationDbContext context, UserContextService userContext) + { + _context = context; + _userContext = userContext; + } + + public async Task> GetSettingsAsync(Guid organizationId) + { + await EnsureDefaultsAsync(organizationId); + + return await _context.CalendarSettings + .Where(s => s.OrganizationId == organizationId && !s.IsDeleted) + .OrderBy(s => s.DisplayOrder) + .ThenBy(s => s.EntityType) + .ToListAsync(); + } + + public async Task GetSettingAsync(Guid organizationId, string entityType) + { + var setting = await _context.CalendarSettings + .FirstOrDefaultAsync(s => s.OrganizationId == organizationId + && s.EntityType == entityType + && !s.IsDeleted); + + if (setting == null) + { + // Create default if missing + setting = CreateDefaultSetting(organizationId, entityType); + _context.CalendarSettings.Add(setting); + await _context.SaveChangesAsync(); + } + + return setting; + } + + public async Task UpdateSettingAsync(CalendarSettings setting) + { + var userId = await _userContext.GetUserIdAsync(); + setting.LastModifiedOn = DateTime.UtcNow; + setting.LastModifiedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + + _context.CalendarSettings.Update(setting); + await _context.SaveChangesAsync(); + + return setting; + } + + public async Task IsAutoCreateEnabledAsync(Guid organizationId, string entityType) + { + var setting = await _context.CalendarSettings + .FirstOrDefaultAsync(s => s.OrganizationId == organizationId + && s.EntityType == entityType + && !s.IsDeleted); + + // Default to true if no setting exists + return setting?.AutoCreateEvents ?? true; + } + + public async Task EnsureDefaultsAsync(Guid organizationId) + { + var userId = await _userContext.GetUserIdAsync(); + var entityTypes = SchedulableEntityRegistry.GetEntityTypeNames(); + var existingSettings = await _context.CalendarSettings + .Where(s => s.OrganizationId == organizationId && !s.IsDeleted) + .Select(s => s.EntityType) + .ToListAsync(); + + var missingTypes = entityTypes.Except(existingSettings).ToList(); + + if (missingTypes.Any()) + { + var newSettings = missingTypes.Select((entityType, index) => + { + var setting = CreateDefaultSetting(organizationId, entityType); + setting.DisplayOrder = existingSettings.Count + index; + setting.CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + setting.LastModifiedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + return setting; + }).ToList(); + + _context.CalendarSettings.AddRange(newSettings); + await _context.SaveChangesAsync(); + } + } + + private CalendarSettings CreateDefaultSetting(Guid organizationId, string entityType) + { + // Get defaults from CalendarEventTypes if available + var config = CalendarEventTypes.Config.ContainsKey(entityType) + ? CalendarEventTypes.Config[entityType] + : null; + + var userId = _userContext.GetUserIdAsync().Result; + return new CalendarSettings + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + EntityType = entityType, + AutoCreateEvents = true, + ShowOnCalendar = true, + DefaultColor = config?.Color, + DefaultIcon = config?.Icon, + DisplayOrder = 0, + CreatedOn = DateTime.UtcNow, + LastModifiedOn = DateTime.UtcNow + }; + } + + public async Task> UpdateMultipleSettingsAsync(List settings) + { + var userId = await _userContext.GetUserIdAsync(); + var now = DateTime.UtcNow; + + foreach (var setting in settings) + { + setting.LastModifiedOn = now; + setting.LastModifiedBy = userId; + _context.CalendarSettings.Update(setting); + } + + await _context.SaveChangesAsync(); + return settings; + } +} diff --git a/Aquiis.Professional/Application/Services/ChecklistService.cs b/Aquiis.Professional/Application/Services/ChecklistService.cs new file mode 100644 index 0000000..a1e005d --- /dev/null +++ b/Aquiis.Professional/Application/Services/ChecklistService.cs @@ -0,0 +1,654 @@ +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Shared.Services; +using Aquiis.Professional.Core.Constants; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Professional.Application.Services +{ + public class ChecklistService + { + private readonly ApplicationDbContext _dbContext; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly UserContextService _userContext; + + public ChecklistService( + ApplicationDbContext dbContext, + IHttpContextAccessor httpContextAccessor, + UserContextService userContext) + { + _dbContext = dbContext; + _httpContextAccessor = httpContextAccessor; + _userContext = userContext; + } + + #region ChecklistTemplates + + public async Task> GetChecklistTemplatesAsync() + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.ChecklistTemplates + .Include(ct => ct.Items.OrderBy(i => i.ItemOrder)) + .Where(ct => !ct.IsDeleted && (ct.OrganizationId == organizationId || ct.IsSystemTemplate)) + .OrderBy(ct => ct.Name) + .ToListAsync(); + } + + public async Task GetChecklistTemplateByIdAsync(Guid templateId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.ChecklistTemplates + .Include(ct => ct.Items.OrderBy(i => i.ItemOrder)) + .FirstOrDefaultAsync(ct => ct.Id == templateId && !ct.IsDeleted && + (ct.OrganizationId == organizationId || ct.IsSystemTemplate)); + } + + public async Task AddChecklistTemplateAsync(ChecklistTemplate template) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Check for duplicate template name within organization + var existingTemplate = await _dbContext.ChecklistTemplates + .FirstOrDefaultAsync(t => t.Name == template.Name && + t.OrganizationId == organizationId && + !t.IsDeleted); + + if (existingTemplate != null) + { + throw new InvalidOperationException($"A template named '{template.Name}' already exists."); + } + + template.OrganizationId = organizationId!.Value; + template.CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + template.CreatedOn = DateTime.UtcNow; + + _dbContext.ChecklistTemplates.Add(template); + await _dbContext.SaveChangesAsync(); + + return template; + } + + public async Task UpdateChecklistTemplateAsync(ChecklistTemplate template) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + template.LastModifiedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + template.LastModifiedOn = DateTime.UtcNow; + + _dbContext.ChecklistTemplates.Update(template); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteChecklistTemplateAsync(Guid templateId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var template = await _dbContext.ChecklistTemplates.FindAsync(templateId); + if (template != null && !template.IsSystemTemplate) + { + template.IsDeleted = true; + template.LastModifiedBy = userId; + template.LastModifiedOn = DateTime.UtcNow; + await _dbContext.SaveChangesAsync(); + } + } + + #endregion + + #region ChecklistTemplateItems + + public async Task AddChecklistTemplateItemAsync(ChecklistTemplateItem item) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + item.Id = Guid.NewGuid(); + item.OrganizationId = organizationId!.Value; + item.CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + item.CreatedOn = DateTime.UtcNow; + + _dbContext.ChecklistTemplateItems.Add(item); + await _dbContext.SaveChangesAsync(); + + return item; + } + + public async Task UpdateChecklistTemplateItemAsync(ChecklistTemplateItem item) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + item.LastModifiedBy = userId; + item.LastModifiedOn = DateTime.UtcNow; + + _dbContext.ChecklistTemplateItems.Update(item); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteChecklistTemplateItemAsync(Guid itemId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var item = await _dbContext.ChecklistTemplateItems.FindAsync(itemId); + if (item != null) + { + _dbContext.ChecklistTemplateItems.Remove(item); + await _dbContext.SaveChangesAsync(); + } + } + + #endregion + + #region Checklists + + public async Task> GetChecklistsAsync(bool includeArchived = false) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var query = _dbContext.Checklists + .Include(c => c.Property) + .Include(c => c.Lease) + .Include(c => c.ChecklistTemplate) + .Include(c => c.Items.OrderBy(i => i.ItemOrder)) + .Where(c => c.OrganizationId == organizationId); + + if (includeArchived) + { + // Show only archived (soft deleted) checklists + query = query.Where(c => c.IsDeleted); + } + else + { + // Show only active (not archived) checklists + query = query.Where(c => !c.IsDeleted); + } + + return await query.OrderByDescending(c => c.CreatedOn).ToListAsync(); + } + + public async Task> GetChecklistsByPropertyIdAsync(Guid propertyId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Checklists + .Include(c => c.Property) + .Include(c => c.Lease) + .Include(c => c.ChecklistTemplate) + .Include(c => c.Items.OrderBy(i => i.ItemOrder)) + .Where(c => !c.IsDeleted && c.OrganizationId == organizationId && c.PropertyId == propertyId) + .OrderByDescending(c => c.CreatedOn) + .ToListAsync(); + } + + public async Task> GetChecklistsByLeaseIdAsync(Guid leaseId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Checklists + .Include(c => c.Property) + .Include(c => c.Lease) + .Include(c => c.ChecklistTemplate) + .Include(c => c.Items.OrderBy(i => i.ItemOrder)) + .Where(c => !c.IsDeleted && c.OrganizationId == organizationId && c.LeaseId == leaseId) + .OrderByDescending(c => c.CreatedOn) + .ToListAsync(); + } + + public async Task GetChecklistByIdAsync(Guid checklistId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Checklists + .Include(c => c.Property) + .Include(c => c.Lease) + .ThenInclude(l => l!.Tenant) + .Include(c => c.ChecklistTemplate) + .Include(c => c.Items.OrderBy(i => i.ItemOrder)) + .Include(c => c.Document) + .FirstOrDefaultAsync(c => c.Id == checklistId && !c.IsDeleted && c.OrganizationId == organizationId); + } + + /// + /// Creates a new checklist instance from a template, including all template items + /// + public async Task CreateChecklistFromTemplateAsync(Guid templateId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Get the template with items + var template = await GetChecklistTemplateByIdAsync(templateId); + if (template == null) + { + throw new InvalidOperationException("Template not found."); + } + + // Create the checklist from template + var checklist = new Checklist + { + Id = Guid.NewGuid(), + Name = template.Name, + ChecklistType = template.Category, + ChecklistTemplateId = template.Id, + Status = ApplicationConstants.ChecklistStatuses.Draft, + OrganizationId = organizationId!.Value, + CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, + CreatedOn = DateTime.UtcNow + }; + + _dbContext.Checklists.Add(checklist); + await _dbContext.SaveChangesAsync(); + + // Create checklist items from template items + foreach (var templateItem in template.Items) + { + var checklistItem = new ChecklistItem + { + Id = Guid.NewGuid(), + ChecklistId = checklist.Id, + ItemText = templateItem.ItemText, + ItemOrder = templateItem.ItemOrder, + CategorySection = templateItem.CategorySection, + SectionOrder = templateItem.SectionOrder, + RequiresValue = templateItem.RequiresValue, + IsChecked = false, + OrganizationId = organizationId!.Value, + CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, + CreatedOn = DateTime.UtcNow + }; + _dbContext.ChecklistItems.Add(checklistItem); + } + + await _dbContext.SaveChangesAsync(); + + // Return checklist with items already loaded in memory + checklist.Items = await _dbContext.ChecklistItems + .Where(i => i.ChecklistId == checklist.Id) + .OrderBy(i => i.SectionOrder) + .ThenBy(i => i.ItemOrder) + .ToListAsync(); + + return checklist; + } + + public async Task AddChecklistAsync(Checklist checklist) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + checklist.Id = Guid.NewGuid(); + checklist.OrganizationId = organizationId!.Value; + checklist.CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + checklist.CreatedOn = DateTime.UtcNow; + + _dbContext.Checklists.Add(checklist); + await _dbContext.SaveChangesAsync(); + + return checklist; + } + + public async Task UpdateChecklistAsync(Checklist checklist) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + checklist.LastModifiedBy = userId; + checklist.LastModifiedOn = DateTime.UtcNow; + + _dbContext.Checklists.Update(checklist); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteChecklistAsync(Guid checklistId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var checklist = await _dbContext.Checklists + .Include(c => c.Items) + .FirstOrDefaultAsync(c => c.Id == checklistId && c.OrganizationId == organizationId); + + if (checklist != null) + { + // Completed checklists cannot be deleted, only archived + if (checklist.Status == "Completed") + { + throw new InvalidOperationException("Completed checklists cannot be deleted. Please archive them instead."); + } + + // Hard delete - remove items first, then checklist + _dbContext.ChecklistItems.RemoveRange(checklist.Items); + _dbContext.Checklists.Remove(checklist); + await _dbContext.SaveChangesAsync(); + } + } + + public async Task ArchiveChecklistAsync(Guid checklistId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var checklist = await _dbContext.Checklists + .FirstOrDefaultAsync(c => c.Id == checklistId && c.OrganizationId == organizationId!); + + if (checklist != null) + { + checklist.IsDeleted = true; + checklist.LastModifiedBy = userId; + checklist.LastModifiedOn = DateTime.UtcNow; + await _dbContext.SaveChangesAsync(); + } + } + + public async Task UnarchiveChecklistAsync(Guid checklistId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var checklist = await _dbContext.Checklists + .FirstOrDefaultAsync(c => c.Id == checklistId && c.OrganizationId == organizationId!); + + if (checklist != null) + { + checklist.IsDeleted = false; + checklist.LastModifiedBy = userId; + checklist.LastModifiedOn = DateTime.UtcNow; + await _dbContext.SaveChangesAsync(); + } + } + + public async Task CompleteChecklistAsync(Guid checklistId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var checklist = await _dbContext.Checklists.FindAsync(checklistId); + if (checklist != null) + { + checklist.Status = "Completed"; + checklist.CompletedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + checklist.CompletedOn = DateTime.UtcNow; + checklist.LastModifiedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + checklist.LastModifiedOn = DateTime.UtcNow; + await _dbContext.SaveChangesAsync(); + + // Check if this is a Property Tour checklist linked to a tour + var tour = await _dbContext.Tours + .Include(s => s.ProspectiveTenant) + .FirstOrDefaultAsync(s => s.ChecklistId == checklistId && !s.IsDeleted); + + if (tour != null) + { + // Mark tour as completed + tour.Status = ApplicationConstants.TourStatuses.Completed; + tour.ConductedBy = userId; + tour.LastModifiedBy = userId; + tour.LastModifiedOn = DateTime.UtcNow; + + // Update calendar event status + if (tour.CalendarEventId.HasValue) + { + var calendarEvent = await _dbContext.CalendarEvents + .FirstOrDefaultAsync(e => e.Id == tour.CalendarEventId.Value); + if (calendarEvent != null) + { + calendarEvent.Status = ApplicationConstants.TourStatuses.Completed; + calendarEvent.LastModifiedBy = userId; + calendarEvent.LastModifiedOn = DateTime.UtcNow; + } + } + + // Update prospect status back to Lead (tour completed, awaiting application) + if (tour.ProspectiveTenant != null && + tour.ProspectiveTenant.Status == ApplicationConstants.ProspectiveStatuses.TourScheduled) + { + // Check if they have other scheduled tours + var hasOtherScheduledTours = await _dbContext.Tours + .AnyAsync(s => s.ProspectiveTenantId == tour.ProspectiveTenantId + && s.Id != tour.Id + && !s.IsDeleted + && s.Status == ApplicationConstants.TourStatuses.Scheduled); + + // Only revert to Lead if no other scheduled tours + if (!hasOtherScheduledTours) + { + tour.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.Lead; + tour.ProspectiveTenant.LastModifiedBy = userId; + tour.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + } + + await _dbContext.SaveChangesAsync(); + } + } + } + + public async Task SaveChecklistAsTemplateAsync(Guid checklistId, string templateName, string? templateDescription = null) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Check for duplicate template name + var existingTemplate = await _dbContext.ChecklistTemplates + .FirstOrDefaultAsync(t => t.Name == templateName && + t.OrganizationId == organizationId! && + !t.IsDeleted); + + if (existingTemplate != null) + { + throw new InvalidOperationException($"A template named '{templateName}' already exists. Please choose a different name."); + } + + // Get the checklist with its items + var checklist = await _dbContext.Checklists + .Include(c => c.Items.OrderBy(i => i.ItemOrder)) + .FirstOrDefaultAsync(c => c.Id == checklistId && c.OrganizationId == organizationId!); + + if (checklist == null) + { + throw new InvalidOperationException("Checklist not found."); + } + + // Create new template + var template = new ChecklistTemplate + { + Name = templateName, + Description = templateDescription ?? $"Template created from checklist: {checklist.Name}", + Category = checklist.ChecklistType, + IsSystemTemplate = false, + OrganizationId = organizationId!.Value, + CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, + CreatedOn = DateTime.UtcNow + }; + + _dbContext.ChecklistTemplates.Add(template); + await _dbContext.SaveChangesAsync(); + + // Copy items to template + foreach (var item in checklist.Items) + { + var templateItem = new ChecklistTemplateItem + { + Id = Guid.NewGuid(), + ChecklistTemplateId = template.Id, + ItemText = item.ItemText, + ItemOrder = item.ItemOrder, + CategorySection = item.CategorySection, + SectionOrder = item.SectionOrder, + IsRequired = false, // User can customize this later + RequiresValue = item.RequiresValue, + AllowsNotes = true, + OrganizationId = organizationId!.Value, + CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, + CreatedOn = DateTime.UtcNow + }; + + _dbContext.ChecklistTemplateItems.Add(templateItem); + } + + await _dbContext.SaveChangesAsync(); + + return template; + } + + #endregion + + #region ChecklistItems + + public async Task AddChecklistItemAsync(ChecklistItem item) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + item.Id = Guid.NewGuid(); + item.OrganizationId = organizationId!.Value; + item.CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty; + item.CreatedOn = DateTime.UtcNow; + + _dbContext.ChecklistItems.Add(item); + await _dbContext.SaveChangesAsync(); + + return item; + } + + public async Task UpdateChecklistItemAsync(ChecklistItem item) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + item.LastModifiedBy = userId; + item.LastModifiedOn = DateTime.UtcNow; + + _dbContext.ChecklistItems.Update(item); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteChecklistItemAsync(Guid itemId) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var item = await _dbContext.ChecklistItems.FindAsync(itemId); + if (item != null) + { + item.IsDeleted = true; + item.LastModifiedBy = userId; + item.LastModifiedOn = DateTime.UtcNow; + await _dbContext.SaveChangesAsync(); + } + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/DocumentService.cs b/Aquiis.Professional/Application/Services/DocumentService.cs new file mode 100644 index 0000000..d6f993a --- /dev/null +++ b/Aquiis.Professional/Application/Services/DocumentService.cs @@ -0,0 +1,432 @@ +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Aquiis.Professional.Application.Services.PdfGenerators; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing Document entities. + /// Inherits common CRUD operations from BaseService and adds document-specific business logic. + /// + public class DocumentService : BaseService + { + public DocumentService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings) + : base(context, logger, userContext, settings) + { + } + + #region Overrides with Document-Specific Logic + + /// + /// Validates a document entity before create/update operations. + /// + protected override async Task ValidateEntityAsync(Document entity) + { + var errors = new List(); + + // Required field validation + if (string.IsNullOrWhiteSpace(entity.FileName)) + { + errors.Add("FileName is required"); + } + + if (string.IsNullOrWhiteSpace(entity.FileExtension)) + { + errors.Add("FileExtension is required"); + } + + if (string.IsNullOrWhiteSpace(entity.DocumentType)) + { + errors.Add("DocumentType is required"); + } + + if (entity.FileData == null || entity.FileData.Length == 0) + { + errors.Add("FileData is required"); + } + + // Business rule: At least one foreign key must be set + if (!entity.PropertyId.HasValue + && !entity.TenantId.HasValue + && !entity.LeaseId.HasValue + && !entity.InvoiceId.HasValue + && !entity.PaymentId.HasValue) + { + errors.Add("Document must be associated with at least one entity (Property, Tenant, Lease, Invoice, or Payment)"); + } + + // Validate file size (e.g., max 10MB) + const long maxFileSizeBytes = 10 * 1024 * 1024; // 10MB + if (entity.FileSize > maxFileSizeBytes) + { + errors.Add($"File size exceeds maximum allowed size of {maxFileSizeBytes / (1024 * 1024)}MB"); + } + + if (errors.Any()) + { + throw new ValidationException(string.Join("; ", errors)); + } + + await base.ValidateEntityAsync(entity); + } + + #endregion + + #region Retrieval Methods + + /// + /// Gets a document with all related entities. + /// + public async Task GetDocumentWithRelationsAsync(Guid documentId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var document = await _context.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .ThenInclude(l => l!.Property) + .Include(d => d.Lease) + .ThenInclude(l => l!.Tenant) + .Include(d => d.Invoice) + .Include(d => d.Payment) + .Where(d => d.Id == documentId + && !d.IsDeleted + && d.OrganizationId == organizationId) + .FirstOrDefaultAsync(); + + return document; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentWithRelations"); + throw; + } + } + + /// + /// Gets all documents with related entities. + /// + public async Task> GetDocumentsWithRelationsAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .ThenInclude(l => l!.Property) + .Include(d => d.Lease) + .ThenInclude(l => l!.Tenant) + .Include(d => d.Invoice) + .Include(d => d.Payment) + .Where(d => !d.IsDeleted && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentsWithRelations"); + throw; + } + } + + #endregion + + #region Business Logic Methods + + /// + /// Gets all documents for a specific property. + /// + public async Task> GetDocumentsByPropertyIdAsync(Guid propertyId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .Where(d => d.PropertyId == propertyId + && !d.IsDeleted + && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentsByPropertyId"); + throw; + } + } + + /// + /// Gets all documents for a specific tenant. + /// + public async Task> GetDocumentsByTenantIdAsync(Guid tenantId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .Where(d => d.TenantId == tenantId + && !d.IsDeleted + && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentsByTenantId"); + throw; + } + } + + /// + /// Gets all documents for a specific lease. + /// + public async Task> GetDocumentsByLeaseIdAsync(Guid leaseId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Include(d => d.Lease) + .ThenInclude(l => l!.Property) + .Include(d => d.Lease) + .ThenInclude(l => l!.Tenant) + .Where(d => d.LeaseId == leaseId + && !d.IsDeleted + && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentsByLeaseId"); + throw; + } + } + + /// + /// Gets all documents for a specific invoice. + /// + public async Task> GetDocumentsByInvoiceIdAsync(Guid invoiceId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Include(d => d.Invoice) + .Where(d => d.InvoiceId == invoiceId + && !d.IsDeleted + && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentsByInvoiceId"); + throw; + } + } + + /// + /// Gets all documents for a specific payment. + /// + public async Task> GetDocumentsByPaymentIdAsync(Guid paymentId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Include(d => d.Payment) + .Where(d => d.PaymentId == paymentId + && !d.IsDeleted + && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentsByPaymentId"); + throw; + } + } + + /// + /// Gets documents by document type (e.g., "Lease Agreement", "Invoice", "Receipt"). + /// + public async Task> GetDocumentsByTypeAsync(string documentType) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .Where(d => d.DocumentType == documentType + && !d.IsDeleted + && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentsByType"); + throw; + } + } + + /// + /// Searches documents by filename. + /// + public async Task> SearchDocumentsByFilenameAsync(string searchTerm, int maxResults = 20) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrWhiteSpace(searchTerm)) + { + // Return recent documents if no search term + return await _context.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .Where(d => !d.IsDeleted && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .Take(maxResults) + .ToListAsync(); + } + + var searchLower = searchTerm.ToLower(); + + return await _context.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .Where(d => !d.IsDeleted + && d.OrganizationId == organizationId + && (d.FileName.ToLower().Contains(searchLower) + || d.Description.ToLower().Contains(searchLower))) + .OrderByDescending(d => d.CreatedOn) + .Take(maxResults) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "SearchDocumentsByFilename"); + throw; + } + } + + /// + /// Calculates total storage used by all documents in the organization (in bytes). + /// + public async Task CalculateTotalStorageUsedAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Where(d => !d.IsDeleted && d.OrganizationId == organizationId) + .SumAsync(d => d.FileSize); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "CalculateTotalStorageUsed"); + throw; + } + } + + /// + /// Gets documents uploaded within a specific date range. + /// + public async Task> GetDocumentsByDateRangeAsync(DateTime startDate, DateTime endDate) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .Where(d => !d.IsDeleted + && d.OrganizationId == organizationId + && d.CreatedOn >= startDate + && d.CreatedOn <= endDate) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentsByDateRange"); + throw; + } + } + + /// + /// Gets document count by document type for reporting. + /// + public async Task> GetDocumentCountByTypeAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Documents + .Where(d => !d.IsDeleted && d.OrganizationId == organizationId) + .GroupBy(d => d.DocumentType) + .Select(g => new { Type = g.Key, Count = g.Count() }) + .ToDictionaryAsync(x => x.Type, x => x.Count); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetDocumentCountByType"); + throw; + } + } + + #endregion + + #region PDF Generation Methods + + /// + /// Generates a lease document PDF. + /// + public async Task GenerateLeaseDocumentAsync(Lease lease) + { + return await LeasePdfGenerator.GenerateLeasePdf(lease); + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/EmailSettingsService.cs b/Aquiis.Professional/Application/Services/EmailSettingsService.cs new file mode 100644 index 0000000..cb3dc47 --- /dev/null +++ b/Aquiis.Professional/Application/Services/EmailSettingsService.cs @@ -0,0 +1,164 @@ +using System; +using System.Threading.Tasks; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Interfaces.Services; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Infrastructure.Services; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SendGrid; +using SendGrid.Helpers.Mail; + +namespace Aquiis.Professional.Application.Services +{ + public class EmailSettingsService : BaseService + { + private readonly SendGridEmailService _emailService; + + public EmailSettingsService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings, + SendGridEmailService emailService) + : base(context, logger, userContext, settings) + { + _emailService = emailService; + } + + /// + /// Get email settings for current organization or create default disabled settings + /// + public async Task GetOrCreateSettingsAsync() + { + var orgId = await _userContext.GetActiveOrganizationIdAsync(); + if (orgId == null) + { + throw new UnauthorizedAccessException("No active organization"); + } + + var settings = await _dbSet + .FirstOrDefaultAsync(s => s.OrganizationId == orgId && !s.IsDeleted); + + if (settings == null) + { + settings = new OrganizationEmailSettings + { + Id = Guid.NewGuid(), + OrganizationId = orgId.Value, + IsEmailEnabled = false, + DailyLimit = 100, // SendGrid free tier default + MonthlyLimit = 40000, + CreatedBy = await _userContext.GetUserIdAsync() ?? string.Empty, + CreatedOn = DateTime.UtcNow + }; + await CreateAsync(settings); + } + + return settings; + } + + /// + /// Configure SendGrid API key and enable email functionality + /// + public async Task UpdateSendGridConfigAsync( + string apiKey, + string fromEmail, + string fromName) + { + // Verify the API key works before saving + if (!await _emailService.VerifyApiKeyAsync(apiKey)) + { + return OperationResult.FailureResult( + "Invalid SendGrid API key. Please verify the key has Mail Send permissions."); + } + + var settings = await GetOrCreateSettingsAsync(); + + settings.SendGridApiKeyEncrypted = _emailService.EncryptApiKey(apiKey); + settings.FromEmail = fromEmail; + settings.FromName = fromName; + settings.IsEmailEnabled = true; + settings.IsVerified = true; + settings.LastVerifiedOn = DateTime.UtcNow; + settings.LastError = null; + + await UpdateAsync(settings); + + return OperationResult.SuccessResult("SendGrid configuration saved successfully"); + } + + /// + /// Disable email functionality for organization + /// + public async Task DisableEmailAsync() + { + var settings = await GetOrCreateSettingsAsync(); + settings.IsEmailEnabled = false; + await UpdateAsync(settings); + + return OperationResult.SuccessResult("Email notifications disabled"); + } + + /// + /// Re-enable email functionality + /// + public async Task EnableEmailAsync() + { + var settings = await GetOrCreateSettingsAsync(); + + if (string.IsNullOrEmpty(settings.SendGridApiKeyEncrypted)) + { + return OperationResult.FailureResult( + "SendGrid API key not configured. Please configure SendGrid first."); + } + + settings.IsEmailEnabled = true; + await UpdateAsync(settings); + + return OperationResult.SuccessResult("Email notifications enabled"); + } + + /// + /// Send a test email to verify configuration + /// + public async Task TestEmailConfigurationAsync(string testEmail) + { + try + { + await _emailService.SendEmailAsync( + testEmail, + "Aquiis Email Configuration Test", + "

Configuration Test Successful!

" + + "

This is a test email to verify your SendGrid configuration is working correctly.

" + + "

If you received this email, your email integration is properly configured.

"); + + return OperationResult.SuccessResult("Test email sent successfully! Check your inbox."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Test email failed"); + return OperationResult.FailureResult($"Failed to send test email: {ex.Message}"); + } + } + + /// + /// Update email sender information + /// + public async Task UpdateSenderInfoAsync(string fromEmail, string fromName) + { + var settings = await GetOrCreateSettingsAsync(); + + settings.FromEmail = fromEmail; + settings.FromName = fromName; + + await UpdateAsync(settings); + + return OperationResult.SuccessResult("Sender information updated"); + } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Application/Services/FinancialReportService.cs b/Aquiis.Professional/Application/Services/FinancialReportService.cs new file mode 100644 index 0000000..70ad8bd --- /dev/null +++ b/Aquiis.Professional/Application/Services/FinancialReportService.cs @@ -0,0 +1,287 @@ +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Professional.Application.Services; + +public class FinancialReportService +{ + private readonly IDbContextFactory _contextFactory; + + public FinancialReportService(IDbContextFactory contextFactory) + { + _contextFactory = contextFactory; + } + + /// + /// Generate income statement for a specific period and optional property + /// + public async Task GenerateIncomeStatementAsync( + Guid organizationId, + DateTime startDate, + DateTime endDate, + Guid? propertyId = null) + { + using var context = await _contextFactory.CreateDbContextAsync(); + + var statement = new IncomeStatement + { + StartDate = startDate, + EndDate = endDate, + PropertyId = propertyId + }; + + // Get property name if filtering by property + if (propertyId.HasValue) + { + var property = await context.Properties + .Where(p => p.Id == propertyId.Value && p.OrganizationId == organizationId) + .FirstOrDefaultAsync(); + statement.PropertyName = property?.Address; + } + + // Calculate total rent income from payments (all payments are rent payments) + var paymentsQuery = context.Payments + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .Where(p => p.Invoice.Lease.Property.OrganizationId == organizationId && + p.PaidOn >= startDate && + p.PaidOn <= endDate); + + if (propertyId.HasValue) + { + paymentsQuery = paymentsQuery.Where(p => p.Invoice.Lease.PropertyId == propertyId.Value); + } + + var totalPayments = await paymentsQuery.SumAsync(p => p.Amount); + statement.TotalRentIncome = totalPayments; + statement.TotalOtherIncome = 0; // No other income tracked currently + + // Get maintenance expenses (this is the ONLY expense type tracked) + var maintenanceQuery = context.MaintenanceRequests + .Where(m => m.CompletedOn.HasValue && + m.CompletedOn.Value >= startDate && + m.CompletedOn.Value <= endDate && + m.Status == "Completed" && + m.ActualCost > 0); + + if (propertyId.HasValue) + { + maintenanceQuery = maintenanceQuery.Where(m => m.PropertyId == propertyId.Value); + } + else + { + // For all properties, need to filter by user's properties + var userPropertyIds = await context.Properties + .Where(p => p.OrganizationId == organizationId) + .Select(p => p.Id) + .ToListAsync(); + maintenanceQuery = maintenanceQuery.Where(m => userPropertyIds.Contains(m.PropertyId)); + } + + var maintenanceRequests = await maintenanceQuery.ToListAsync(); + + // All maintenance costs go to MaintenanceExpenses + statement.MaintenanceExpenses = maintenanceRequests.Sum(m => m.ActualCost); + + // Other expense categories are currently zero (no data tracked for these yet) + statement.UtilityExpenses = 0; + statement.InsuranceExpenses = 0; + statement.TaxExpenses = 0; + statement.ManagementFees = 0; + statement.OtherExpenses = 0; + + return statement; + } + + /// + /// Generate rent roll report showing all properties and tenants + /// + public async Task> GenerateRentRollAsync(Guid organizationId, DateTime asOfDate) + { + using var context = await _contextFactory.CreateDbContextAsync(); + + var rentRoll = await context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Include(l => l.Invoices) + .ThenInclude(i => i.Payments) + .Where(l => l.Property.OrganizationId == organizationId && + l.Tenant != null && + l.StartDate <= asOfDate && + l.EndDate >= asOfDate) + .OrderBy(l => l.Property.Address) + .ThenBy(l => l.Tenant!.LastName) + .Select(l => new RentRollItem + { + PropertyId = l.PropertyId, + PropertyName = l.Property.Address, + PropertyAddress = l.Property.Address, + TenantId = l.TenantId, + TenantName = $"{l.Tenant!.FirstName} {l.Tenant!.LastName}", + LeaseStatus = l.Status, + LeaseStartDate = l.StartDate, + LeaseEndDate = l.EndDate, + MonthlyRent = l.MonthlyRent, + SecurityDeposit = l.SecurityDeposit, + TotalPaid = l.Invoices.SelectMany(i => i.Payments).Sum(p => p.Amount), + TotalDue = l.Invoices.Where(i => i.Status != "Cancelled").Sum(i => i.Amount) + }) + .ToListAsync(); + + return rentRoll; + } + + /// + /// Generate property performance comparison report + /// + public async Task> GeneratePropertyPerformanceAsync( + Guid organizationId, + DateTime startDate, + DateTime endDate) + { + using var context = await _contextFactory.CreateDbContextAsync(); + + var properties = await context.Properties + .Where(p => p.OrganizationId == organizationId) + .ToListAsync(); + + var performance = new List(); + var totalDays = (endDate - startDate).Days + 1; + + foreach (var property in properties) + { + // Calculate income from rent payments + var income = await context.Payments + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .Where(p => p.Invoice.Lease.PropertyId == property.Id && + p.PaidOn >= startDate && + p.PaidOn <= endDate) + .SumAsync(p => p.Amount); + + // Calculate expenses from maintenance requests only + var expenses = await context.MaintenanceRequests + .Where(m => m.PropertyId == property.Id && + m.CompletedOn.HasValue && + m.CompletedOn.Value >= startDate && + m.CompletedOn.Value <= endDate && + m.Status == "Completed" && + m.ActualCost > 0) + .SumAsync(m => m.ActualCost); + + // Calculate occupancy days + var leases = await context.Leases + .Where(l => l.PropertyId == property.Id && + l.Status == "Active" && + l.StartDate <= endDate && + l.EndDate >= startDate) + .ToListAsync(); + + var occupancyDays = 0; + foreach (var lease in leases) + { + var leaseStart = lease.StartDate > startDate ? lease.StartDate : startDate; + var leaseEnd = lease.EndDate < endDate ? lease.EndDate : endDate; + if (leaseEnd >= leaseStart) + { + occupancyDays += (leaseEnd - leaseStart).Days + 1; + } + } + + // Calculate ROI (simplified - based on profit margin since we don't track purchase price) + var roi = income > 0 + ? ((income - expenses) / income) * 100 + : 0; + + performance.Add(new PropertyPerformance + { + PropertyId = property.Id, + PropertyName = property.Address, + PropertyAddress = property.Address, + TotalIncome = income, + TotalExpenses = expenses, + ROI = roi, + OccupancyDays = occupancyDays, + TotalDays = totalDays + }); + } + + return performance.OrderByDescending(p => p.NetIncome).ToList(); + } + + /// + /// Generate tax report data for Schedule E + /// + public async Task> GenerateTaxReportAsync(Guid organizationId, int year, Guid? propertyId = null) + { + using var context = await _contextFactory.CreateDbContextAsync(); + var startDate = new DateTime(year, 1, 1); + var endDate = new DateTime(year, 12, 31); + + var propertiesQuery = context.Properties.Where(p => p.OrganizationId == organizationId); + if (propertyId.HasValue) + { + propertiesQuery = propertiesQuery.Where(p => p.Id == propertyId.Value); + } + + var properties = await propertiesQuery.ToListAsync(); + var taxReports = new List(); + + foreach (var property in properties) + { + // Calculate rent income from payments + var rentIncome = await context.Payments + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .Where(p => p.Invoice.Lease.PropertyId == property.Id && + p.PaidOn >= startDate && + p.PaidOn <= endDate) + .SumAsync(p => p.Amount); + + // Get maintenance expenses (this is the only expense type currently tracked) + var maintenanceExpenses = await context.MaintenanceRequests + .Where(m => m.PropertyId == property.Id && + m.CompletedOn.HasValue && + m.CompletedOn.Value >= startDate && + m.CompletedOn.Value <= endDate && + m.Status == "Completed" && + m.ActualCost > 0) + .ToListAsync(); + + // Calculate depreciation (simplified - 27.5 years for residential rental) + // Note: Since we don't track purchase price, this should be manually entered + var depreciationAmount = 0m; + + var totalMaintenanceCost = maintenanceExpenses.Sum(m => m.ActualCost); + + var taxReport = new TaxReportData + { + Year = year, + PropertyId = property.Id, + PropertyName = property.Address, + TotalRentIncome = rentIncome, + DepreciationAmount = depreciationAmount, + + // Currently only maintenance/repairs are tracked + Advertising = 0, + Cleaning = 0, + Insurance = 0, + Legal = 0, + Management = 0, + MortgageInterest = 0, + Repairs = totalMaintenanceCost, // All maintenance costs + Supplies = 0, + Taxes = 0, + Utilities = 0, + Other = 0 + }; + + taxReport.TotalExpenses = totalMaintenanceCost; + + taxReports.Add(taxReport); + } + + return taxReports; + } +} diff --git a/Aquiis.Professional/Application/Services/InspectionService.cs b/Aquiis.Professional/Application/Services/InspectionService.cs new file mode 100644 index 0000000..2fc8783 --- /dev/null +++ b/Aquiis.Professional/Application/Services/InspectionService.cs @@ -0,0 +1,276 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Interfaces; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing property inspections with business logic for scheduling, + /// tracking, and integration with calendar events. + /// + public class InspectionService : BaseService + { + private readonly ICalendarEventService _calendarEventService; + + public InspectionService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings, + ICalendarEventService calendarEventService) + : base(context, logger, userContext, settings) + { + _calendarEventService = calendarEventService; + } + + #region Helper Methods + + protected async Task GetUserIdAsync() + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + return userId; + } + + protected async Task GetActiveOrganizationIdAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue) + { + throw new UnauthorizedAccessException("No active organization."); + } + return organizationId.Value; + } + + #endregion + + /// + /// Validates inspection business rules. + /// + protected override async Task ValidateEntityAsync(Inspection entity) + { + var errors = new List(); + + // Required fields + if (entity.PropertyId == Guid.Empty) + { + errors.Add("Property is required"); + } + + if (string.IsNullOrWhiteSpace(entity.InspectionType)) + { + errors.Add("Inspection type is required"); + } + + if (entity.CompletedOn == default) + { + errors.Add("Completion date is required"); + } + + if (errors.Any()) + { + throw new InvalidOperationException(string.Join("; ", errors)); + } + + await Task.CompletedTask; + } + + /// + /// Gets all inspections for the active organization. + /// + public override async Task> GetAllAsync() + { + var organizationId = await GetActiveOrganizationIdAsync(); + + return await _context.Inspections + .Include(i => i.Property) + .Include(i => i.Lease) + .ThenInclude(l => l!.Tenant) + .Where(i => !i.IsDeleted && i.OrganizationId == organizationId) + .OrderByDescending(i => i.CompletedOn) + .ToListAsync(); + } + + /// + /// Gets inspections by property ID. + /// + public async Task> GetByPropertyIdAsync(Guid propertyId) + { + var organizationId = await GetActiveOrganizationIdAsync(); + + return await _context.Inspections + .Include(i => i.Property) + .Include(i => i.Lease) + .ThenInclude(l => l!.Tenant) + .Where(i => i.PropertyId == propertyId && !i.IsDeleted && i.OrganizationId == organizationId) + .OrderByDescending(i => i.CompletedOn) + .ToListAsync(); + } + + /// + /// Gets a single inspection by ID with related data. + /// + public override async Task GetByIdAsync(Guid id) + { + var organizationId = await GetActiveOrganizationIdAsync(); + + return await _context.Inspections + .Include(i => i.Property) + .Include(i => i.Lease) + .ThenInclude(l => l!.Tenant) + .FirstOrDefaultAsync(i => i.Id == id && !i.IsDeleted && i.OrganizationId == organizationId); + } + + /// + /// Creates a new inspection with calendar event integration. + /// + public override async Task CreateAsync(Inspection inspection) + { + // Base validation and creation + await ValidateEntityAsync(inspection); + + var userId = await GetUserIdAsync(); + var organizationId = await GetActiveOrganizationIdAsync(); + + inspection.Id = Guid.NewGuid(); + inspection.OrganizationId = organizationId; + inspection.CreatedBy = userId; + inspection.CreatedOn = DateTime.UtcNow; + + await _context.Inspections.AddAsync(inspection); + await _context.SaveChangesAsync(); + + // Create calendar event for the inspection + await _calendarEventService.CreateOrUpdateEventAsync(inspection); + + // Update property inspection tracking if this is a routine inspection + if (inspection.InspectionType == ApplicationConstants.InspectionTypes.Routine) + { + await HandleRoutineInspectionCompletionAsync(inspection); + } + + _logger.LogInformation("Created inspection {InspectionId} for property {PropertyId}", + inspection.Id, inspection.PropertyId); + + return inspection; + } + + /// + /// Updates an existing inspection. + /// + public override async Task UpdateAsync(Inspection inspection) + { + await ValidateEntityAsync(inspection); + + var userId = await GetUserIdAsync(); + var organizationId = await GetActiveOrganizationIdAsync(); + + // Security: Verify inspection belongs to active organization + var existing = await _context.Inspections + .FirstOrDefaultAsync(i => i.Id == inspection.Id && i.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Inspection {inspection.Id} not found in active organization."); + } + + // Set tracking fields + inspection.LastModifiedBy = userId; + inspection.LastModifiedOn = DateTime.UtcNow; + inspection.OrganizationId = organizationId; // Prevent org hijacking + + _context.Entry(existing).CurrentValues.SetValues(inspection); + await _context.SaveChangesAsync(); + + // Update calendar event + await _calendarEventService.CreateOrUpdateEventAsync(inspection); + + // Update property inspection tracking if routine inspection date changed + if (inspection.InspectionType == ApplicationConstants.InspectionTypes.Routine) + { + await HandleRoutineInspectionCompletionAsync(inspection); + } + + _logger.LogInformation("Updated inspection {InspectionId}", inspection.Id); + + return inspection; + } + + /// + /// Deletes an inspection (soft delete). + /// + public override async Task DeleteAsync(Guid id) + { + var userId = await GetUserIdAsync(); + var organizationId = await GetActiveOrganizationIdAsync(); + + var inspection = await _context.Inspections + .FirstOrDefaultAsync(i => i.Id == id && i.OrganizationId == organizationId); + + if (inspection == null) + { + throw new KeyNotFoundException($"Inspection {id} not found."); + } + + inspection.IsDeleted = true; + inspection.LastModifiedBy = userId; + inspection.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + // TODO: Delete associated calendar event when interface method is available + // await _calendarEventService.DeleteEventBySourceAsync(id, nameof(Inspection)); + + _logger.LogInformation("Deleted inspection {InspectionId}", id); + + return true; + } + + /// + /// Handles routine inspection completion by updating property tracking and removing old calendar events. + /// + private async Task HandleRoutineInspectionCompletionAsync(Inspection inspection) + { + // Find and update/delete the original property-based routine inspection calendar event + var propertyBasedEvent = await _context.CalendarEvents + .FirstOrDefaultAsync(e => + e.PropertyId == inspection.PropertyId && + e.SourceEntityType == "Property" && + e.EventType == CalendarEventTypes.Inspection && + !e.IsDeleted); + + if (propertyBasedEvent != null) + { + // Remove the old property-based event since we now have an actual inspection record + _context.CalendarEvents.Remove(propertyBasedEvent); + } + + // Update property's routine inspection tracking + var property = await _context.Properties + .FirstOrDefaultAsync(p => p.Id == inspection.PropertyId); + + if (property != null) + { + property.LastRoutineInspectionDate = inspection.CompletedOn; + + // Calculate next routine inspection date based on interval + if (property.RoutineInspectionIntervalMonths > 0) + { + property.NextRoutineInspectionDueDate = inspection.CompletedOn + .AddMonths(property.RoutineInspectionIntervalMonths); + } + + await _context.SaveChangesAsync(); + } + } + } +} diff --git a/Aquiis.Professional/Application/Services/InvoiceService.cs b/Aquiis.Professional/Application/Services/InvoiceService.cs new file mode 100644 index 0000000..89f9cd9 --- /dev/null +++ b/Aquiis.Professional/Application/Services/InvoiceService.cs @@ -0,0 +1,465 @@ +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing Invoice entities. + /// Inherits common CRUD operations from BaseService and adds invoice-specific business logic. + /// + public class InvoiceService : BaseService + { + public InvoiceService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings) + : base(context, logger, userContext, settings) + { + } + + /// + /// Validates an invoice before create/update operations. + /// + protected override async Task ValidateEntityAsync(Invoice entity) + { + var errors = new List(); + + // Required fields + if (entity.LeaseId == Guid.Empty) + { + errors.Add("Lease ID is required."); + } + + if (string.IsNullOrWhiteSpace(entity.InvoiceNumber)) + { + errors.Add("Invoice number is required."); + } + + if (string.IsNullOrWhiteSpace(entity.Description)) + { + errors.Add("Description is required."); + } + + if (entity.Amount <= 0) + { + errors.Add("Amount must be greater than zero."); + } + + if (entity.DueOn < entity.InvoicedOn) + { + errors.Add("Due date cannot be before invoice date."); + } + + // Validate lease exists and belongs to organization + if (entity.LeaseId != Guid.Empty) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var lease = await _context.Leases + .Include(l => l.Property) + .FirstOrDefaultAsync(l => l.Id == entity.LeaseId && !l.IsDeleted); + + if (lease == null) + { + errors.Add($"Lease with ID {entity.LeaseId} does not exist."); + } + else if (lease.Property.OrganizationId != organizationId) + { + errors.Add("Lease does not belong to the current organization."); + } + } + + // Check for duplicate invoice number in same organization + if (!string.IsNullOrWhiteSpace(entity.InvoiceNumber)) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var duplicate = await _context.Invoices + .AnyAsync(i => i.InvoiceNumber == entity.InvoiceNumber + && i.OrganizationId == organizationId + && i.Id != entity.Id + && !i.IsDeleted); + + if (duplicate) + { + errors.Add($"Invoice number '{entity.InvoiceNumber}' already exists."); + } + } + + // Validate status + var validStatuses = new[] { "Pending", "Paid", "Overdue", "Cancelled" }; + if (!string.IsNullOrWhiteSpace(entity.Status) && !validStatuses.Contains(entity.Status)) + { + errors.Add($"Status must be one of: {string.Join(", ", validStatuses)}"); + } + + // Validate amount paid doesn't exceed amount + if (entity.AmountPaid > entity.Amount + (entity.LateFeeAmount ?? 0)) + { + errors.Add("Amount paid cannot exceed invoice amount plus late fees."); + } + + if (errors.Any()) + { + throw new ValidationException(string.Join(" ", errors)); + } + } + + /// + /// Gets all invoices for a specific lease. + /// + public async Task> GetInvoicesByLeaseIdAsync(Guid leaseId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Payments) + .Where(i => i.LeaseId == leaseId + && !i.IsDeleted + && i.OrganizationId == organizationId) + .OrderByDescending(i => i.DueOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetInvoicesByLeaseId"); + throw; + } + } + + /// + /// Gets all invoices with a specific status. + /// + public async Task> GetInvoicesByStatusAsync(string status) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Payments) + .Where(i => i.Status == status + && !i.IsDeleted + && i.OrganizationId == organizationId) + .OrderByDescending(i => i.DueOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetInvoicesByStatus"); + throw; + } + } + + /// + /// Gets all overdue invoices (due date passed and not paid). + /// + public async Task> GetOverdueInvoicesAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var today = DateTime.Today; + + return await _context.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Payments) + .Where(i => i.Status != "Paid" + && i.Status != "Cancelled" + && i.DueOn < today + && !i.IsDeleted + && i.OrganizationId == organizationId) + .OrderBy(i => i.DueOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetOverdueInvoices"); + throw; + } + } + + /// + /// Gets invoices due within the specified number of days. + /// + public async Task> GetInvoicesDueSoonAsync(int daysThreshold = 7) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var today = DateTime.Today; + var thresholdDate = today.AddDays(daysThreshold); + + return await _context.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Payments) + .Where(i => i.Status == "Pending" + && i.DueOn >= today + && i.DueOn <= thresholdDate + && !i.IsDeleted + && i.OrganizationId == organizationId) + .OrderBy(i => i.DueOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetInvoicesDueSoon"); + throw; + } + } + + /// + /// Gets an invoice with all related entities loaded. + /// + public async Task GetInvoiceWithRelationsAsync(Guid invoiceId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Payments) + .Include(i => i.Document) + .FirstOrDefaultAsync(i => i.Id == invoiceId + && !i.IsDeleted + && i.OrganizationId == organizationId); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetInvoiceWithRelations"); + throw; + } + } + + /// + /// Generates a unique invoice number for the organization. + /// Format: INV-YYYYMM-00001 + /// + public async Task GenerateInvoiceNumberAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var invoiceCount = await _context.Invoices + .Where(i => i.OrganizationId == organizationId) + .CountAsync(); + + var nextNumber = invoiceCount + 1; + return $"INV-{DateTime.Now:yyyyMM}-{nextNumber:D5}"; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GenerateInvoiceNumber"); + throw; + } + } + + /// + /// Applies a late fee to an overdue invoice. + /// + public async Task ApplyLateFeeAsync(Guid invoiceId, decimal lateFeeAmount) + { + try + { + var invoice = await GetByIdAsync(invoiceId); + if (invoice == null) + { + throw new InvalidOperationException($"Invoice {invoiceId} not found."); + } + + if (invoice.Status == "Paid" || invoice.Status == "Cancelled") + { + throw new InvalidOperationException("Cannot apply late fee to paid or cancelled invoice."); + } + + if (invoice.LateFeeApplied == true) + { + throw new InvalidOperationException("Late fee has already been applied to this invoice."); + } + + if (lateFeeAmount <= 0) + { + throw new ArgumentException("Late fee amount must be greater than zero."); + } + + invoice.LateFeeAmount = lateFeeAmount; + invoice.LateFeeApplied = true; + invoice.LateFeeAppliedOn = DateTime.UtcNow; + + // Update status to overdue if not already + if (invoice.Status == "Pending") + { + invoice.Status = "Overdue"; + } + + await UpdateAsync(invoice); + + return invoice; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "ApplyLateFee"); + throw; + } + } + + /// + /// Marks a reminder as sent for an invoice. + /// + public async Task MarkReminderSentAsync(Guid invoiceId) + { + try + { + var invoice = await GetByIdAsync(invoiceId); + if (invoice == null) + { + throw new InvalidOperationException($"Invoice {invoiceId} not found."); + } + + invoice.ReminderSent = true; + invoice.ReminderSentOn = DateTime.UtcNow; + + await UpdateAsync(invoice); + + return invoice; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "MarkReminderSent"); + throw; + } + } + + /// + /// Updates the invoice status based on payments received. + /// + public async Task UpdateInvoiceStatusAsync(Guid invoiceId) + { + try + { + var invoice = await GetInvoiceWithRelationsAsync(invoiceId); + if (invoice == null) + { + throw new InvalidOperationException($"Invoice {invoiceId} not found."); + } + + // Calculate total amount due (including late fees) + var totalDue = invoice.Amount + (invoice.LateFeeAmount ?? 0); + var totalPaid = invoice.Payments.Where(p => !p.IsDeleted).Sum(p => p.Amount); + + invoice.AmountPaid = totalPaid; + + // Update status + if (totalPaid >= totalDue) + { + invoice.Status = "Paid"; + invoice.PaidOn = invoice.Payments + .Where(p => !p.IsDeleted) + .OrderByDescending(p => p.PaidOn) + .FirstOrDefault()?.PaidOn ?? DateTime.UtcNow; + } + else if (invoice.Status == "Cancelled") + { + // Don't change cancelled status + } + else if (invoice.DueOn < DateTime.Today) + { + invoice.Status = "Overdue"; + } + else + { + invoice.Status = "Pending"; + } + + await UpdateAsync(invoice); + + return invoice; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "UpdateInvoiceStatus"); + throw; + } + } + + /// + /// Calculates the total outstanding balance across all unpaid invoices. + /// + public async Task CalculateTotalOutstandingAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var total = await _context.Invoices + .Where(i => i.Status != "Paid" + && i.Status != "Cancelled" + && !i.IsDeleted + && i.OrganizationId == organizationId) + .SumAsync(i => (i.Amount + (i.LateFeeAmount ?? 0)) - i.AmountPaid); + + return total; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "CalculateTotalOutstanding"); + throw; + } + } + + /// + /// Gets invoices within a specific date range. + /// + public async Task> GetInvoicesByDateRangeAsync(DateTime startDate, DateTime endDate) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Payments) + .Where(i => i.InvoicedOn >= startDate + && i.InvoicedOn <= endDate + && !i.IsDeleted + && i.OrganizationId == organizationId) + .OrderByDescending(i => i.InvoicedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetInvoicesByDateRange"); + throw; + } + } + } +} diff --git a/Aquiis.Professional/Application/Services/LeaseOfferService.cs b/Aquiis.Professional/Application/Services/LeaseOfferService.cs new file mode 100644 index 0000000..559bc57 --- /dev/null +++ b/Aquiis.Professional/Application/Services/LeaseOfferService.cs @@ -0,0 +1,294 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing LeaseOffer entities. + /// Inherits common CRUD operations from BaseService and adds lease offer-specific business logic. + /// + public class LeaseOfferService : BaseService + { + public LeaseOfferService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings) + : base(context, logger, userContext, settings) + { + } + + #region Overrides with LeaseOffer-Specific Logic + + /// + /// Validates a lease offer entity before create/update operations. + /// + protected override async Task ValidateEntityAsync(LeaseOffer entity) + { + var errors = new List(); + + // Required field validation + if (entity.RentalApplicationId == Guid.Empty) + { + errors.Add("RentalApplicationId is required"); + } + + if (entity.PropertyId == Guid.Empty) + { + errors.Add("PropertyId is required"); + } + + if (entity.ProspectiveTenantId == Guid.Empty) + { + errors.Add("ProspectiveTenantId is required"); + } + + if (entity.MonthlyRent <= 0) + { + errors.Add("MonthlyRent must be greater than zero"); + } + + if (entity.SecurityDeposit < 0) + { + errors.Add("SecurityDeposit cannot be negative"); + } + + if (entity.OfferedOn == DateTime.MinValue) + { + errors.Add("OfferedOn is required"); + } + + if (errors.Any()) + { + throw new ValidationException(string.Join("; ", errors)); + } + + await base.ValidateEntityAsync(entity); + } + + /// + /// Sets default values for create operations. + /// + protected override async Task SetCreateDefaultsAsync(LeaseOffer entity) + { + entity = await base.SetCreateDefaultsAsync(entity); + + // Set default status if not already set + if (string.IsNullOrWhiteSpace(entity.Status)) + { + entity.Status = "Pending"; + } + + // Set offered date if not already set + if (entity.OfferedOn == DateTime.MinValue) + { + entity.OfferedOn = DateTime.UtcNow; + } + + // Set expiration date if not already set (default 7 days) + if (entity.ExpiresOn == DateTime.MinValue) + { + entity.ExpiresOn = entity.OfferedOn.AddDays(7); + } + + return entity; + } + + #endregion + + #region Retrieval Methods + + /// + /// Gets a lease offer with all related entities. + /// + public async Task GetLeaseOfferWithRelationsAsync(Guid leaseOfferId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .Include(lo => lo.Property) + .Include(lo => lo.ProspectiveTenant) + .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId + && !lo.IsDeleted + && lo.OrganizationId == organizationId); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeaseOfferWithRelations"); + throw; + } + } + + /// + /// Gets all lease offers with related entities. + /// + public async Task> GetLeaseOffersWithRelationsAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .Include(lo => lo.Property) + .Include(lo => lo.ProspectiveTenant) + .Where(lo => !lo.IsDeleted && lo.OrganizationId == organizationId) + .OrderByDescending(lo => lo.OfferedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeaseOffersWithRelations"); + throw; + } + } + + #endregion + + #region Business Logic Methods + + /// + /// Gets lease offer by rental application ID. + /// + public async Task GetLeaseOfferByApplicationIdAsync(Guid applicationId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .Include(lo => lo.Property) + .Include(lo => lo.ProspectiveTenant) + .FirstOrDefaultAsync(lo => lo.RentalApplicationId == applicationId + && !lo.IsDeleted + && lo.OrganizationId == organizationId); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeaseOfferByApplicationId"); + throw; + } + } + + /// + /// Gets lease offers by property ID. + /// + public async Task> GetLeaseOffersByPropertyIdAsync(Guid propertyId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .Include(lo => lo.Property) + .Include(lo => lo.ProspectiveTenant) + .Where(lo => lo.PropertyId == propertyId + && !lo.IsDeleted + && lo.OrganizationId == organizationId) + .OrderByDescending(lo => lo.OfferedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeaseOffersByPropertyId"); + throw; + } + } + + /// + /// Gets lease offers by status. + /// + public async Task> GetLeaseOffersByStatusAsync(string status) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .Include(lo => lo.Property) + .Include(lo => lo.ProspectiveTenant) + .Where(lo => lo.Status == status + && !lo.IsDeleted + && lo.OrganizationId == organizationId) + .OrderByDescending(lo => lo.OfferedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeaseOffersByStatus"); + throw; + } + } + + /// + /// Gets active (pending) lease offers. + /// + public async Task> GetActiveLeaseOffersAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .Include(lo => lo.Property) + .Include(lo => lo.ProspectiveTenant) + .Where(lo => lo.Status == "Pending" + && !lo.IsDeleted + && lo.OrganizationId == organizationId + && lo.ExpiresOn > DateTime.UtcNow) + .OrderByDescending(lo => lo.OfferedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetActiveLeaseOffers"); + throw; + } + } + + /// + /// Updates lease offer status. + /// + public async Task UpdateLeaseOfferStatusAsync(Guid leaseOfferId, string newStatus, string? responseNotes = null) + { + try + { + var leaseOffer = await GetByIdAsync(leaseOfferId); + if (leaseOffer == null) + { + throw new InvalidOperationException($"Lease offer {leaseOfferId} not found"); + } + + leaseOffer.Status = newStatus; + leaseOffer.RespondedOn = DateTime.UtcNow; + + if (!string.IsNullOrWhiteSpace(responseNotes)) + { + leaseOffer.ResponseNotes = responseNotes; + } + + return await UpdateAsync(leaseOffer); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "UpdateLeaseOfferStatus"); + throw; + } + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/LeaseService.cs b/Aquiis.Professional/Application/Services/LeaseService.cs new file mode 100644 index 0000000..9ed0f7b --- /dev/null +++ b/Aquiis.Professional/Application/Services/LeaseService.cs @@ -0,0 +1,492 @@ +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing Lease entities. + /// Inherits common CRUD operations from BaseService and adds lease-specific business logic. + /// + public class LeaseService : BaseService + { + public LeaseService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings) + : base(context, logger, userContext, settings) + { + } + + #region Overrides with Lease-Specific Logic + + /// + /// Validates a lease entity before create/update operations. + /// + protected override async Task ValidateEntityAsync(Lease entity) + { + var errors = new List(); + + // Required field validation + if (entity.PropertyId == Guid.Empty) + { + errors.Add("PropertyId is required"); + } + + if (entity.TenantId == Guid.Empty) + { + errors.Add("TenantId is required"); + } + + if (entity.StartDate == default) + { + errors.Add("StartDate is required"); + } + + if (entity.EndDate == default) + { + errors.Add("EndDate is required"); + } + + if (entity.MonthlyRent <= 0) + { + errors.Add("MonthlyRent must be greater than 0"); + } + + // Business rule validation + if (entity.EndDate <= entity.StartDate) + { + errors.Add("EndDate must be after StartDate"); + } + + // Check for overlapping leases on the same property + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var overlappingLease = await _context.Leases + .Include(l => l.Property) + .Where(l => l.PropertyId == entity.PropertyId + && l.Id != entity.Id + && !l.IsDeleted + && l.Property.OrganizationId == organizationId + && (l.Status == ApplicationConstants.LeaseStatuses.Active + || l.Status == ApplicationConstants.LeaseStatuses.Pending)) + .Where(l => + // New lease starts during existing lease + (entity.StartDate >= l.StartDate && entity.StartDate <= l.EndDate) || + // New lease ends during existing lease + (entity.EndDate >= l.StartDate && entity.EndDate <= l.EndDate) || + // New lease completely encompasses existing lease + (entity.StartDate <= l.StartDate && entity.EndDate >= l.EndDate)) + .FirstOrDefaultAsync(); + + if (overlappingLease != null) + { + errors.Add($"A lease already exists for this property during the specified date range (Lease ID: {overlappingLease.Id})"); + } + + if (errors.Any()) + { + throw new ValidationException(string.Join("; ", errors)); + } + + await base.ValidateEntityAsync(entity); + } + + /// + /// Creates a new lease and updates the property availability status. + /// + public override async Task CreateAsync(Lease entity) + { + var lease = await base.CreateAsync(entity); + + // If lease is active, mark property as unavailable + if (entity.Status == ApplicationConstants.LeaseStatuses.Active) + { + var property = await _context.Properties.FindAsync(entity.PropertyId); + if (property != null) + { + property.IsAvailable = false; + property.LastModifiedOn = DateTime.UtcNow; + property.LastModifiedBy = await _userContext.GetUserIdAsync(); + _context.Properties.Update(property); + await _context.SaveChangesAsync(); + } + } + + return lease; + } + + /// + /// Deletes (soft deletes) a lease and updates property availability if needed. + /// + public override async Task DeleteAsync(Guid id) + { + var lease = await GetByIdAsync(id); + if (lease == null) return false; + + var result = await base.DeleteAsync(id); + + // If lease was active, check if property should be marked available + if (result && lease.Status == ApplicationConstants.LeaseStatuses.Active) + { + var property = await _context.Properties.FindAsync(lease.PropertyId); + if (property != null) + { + // Check if there are any other active/pending leases for this property + var hasOtherActiveLeases = await _context.Leases + .AnyAsync(l => l.PropertyId == lease.PropertyId + && l.Id != lease.Id + && !l.IsDeleted + && (l.Status == ApplicationConstants.LeaseStatuses.Active + || l.Status == ApplicationConstants.LeaseStatuses.Pending)); + + if (!hasOtherActiveLeases) + { + property.IsAvailable = true; + property.LastModifiedOn = DateTime.UtcNow; + property.LastModifiedBy = await _userContext.GetUserIdAsync(); + _context.Properties.Update(property); + await _context.SaveChangesAsync(); + } + } + } + + return result; + } + + #endregion + + #region Retrieval Methods + + /// + /// Gets a lease with all related entities (Property, Tenant, Documents, Invoices). + /// + public async Task GetLeaseWithRelationsAsync(Guid leaseId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var lease = await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Include(l => l.Document) + .Include(l => l.Documents) + .Include(l => l.Invoices) + .Where(l => l.Id == leaseId + && !l.IsDeleted + && l.Property.OrganizationId == organizationId) + .FirstOrDefaultAsync(); + + return lease; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeaseWithRelations"); + throw; + } + } + + /// + /// Gets all leases with Property and Tenant relations. + /// + public async Task> GetLeasesWithRelationsAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => !l.IsDeleted && l.Property.OrganizationId == organizationId) + .OrderByDescending(l => l.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeasesWithRelations"); + throw; + } + } + + #endregion + + #region Business Logic Methods + + /// + /// Gets all leases for a specific property. + /// + public async Task> GetLeasesByPropertyIdAsync(Guid propertyId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.PropertyId == propertyId + && !l.IsDeleted + && l.Property.OrganizationId == organizationId) + .OrderByDescending(l => l.StartDate) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeasesByPropertyId"); + throw; + } + } + + /// + /// Gets all leases for a specific tenant. + /// + public async Task> GetLeasesByTenantIdAsync(Guid tenantId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.TenantId == tenantId + && !l.IsDeleted + && l.Property.OrganizationId == organizationId) + .OrderByDescending(l => l.StartDate) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeasesByTenantId"); + throw; + } + } + + /// + /// Gets all active leases (current leases within their term). + /// + public async Task> GetActiveLeasesAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var today = DateTime.Today; + + return await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => !l.IsDeleted + && l.Property.OrganizationId == organizationId + && l.Status == ApplicationConstants.LeaseStatuses.Active + && l.StartDate <= today + && l.EndDate >= today) + .OrderBy(l => l.Property.Address) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetActiveLeases"); + throw; + } + } + + /// + /// Gets leases that are expiring within the specified number of days. + /// + public async Task> GetLeasesExpiringSoonAsync(int daysThreshold = 90) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var today = DateTime.Today; + var expirationDate = today.AddDays(daysThreshold); + + return await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => !l.IsDeleted + && l.Property.OrganizationId == organizationId + && l.Status == ApplicationConstants.LeaseStatuses.Active + && l.EndDate >= today + && l.EndDate <= expirationDate) + .OrderBy(l => l.EndDate) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeasesExpiringSoon"); + throw; + } + } + + /// + /// Gets leases by status. + /// + public async Task> GetLeasesByStatusAsync(string status) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => !l.IsDeleted + && l.Property.OrganizationId == organizationId + && l.Status == status) + .OrderByDescending(l => l.StartDate) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetLeasesByStatus"); + throw; + } + } + + /// + /// Gets current and upcoming leases for a property (Active or Pending status). + /// + public async Task> GetCurrentAndUpcomingLeasesByPropertyIdAsync(Guid propertyId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.PropertyId == propertyId + && !l.IsDeleted + && l.Property.OrganizationId == organizationId + && (l.Status == ApplicationConstants.LeaseStatuses.Active + || l.Status == ApplicationConstants.LeaseStatuses.Pending)) + .OrderBy(l => l.StartDate) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetCurrentAndUpcomingLeasesByPropertyId"); + throw; + } + } + + /// + /// Gets active leases for a specific property. + /// + public async Task> GetActiveLeasesByPropertyIdAsync(Guid propertyId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var today = DateTime.Today; + + return await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.PropertyId == propertyId + && !l.IsDeleted + && l.Property.OrganizationId == organizationId + && l.Status == ApplicationConstants.LeaseStatuses.Active + && l.StartDate <= today + && l.EndDate >= today) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetActiveLeasesByPropertyId"); + throw; + } + } + + /// + /// Calculates the total rent for a lease over its entire term. + /// + public async Task CalculateTotalLeaseValueAsync(Guid leaseId) + { + try + { + var lease = await GetByIdAsync(leaseId); + if (lease == null) + { + throw new InvalidOperationException($"Lease not found: {leaseId}"); + } + + var months = ((lease.EndDate.Year - lease.StartDate.Year) * 12) + + lease.EndDate.Month - lease.StartDate.Month; + + // Add 1 to include both start and end months + return lease.MonthlyRent * (months + 1); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "CalculateTotalLeaseValue"); + throw; + } + } + + /// + /// Updates the status of a lease. + /// + public async Task UpdateLeaseStatusAsync(Guid leaseId, string newStatus) + { + try + { + var lease = await GetByIdAsync(leaseId); + if (lease == null) + { + throw new InvalidOperationException($"Lease not found: {leaseId}"); + } + + lease.Status = newStatus; + + // Update property availability based on status + var property = await _context.Properties.FindAsync(lease.PropertyId); + if (property != null) + { + if (newStatus == ApplicationConstants.LeaseStatuses.Active) + { + property.IsAvailable = false; + } + else if (newStatus == ApplicationConstants.LeaseStatuses.Terminated + || newStatus == ApplicationConstants.LeaseStatuses.Expired) + { + // Only mark available if no other active leases exist + var hasOtherActiveLeases = await _context.Leases + .AnyAsync(l => l.PropertyId == lease.PropertyId + && l.Id != lease.Id + && !l.IsDeleted + && (l.Status == ApplicationConstants.LeaseStatuses.Active + || l.Status == ApplicationConstants.LeaseStatuses.Pending)); + + if (!hasOtherActiveLeases) + { + property.IsAvailable = true; + } + } + + property.LastModifiedOn = DateTime.UtcNow; + property.LastModifiedBy = await _userContext.GetUserIdAsync(); + _context.Properties.Update(property); + } + + return await UpdateAsync(lease); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "UpdateLeaseStatus"); + throw; + } + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/MaintenanceService.cs b/Aquiis.Professional/Application/Services/MaintenanceService.cs new file mode 100644 index 0000000..2adf3fd --- /dev/null +++ b/Aquiis.Professional/Application/Services/MaintenanceService.cs @@ -0,0 +1,492 @@ +using Aquiis.Professional.Application.Services.Workflows; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Interfaces; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing maintenance requests with business logic for status updates, + /// assignment tracking, and overdue detection. + /// + public class MaintenanceService : BaseService + { + private readonly ICalendarEventService _calendarEventService; + + public MaintenanceService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings, + ICalendarEventService calendarEventService) + : base(context, logger, userContext, settings) + { + _calendarEventService = calendarEventService; + } + + /// + /// Validates maintenance request business rules. + /// + protected override async Task ValidateEntityAsync(MaintenanceRequest entity) + { + var errors = new List(); + + // Required fields + if (entity.PropertyId == Guid.Empty) + { + errors.Add("Property is required"); + } + + if (string.IsNullOrWhiteSpace(entity.Title)) + { + errors.Add("Title is required"); + } + + if (string.IsNullOrWhiteSpace(entity.Description)) + { + errors.Add("Description is required"); + } + + if (string.IsNullOrWhiteSpace(entity.RequestType)) + { + errors.Add("Request type is required"); + } + + if (string.IsNullOrWhiteSpace(entity.Priority)) + { + errors.Add("Priority is required"); + } + + if (string.IsNullOrWhiteSpace(entity.Status)) + { + errors.Add("Status is required"); + } + + // Validate priority + var validPriorities = new[] { "Low", "Medium", "High", "Urgent" }; + if (!validPriorities.Contains(entity.Priority)) + { + errors.Add($"Priority must be one of: {string.Join(", ", validPriorities)}"); + } + + // Validate status + var validStatuses = new[] { "Submitted", "In Progress", "Completed", "Cancelled" }; + if (!validStatuses.Contains(entity.Status)) + { + errors.Add($"Status must be one of: {string.Join(", ", validStatuses)}"); + } + + // Validate dates + if (entity.RequestedOn > DateTime.Today) + { + errors.Add("Requested date cannot be in the future"); + } + + if (entity.ScheduledOn.HasValue && entity.ScheduledOn.Value.Date < entity.RequestedOn.Date) + { + errors.Add("Scheduled date cannot be before requested date"); + } + + if (entity.CompletedOn.HasValue && entity.CompletedOn.Value.Date < entity.RequestedOn.Date) + { + errors.Add("Completed date cannot be before requested date"); + } + + // Validate costs + if (entity.EstimatedCost < 0) + { + errors.Add("Estimated cost cannot be negative"); + } + + if (entity.ActualCost < 0) + { + errors.Add("Actual cost cannot be negative"); + } + + // Validate status-specific rules + if (entity.Status == "Completed") + { + if (!entity.CompletedOn.HasValue) + { + errors.Add("Completed date is required when status is Completed"); + } + } + + // Verify property exists and belongs to organization + if (entity.PropertyId != Guid.Empty) + { + var property = await _context.Properties + .FirstOrDefaultAsync(p => p.Id == entity.PropertyId && !p.IsDeleted); + + if (property == null) + { + errors.Add($"Property with ID {entity.PropertyId} not found"); + } + else if (property.OrganizationId != entity.OrganizationId) + { + errors.Add("Property does not belong to the same organization"); + } + } + + // If LeaseId is provided, verify it exists and belongs to the same property + if (entity.LeaseId.HasValue && entity.LeaseId.Value != Guid.Empty) + { + var lease = await _context.Leases + .FirstOrDefaultAsync(l => l.Id == entity.LeaseId.Value && !l.IsDeleted); + + if (lease == null) + { + errors.Add($"Lease with ID {entity.LeaseId.Value} not found"); + } + else if (lease.PropertyId != entity.PropertyId) + { + errors.Add("Lease does not belong to the specified property"); + } + else if (lease.OrganizationId != entity.OrganizationId) + { + errors.Add("Lease does not belong to the same organization"); + } + } + + if (errors.Any()) + { + throw new ValidationException(string.Join("; ", errors)); + } + + await Task.CompletedTask; + } + + /// + /// Creates a maintenance request and automatically creates a calendar event. + /// + public override async Task CreateAsync(MaintenanceRequest entity) + { + var maintenanceRequest = await base.CreateAsync(entity); + + // Create calendar event for the maintenance request + await _calendarEventService.CreateOrUpdateEventAsync(maintenanceRequest); + + return maintenanceRequest; + } + + /// + /// Updates a maintenance request and synchronizes the calendar event. + /// + public override async Task UpdateAsync(MaintenanceRequest entity) + { + var maintenanceRequest = await base.UpdateAsync(entity); + + // Update calendar event + await _calendarEventService.CreateOrUpdateEventAsync(maintenanceRequest); + + return maintenanceRequest; + } + + /// + /// Deletes a maintenance request and removes the associated calendar event. + /// + public override async Task DeleteAsync(Guid id) + { + var maintenanceRequest = await GetByIdAsync(id); + + var result = await base.DeleteAsync(id); + + if (result && maintenanceRequest != null) + { + // Delete associated calendar event + await _calendarEventService.DeleteEventAsync(maintenanceRequest.CalendarEventId); + } + + return result; + } + + /// + /// Gets all maintenance requests for a specific property. + /// + public async Task> GetMaintenanceRequestsByPropertyAsync(Guid propertyId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.PropertyId == propertyId && + m.OrganizationId == organizationId && + !m.IsDeleted) + .OrderByDescending(m => m.RequestedOn) + .ToListAsync(); + } + + /// + /// Gets all maintenance requests for a specific lease. + /// + public async Task> GetMaintenanceRequestsByLeaseAsync(Guid leaseId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.LeaseId == leaseId && + m.OrganizationId == organizationId && + !m.IsDeleted) + .OrderByDescending(m => m.RequestedOn) + .ToListAsync(); + } + + /// + /// Gets maintenance requests by status. + /// + public async Task> GetMaintenanceRequestsByStatusAsync(string status) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.Status == status && + m.OrganizationId == organizationId && + !m.IsDeleted) + .OrderByDescending(m => m.RequestedOn) + .ToListAsync(); + } + + /// + /// Gets maintenance requests by priority level. + /// + public async Task> GetMaintenanceRequestsByPriorityAsync(string priority) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.Priority == priority && + m.OrganizationId == organizationId && + !m.IsDeleted) + .OrderByDescending(m => m.RequestedOn) + .ToListAsync(); + } + + /// + /// Gets overdue maintenance requests (scheduled date has passed but not completed). + /// + public async Task> GetOverdueMaintenanceRequestsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var today = DateTime.Today; + + return await _context.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.OrganizationId == organizationId && + !m.IsDeleted && + m.Status != "Completed" && + m.Status != "Cancelled" && + m.ScheduledOn.HasValue && + m.ScheduledOn.Value.Date < today) + .OrderBy(m => m.ScheduledOn) + .ToListAsync(); + } + + /// + /// Gets the count of open (not completed/cancelled) maintenance requests. + /// + public async Task GetOpenMaintenanceRequestCountAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.MaintenanceRequests + .Where(m => m.OrganizationId == organizationId && + !m.IsDeleted && + m.Status != "Completed" && + m.Status != "Cancelled") + .CountAsync(); + } + + /// + /// Gets the count of urgent priority maintenance requests. + /// + public async Task GetUrgentMaintenanceRequestCountAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.MaintenanceRequests + .Where(m => m.OrganizationId == organizationId && + !m.IsDeleted && + m.Priority == "Urgent" && + m.Status != "Completed" && + m.Status != "Cancelled") + .CountAsync(); + } + + /// + /// Gets a maintenance request with all related entities loaded. + /// + public async Task GetMaintenanceRequestWithRelationsAsync(Guid id) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .ThenInclude(l => l!.Tenant) + .FirstOrDefaultAsync(m => m.Id == id && + m.OrganizationId == organizationId && + !m.IsDeleted); + } + + /// + /// Updates the status of a maintenance request with automatic date tracking. + /// + public async Task UpdateMaintenanceRequestStatusAsync(Guid id, string status) + { + var maintenanceRequest = await GetByIdAsync(id); + + if (maintenanceRequest == null) + { + throw new ValidationException($"Maintenance request {id} not found"); + } + + maintenanceRequest.Status = status; + + // Auto-set completed date when marked as completed + if (status == "Completed" && !maintenanceRequest.CompletedOn.HasValue) + { + maintenanceRequest.CompletedOn = DateTime.Today; + } + + return await UpdateAsync(maintenanceRequest); + } + + /// + /// Assigns a maintenance request to a contractor or maintenance person. + /// + public async Task AssignMaintenanceRequestAsync(Guid id, string assignedTo, DateTime? scheduledOn = null) + { + var maintenanceRequest = await GetByIdAsync(id); + + if (maintenanceRequest == null) + { + throw new ValidationException($"Maintenance request {id} not found"); + } + + maintenanceRequest.AssignedTo = assignedTo; + + if (scheduledOn.HasValue) + { + maintenanceRequest.ScheduledOn = scheduledOn.Value; + } + + // Auto-update status to In Progress if still Submitted + if (maintenanceRequest.Status == "Submitted") + { + maintenanceRequest.Status = "In Progress"; + } + + return await UpdateAsync(maintenanceRequest); + } + + /// + /// Completes a maintenance request with actual cost and resolution notes. + /// + public async Task CompleteMaintenanceRequestAsync( + Guid id, + decimal actualCost, + string resolutionNotes) + { + var maintenanceRequest = await GetByIdAsync(id); + + if (maintenanceRequest == null) + { + throw new ValidationException($"Maintenance request {id} not found"); + } + + maintenanceRequest.Status = "Completed"; + maintenanceRequest.CompletedOn = DateTime.Today; + maintenanceRequest.ActualCost = actualCost; + maintenanceRequest.ResolutionNotes = resolutionNotes; + + return await UpdateAsync(maintenanceRequest); + } + + /// + /// Gets maintenance requests assigned to a specific person. + /// + public async Task> GetMaintenanceRequestsByAssigneeAsync(string assignedTo) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.AssignedTo == assignedTo && + m.OrganizationId == organizationId && + !m.IsDeleted && + m.Status != "Completed" && + m.Status != "Cancelled") + .OrderByDescending(m => m.Priority == "Urgent") + .ThenByDescending(m => m.Priority == "High") + .ThenBy(m => m.ScheduledOn) + .ToListAsync(); + } + + /// + /// Calculates average days to complete maintenance requests. + /// + public async Task CalculateAverageDaysToCompleteAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var completedRequests = await _context.MaintenanceRequests + .Where(m => m.OrganizationId == organizationId && + !m.IsDeleted && + m.Status == "Completed" && + m.CompletedOn.HasValue) + .Select(m => new { m.RequestedOn, m.CompletedOn }) + .ToListAsync(); + + if (!completedRequests.Any()) + { + return 0; + } + + var totalDays = completedRequests.Sum(r => (r.CompletedOn!.Value.Date - r.RequestedOn.Date).Days); + return (double)totalDays / completedRequests.Count; + } + + /// + /// Gets maintenance cost summary by property. + /// + public async Task> GetMaintenanceCostsByPropertyAsync(DateTime? startDate = null, DateTime? endDate = null) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var query = _context.MaintenanceRequests + .Where(m => m.OrganizationId == organizationId && + !m.IsDeleted && + m.Status == "Completed"); + + if (startDate.HasValue) + { + query = query.Where(m => m.CompletedOn >= startDate.Value); + } + + if (endDate.HasValue) + { + query = query.Where(m => m.CompletedOn <= endDate.Value); + } + + return await query + .GroupBy(m => m.PropertyId) + .Select(g => new { PropertyId = g.Key, TotalCost = g.Sum(m => m.ActualCost) }) + .ToDictionaryAsync(x => x.PropertyId, x => x.TotalCost); + } + } +} diff --git a/Aquiis.Professional/Application/Services/NoteService.cs b/Aquiis.Professional/Application/Services/NoteService.cs new file mode 100644 index 0000000..2556bf0 --- /dev/null +++ b/Aquiis.Professional/Application/Services/NoteService.cs @@ -0,0 +1,105 @@ +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Shared.Components.Account; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Professional.Application.Services +{ + public class NoteService + { + private readonly ApplicationDbContext _context; + private readonly UserContextService _userContext; + + public NoteService(ApplicationDbContext context, UserContextService userContext) + { + _context = context; + _userContext = userContext; + } + + /// + /// Add a note to an entity + /// + public async Task AddNoteAsync(string entityType, Guid entityId, string content) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync(); + var userFullName = await _userContext.GetUserNameAsync(); + var userEmail = await _userContext.GetUserEmailAsync(); + + if (!organizationId.HasValue || string.IsNullOrEmpty(userId)) + { + throw new InvalidOperationException("User context is not available."); + } + + var note = new Note + { + Id = Guid.NewGuid(), + OrganizationId = organizationId!.Value, + EntityType = entityType, + EntityId = entityId, + Content = content.Trim(), + UserFullName = !string.IsNullOrWhiteSpace(userFullName) ? userFullName : userEmail, + CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, + CreatedOn = DateTime.UtcNow + }; + + _context.Notes.Add(note); + await _context.SaveChangesAsync(); + + return note; + } + + /// + /// Get all notes for an entity, ordered by newest first + /// + public async Task> GetNotesAsync(string entityType, Guid entityId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _context.Notes + .Include(n => n.User) + .Where(n => n.EntityType == entityType + && n.EntityId == entityId + && n.OrganizationId == organizationId + && !n.IsDeleted) + .OrderByDescending(n => n.CreatedOn) + .ToListAsync(); + } + + /// + /// Delete a note (soft delete) + /// + public async Task DeleteNoteAsync(Guid noteId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var note = await _context.Notes + .FirstOrDefaultAsync(n => n.Id == noteId + && n.OrganizationId == organizationId + && !n.IsDeleted); + + if (note == null) + return false; + + var userId = await _userContext.GetUserIdAsync(); + note.IsDeleted = true; + note.LastModifiedBy = userId; + note.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return true; + } + + /// + /// Get note count for an entity + /// + public async Task GetNoteCountAsync(string entityType, Guid entityId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _context.Notes + .CountAsync(n => n.EntityType == entityType + && n.EntityId == entityId + && n.OrganizationId == organizationId + && !n.IsDeleted); + } + } +} diff --git a/Aquiis.Professional/Application/Services/NotificationService.cs b/Aquiis.Professional/Application/Services/NotificationService.cs new file mode 100644 index 0000000..f079dda --- /dev/null +++ b/Aquiis.Professional/Application/Services/NotificationService.cs @@ -0,0 +1,252 @@ + +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Interfaces.Services; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services; +public class NotificationService : BaseService +{ + private readonly IEmailService _emailService; + private readonly ISMSService _smsService; + private new readonly ILogger _logger; + + public NotificationService( + ApplicationDbContext context, + UserContextService userContext, + IEmailService emailService, + ISMSService smsService, + IOptions appSettings, + ILogger logger) + : base(context, logger, userContext, appSettings) + { + _emailService = emailService; + _smsService = smsService; + _logger = logger; + } + + /// + /// Create and send a notification to a user + /// + public async Task SendNotificationAsync( + string recipientUserId, + string title, + string message, + string type, + string category, + Guid? relatedEntityId = null, + string? relatedEntityType = null) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Get user preferences + var preferences = await GetNotificationPreferencesAsync(recipientUserId); + + var notification = new Notification + { + Id = Guid.NewGuid(), + OrganizationId = organizationId!.Value, + RecipientUserId = recipientUserId, + Title = title, + Message = message, + Type = type, + Category = category, + RelatedEntityId = relatedEntityId, + RelatedEntityType = relatedEntityType, + SentOn = DateTime.UtcNow, + IsRead = false, + SendInApp = preferences.EnableInAppNotifications, + SendEmail = preferences.EnableEmailNotifications && ShouldSendEmail(category, preferences), + SendSMS = preferences.EnableSMSNotifications && ShouldSendSMS(category, preferences) + }; + + // Save in-app notification + await CreateAsync(notification); + + // Send email if enabled + if (notification.SendEmail && !string.IsNullOrEmpty(preferences.EmailAddress)) + { + try + { + await _emailService.SendEmailAsync( + preferences.EmailAddress, + title, + message); + + notification.EmailSent = true; + notification.EmailSentOn = DateTime.UtcNow; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to send email notification to {recipientUserId}"); + notification.EmailError = ex.Message; + } + } + + // Send SMS if enabled + if (notification.SendSMS && !string.IsNullOrEmpty(preferences.PhoneNumber)) + { + try + { + await _smsService.SendSMSAsync( + preferences.PhoneNumber, + $"{title}: {message}"); + + notification.SMSSent = true; + notification.SMSSentOn = DateTime.UtcNow; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to send SMS notification to {recipientUserId}"); + notification.SMSError = ex.Message; + } + } + + await UpdateAsync(notification); + + return notification; + } + + /// + /// Mark notification as read + /// + public async Task MarkAsReadAsync(Guid notificationId) + { + var notification = await GetByIdAsync(notificationId); + if (notification == null) return; + + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + + await UpdateAsync(notification); + } + + /// + /// Get unread notifications for current user + /// + public async Task> GetUnreadNotificationsAsync() + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Notifications + .Where(n => n.OrganizationId == organizationId + && n.RecipientUserId == userId + && !n.IsRead + && !n.IsDeleted) + .OrderByDescending(n => n.SentOn) + .Take(50) + .ToListAsync(); + } + + /// + /// Get notification history for current user + /// + public async Task> GetNotificationHistoryAsync(int count = 100) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Notifications + .Where(n => n.OrganizationId == organizationId + && n.RecipientUserId == userId + && !n.IsDeleted) + .OrderByDescending(n => n.SentOn) + .Take(count) + .ToListAsync(); + } + + /// + /// Get notification preferences for current user + /// + public async Task GetUserPreferencesAsync() + { + var userId = await _userContext.GetUserIdAsync(); + return await GetNotificationPreferencesAsync(userId); + } + + /// + /// Update notification preferences for current user + /// + public async Task UpdateUserPreferencesAsync(NotificationPreferences preferences) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Ensure the preferences belong to the current user and organization + if (preferences.UserId != userId || preferences.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("Cannot update preferences for another user"); + } + + _context.NotificationPreferences.Update(preferences); + await _context.SaveChangesAsync(); + return preferences; + } + + /// + /// Get or create notification preferences for user + /// + private async Task GetNotificationPreferencesAsync(string userId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var preferences = await _context.NotificationPreferences + .FirstOrDefaultAsync(p => p.OrganizationId == organizationId + && p.UserId == userId + && !p.IsDeleted); + + if (preferences == null) + { + // Create default preferences + preferences = new NotificationPreferences + { + Id = Guid.NewGuid(), + OrganizationId = organizationId!.Value, + UserId = userId, + EnableInAppNotifications = true, + EnableEmailNotifications = true, + EnableSMSNotifications = false, + EmailLeaseExpiring = true, + EmailPaymentDue = true, + EmailPaymentReceived = true, + EmailApplicationStatusChange = true, + EmailMaintenanceUpdate = true, + EmailInspectionScheduled = true + }; + + _context.NotificationPreferences.Add(preferences); + await _context.SaveChangesAsync(); + } + + return preferences; + } + + private bool ShouldSendEmail(string category, NotificationPreferences prefs) + { + return category switch + { + NotificationConstants.Categories.Lease => prefs.EmailLeaseExpiring, + NotificationConstants.Categories.Payment => prefs.EmailPaymentDue, + NotificationConstants.Categories.Application => prefs.EmailApplicationStatusChange, + NotificationConstants.Categories.Maintenance => prefs.EmailMaintenanceUpdate, + NotificationConstants.Categories.Inspection => prefs.EmailInspectionScheduled, + _ => true + }; + } + + private bool ShouldSendSMS(string category, NotificationPreferences prefs) + { + return category switch + { + NotificationConstants.Categories.Payment => prefs.SMSPaymentDue, + NotificationConstants.Categories.Maintenance => prefs.SMSMaintenanceEmergency, + NotificationConstants.Categories.Lease => prefs.SMSLeaseExpiringUrgent, + _ => false + }; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Application/Services/OrganizationService.cs b/Aquiis.Professional/Application/Services/OrganizationService.cs new file mode 100644 index 0000000..a4120ff --- /dev/null +++ b/Aquiis.Professional/Application/Services/OrganizationService.cs @@ -0,0 +1,495 @@ +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Aquiis.Professional.Shared.Services; +using Aquiis.Professional.Shared; + +namespace Aquiis.Professional.Application.Services +{ + public class OrganizationService + { + private readonly ApplicationDbContext _dbContext; + private readonly UserContextService _userContext; + + public OrganizationService(ApplicationDbContext dbContext, UserContextService _userContextService) + { + _dbContext = dbContext; + _userContext = _userContextService; + } + + #region CRUD Operations + + /// + /// Create a new organization + /// + public async Task CreateOrganizationAsync(string ownerId, string name, string? displayName = null, string? state = null) + { + var organization = new Organization + { + Id = Guid.NewGuid(), + OwnerId = ownerId, + Name = name, + DisplayName = displayName ?? name, + State = state, + IsActive = true, + CreatedOn = DateTime.UtcNow, + CreatedBy = ownerId + }; + + _dbContext.Organizations.Add(organization); + + // Create Owner entry in UserOrganizations + var userOrganization = new UserOrganization + { + Id = Guid.NewGuid(), + UserId = ownerId, + OrganizationId = organization.Id, + Role = ApplicationConstants.OrganizationRoles.Owner, + GrantedBy = ownerId, + GrantedOn = DateTime.UtcNow, + IsActive = true, + CreatedOn = DateTime.UtcNow, + CreatedBy = ownerId + }; + + _dbContext.UserOrganizations.Add(userOrganization); + + // add organization settings record with defaults + var settings = new OrganizationSettings + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + Name = organization.Name, + LateFeeEnabled = true, + LateFeeAutoApply = true, + LateFeeGracePeriodDays = 3, + LateFeePercentage = 0.05m, + MaxLateFeeAmount = 50.00m, + PaymentReminderEnabled = true, + PaymentReminderDaysBefore = 3, + CreatedOn = DateTime.UtcNow, + CreatedBy = ownerId + }; + + await _dbContext.OrganizationSettings.AddAsync(settings); + await _dbContext.SaveChangesAsync(); + + return organization; + } + + /// + /// Create a new organization + /// + public async Task CreateOrganizationAsync(Organization organization) + { + + var userId = await _userContext.GetUserIdAsync(); + + if(string.IsNullOrEmpty(userId)) + throw new InvalidOperationException("Cannot create organization: User ID is not available in context."); + + + organization.Id = Guid.NewGuid(); + organization.OwnerId = userId; + organization.IsActive = true; + organization.CreatedOn = DateTime.UtcNow; + organization.CreatedBy = userId; + + _dbContext.Organizations.Add(organization); + + // Create Owner entry in UserOrganizations + var userOrganization = new UserOrganization + { + Id = Guid.NewGuid(), + UserId = userId, + OrganizationId = organization.Id, + Role = ApplicationConstants.OrganizationRoles.Owner, + GrantedBy = userId, + GrantedOn = DateTime.UtcNow, + IsActive = true, + CreatedOn = DateTime.UtcNow, + CreatedBy = userId + }; + + _dbContext.UserOrganizations.Add(userOrganization); + await _dbContext.SaveChangesAsync(); + + // add organization settings record with defaults + var settings = new OrganizationSettings + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + Name = organization.Name, + LateFeeEnabled = true, + LateFeeAutoApply = true, + LateFeeGracePeriodDays = 3, + LateFeePercentage = 0.05m, + MaxLateFeeAmount = 50.00m, + PaymentReminderEnabled = true, + PaymentReminderDaysBefore = 3, + CreatedOn = DateTime.UtcNow, + CreatedBy = userId + }; + + await _dbContext.OrganizationSettings.AddAsync(settings); + await _dbContext.SaveChangesAsync(); + + return organization; + } + + + /// + /// Get organization by ID + /// + public async Task GetOrganizationByIdAsync(Guid organizationId) + { + return await _dbContext.Organizations + .Include(o => o.UserOrganizations) + .FirstOrDefaultAsync(o => o.Id == organizationId && !o.IsDeleted); + } + + /// + /// Get all organizations owned by a user + /// + public async Task> GetOwnedOrganizationsAsync(string userId) + { + return await _dbContext.Organizations + .Where(o => o.OwnerId == userId && !o.IsDeleted) + .OrderBy(o => o.Name) + .ToListAsync(); + } + + /// + /// Get all organizations a user has access to (via UserOrganizations) + /// + public async Task> GetUserOrganizationsAsync(string userId) + { + return await _dbContext.UserOrganizations + .Include(uo => uo.Organization) + .Where(uo => uo.UserId == userId && uo.IsActive && !uo.IsDeleted) + .Where(uo => !uo.Organization.IsDeleted) + .OrderBy(uo => uo.Organization.Name) + .ToListAsync(); + } + + /// + /// Update organization details + /// + public async Task UpdateOrganizationAsync(Organization organization) + { + var existing = await _dbContext.Organizations.FindAsync(organization.Id); + if (existing == null || existing.IsDeleted) + return false; + + existing.Name = organization.Name; + existing.DisplayName = organization.DisplayName; + existing.State = organization.State; + existing.IsActive = organization.IsActive; + existing.LastModifiedOn = DateTime.UtcNow; + existing.LastModifiedBy = organization.LastModifiedBy; + + await _dbContext.SaveChangesAsync(); + return true; + } + + /// + /// Delete organization (soft delete) + /// + public async Task DeleteOrganizationAsync(Guid organizationId, string deletedBy) + { + var organization = await _dbContext.Organizations.FindAsync(organizationId); + if (organization == null || organization.IsDeleted) + return false; + + organization.IsDeleted = true; + organization.IsActive = false; + organization.LastModifiedOn = DateTime.UtcNow; + organization.LastModifiedBy = deletedBy; + + // Soft delete all UserOrganizations entries + var userOrgs = await _dbContext.UserOrganizations + .Where(uo => uo.OrganizationId == organizationId) + .ToListAsync(); + + foreach (var uo in userOrgs) + { + uo.IsDeleted = true; + uo.IsActive = false; + uo.RevokedOn = DateTime.UtcNow; + uo.LastModifiedOn = DateTime.UtcNow; + uo.LastModifiedBy = deletedBy; + } + + await _dbContext.SaveChangesAsync(); + return true; + } + + #endregion + + #region Permission & Role Management + + /// + /// Check if user is the owner of an organization + /// + public async Task IsOwnerAsync(string userId, Guid organizationId) + { + var organization = await _dbContext.Organizations.FindAsync(organizationId); + return organization != null && organization.OwnerId == userId && !organization.IsDeleted; + } + + /// + /// Check if user has administrator role in an organization + /// + public async Task IsAdministratorAsync(string userId, Guid organizationId) + { + var role = await GetUserRoleForOrganizationAsync(userId, organizationId); + return role == ApplicationConstants.OrganizationRoles.Administrator; + } + + /// + /// Check if user can access an organization (has any active role) + /// + public async Task CanAccessOrganizationAsync(string userId, Guid organizationId) + { + return await _dbContext.UserOrganizations + .AnyAsync(uo => uo.UserId == userId + && uo.OrganizationId == organizationId + && uo.IsActive + && !uo.IsDeleted); + } + + /// + /// Get user's role for a specific organization + /// + public async Task GetUserRoleForOrganizationAsync(string userId, Guid organizationId) + { + var userOrg = await _dbContext.UserOrganizations + .FirstOrDefaultAsync(uo => uo.UserId == userId + && uo.OrganizationId == organizationId + && uo.IsActive + && !uo.IsDeleted); + + return userOrg?.Role; + } + + #endregion + + #region User-Organization Assignment + + /// + /// Grant a user access to an organization with a specific role + /// + public async Task GrantOrganizationAccessAsync(string userId, Guid organizationId, string role, string grantedBy) + { + // Validate role + if (!ApplicationConstants.OrganizationRoles.IsValid(role)) + throw new ArgumentException($"Invalid role: {role}"); + + // Check if organization exists + var organization = await _dbContext.Organizations.FindAsync(organizationId); + if (organization == null || organization.IsDeleted) + return false; + + // Check if user already has access + var existing = await _dbContext.UserOrganizations + .FirstOrDefaultAsync(uo => uo.UserId == userId && uo.OrganizationId == organizationId); + + if (existing != null) + { + // Reactivate if previously revoked + if (!existing.IsActive || existing.IsDeleted) + { + existing.IsActive = true; + existing.IsDeleted = false; + existing.Role = role; + existing.RevokedOn = null; + existing.LastModifiedOn = DateTime.UtcNow; + existing.LastModifiedBy = grantedBy; + } + else + { + // Already has active access + return false; + } + } + else + { + // Create new access + var userOrganization = new UserOrganization + { + Id = Guid.NewGuid(), + UserId = userId, + OrganizationId = organizationId, + Role = role, + GrantedBy = grantedBy, + GrantedOn = DateTime.UtcNow, + IsActive = true, + CreatedOn = DateTime.UtcNow, + CreatedBy = grantedBy + }; + + _dbContext.UserOrganizations.Add(userOrganization); + } + + await _dbContext.SaveChangesAsync(); + return true; + } + + /// + /// Revoke a user's access to an organization + /// + public async Task RevokeOrganizationAccessAsync(string userId, Guid organizationId, string revokedBy) + { + var userOrg = await _dbContext.UserOrganizations + .FirstOrDefaultAsync(uo => uo.UserId == userId + && uo.OrganizationId == organizationId + && uo.IsActive); + + if (userOrg == null) + return false; + + // Don't allow revoking owner access + if (userOrg.Role == ApplicationConstants.OrganizationRoles.Owner) + { + var organization = await _dbContext.Organizations.FindAsync(organizationId); + if (organization?.OwnerId == userId) + throw new InvalidOperationException("Cannot revoke owner's access to their own organization"); + } + + userOrg.IsActive = false; + userOrg.RevokedOn = DateTime.UtcNow; + userOrg.LastModifiedOn = DateTime.UtcNow; + userOrg.LastModifiedBy = revokedBy; + + await _dbContext.SaveChangesAsync(); + return true; + } + + /// + /// Update a user's role in an organization + /// + public async Task UpdateUserRoleAsync(string userId, Guid organizationId, string newRole, string modifiedBy) + { + // Validate role + if (!ApplicationConstants.OrganizationRoles.IsValid(newRole)) + throw new ArgumentException($"Invalid role: {newRole}"); + + var userOrg = await _dbContext.UserOrganizations + .FirstOrDefaultAsync(uo => uo.UserId == userId + && uo.OrganizationId == organizationId + && uo.IsActive + && !uo.IsDeleted); + + if (userOrg == null) + return false; + + // Don't allow changing owner role + if (userOrg.Role == ApplicationConstants.OrganizationRoles.Owner) + { + var organization = await _dbContext.Organizations.FindAsync(organizationId); + if (organization?.OwnerId == userId) + throw new InvalidOperationException("Cannot change the role of the organization owner"); + } + + userOrg.Role = newRole; + userOrg.LastModifiedOn = DateTime.UtcNow; + userOrg.LastModifiedBy = modifiedBy; + + await _dbContext.SaveChangesAsync(); + return true; + } + + /// + /// Get all users with access to an organization + /// + public async Task> GetOrganizationUsersAsync(Guid organizationId) + { + return await _dbContext.UserOrganizations + .Where(uo => uo.OrganizationId == organizationId && uo.IsActive && !uo.IsDeleted && uo.UserId != ApplicationConstants.SystemUser.Id) + .OrderBy(uo => uo.Role) + .ThenBy(uo => uo.UserId) + .ToListAsync(); + } + + /// + /// Get all organization assignments for a user (including revoked) + /// + public async Task> GetUserAssignmentsAsync() + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + throw new InvalidOperationException("Cannot get user assignments: User ID is not available in context."); + + return await _dbContext.UserOrganizations + .Include(uo => uo.Organization) + .Where(uo => uo.UserId == userId && !uo.IsDeleted && uo.UserId != ApplicationConstants.SystemUser.Id) + .OrderByDescending(uo => uo.IsActive) + .ThenBy(uo => uo.Organization.Name) + .ToListAsync(); + } + + /// + /// Get all organization assignments for a user (including revoked) + /// + public async Task> GetActiveUserAssignmentsAsync() + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + throw new InvalidOperationException("Cannot get user assignments: User ID is not available in context."); + + return await _dbContext.UserOrganizations + .Include(uo => uo.Organization) + .Where(uo => uo.UserId == userId && !uo.IsDeleted && uo.IsActive && uo.UserId != ApplicationConstants.SystemUser.Id) + .OrderByDescending(uo => uo.IsActive) + .ThenBy(uo => uo.Organization.Name) + .ToListAsync(); + } + + /// + /// Gets organization settings by organization ID (for scheduled tasks). + /// + public async Task GetOrganizationSettingsByOrgIdAsync(Guid organizationId) + { + return await _dbContext.OrganizationSettings + .Where(s => !s.IsDeleted && s.OrganizationId == organizationId) + .FirstOrDefaultAsync(); + } + + /// + /// Gets the organization settings for the current user's active organization. + /// If no settings exist, creates default settings. + /// + public async Task GetOrganizationSettingsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue || organizationId == Guid.Empty) + throw new InvalidOperationException("Organization ID not found for current user"); + + return await GetOrganizationSettingsByOrgIdAsync(organizationId.Value); + } + + /// + /// Updates the organization settings for the current user's organization. + /// + public async Task UpdateOrganizationSettingsAsync(OrganizationSettings settings) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue || organizationId == Guid.Empty) + throw new InvalidOperationException("Organization ID not found for current user"); + + if (settings.OrganizationId != organizationId.Value) + throw new InvalidOperationException("Cannot update settings for a different organization"); + + var userId = await _userContext.GetUserIdAsync(); + settings.LastModifiedOn = DateTime.UtcNow; + settings.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + + _dbContext.OrganizationSettings.Update(settings); + await _dbContext.SaveChangesAsync(); + return true; + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/PaymentService.cs b/Aquiis.Professional/Application/Services/PaymentService.cs new file mode 100644 index 0000000..6fe5289 --- /dev/null +++ b/Aquiis.Professional/Application/Services/PaymentService.cs @@ -0,0 +1,410 @@ +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing Payment entities. + /// Inherits common CRUD operations from BaseService and adds payment-specific business logic. + /// + public class PaymentService : BaseService + { + public PaymentService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings) + : base(context, logger, userContext, settings) + { + } + + /// + /// Validates a payment before create/update operations. + /// + protected override async Task ValidateEntityAsync(Payment entity) + { + var errors = new List(); + + // Required fields + if (entity.InvoiceId == Guid.Empty) + { + errors.Add("Invoice ID is required."); + } + + if (entity.Amount <= 0) + { + errors.Add("Payment amount must be greater than zero."); + } + + if (entity.PaidOn > DateTime.UtcNow.Date.AddDays(1)) + { + errors.Add("Payment date cannot be in the future."); + } + + // Validate invoice exists and belongs to organization + if (entity.InvoiceId != Guid.Empty) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var invoice = await _context.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .FirstOrDefaultAsync(i => i.Id == entity.InvoiceId && !i.IsDeleted); + + if (invoice == null) + { + errors.Add($"Invoice with ID {entity.InvoiceId} does not exist."); + } + else if (invoice.Lease?.Property?.OrganizationId != organizationId) + { + errors.Add("Invoice does not belong to the current organization."); + } + else + { + // Validate payment doesn't exceed invoice balance + var existingPayments = await _context.Payments + .Where(p => p.InvoiceId == entity.InvoiceId + && !p.IsDeleted + && p.Id != entity.Id) // Exclude current payment for updates + .SumAsync(p => p.Amount); + + var totalWithThisPayment = existingPayments + entity.Amount; + var invoiceTotal = invoice.Amount + (invoice.LateFeeAmount ?? 0); + + if (totalWithThisPayment > invoiceTotal) + { + errors.Add($"Payment amount would exceed invoice balance. Invoice total: {invoiceTotal:C}, Already paid: {existingPayments:C}, This payment: {entity.Amount:C}"); + } + } + } + + // Validate payment method + var validMethods = ApplicationConstants.PaymentMethods.AllPaymentMethods; + + if (!string.IsNullOrWhiteSpace(entity.PaymentMethod) && !validMethods.Contains(entity.PaymentMethod)) + { + errors.Add($"Payment method must be one of: {string.Join(", ", validMethods)}"); + } + + if (errors.Any()) + { + throw new ValidationException(string.Join(" ", errors)); + } + } + + /// + /// Creates a payment and automatically updates the associated invoice. + /// + public override async Task CreateAsync(Payment entity) + { + var payment = await base.CreateAsync(entity); + await UpdateInvoiceAfterPaymentChangeAsync(payment.InvoiceId); + return payment; + } + + /// + /// Updates a payment and automatically updates the associated invoice. + /// + public override async Task UpdateAsync(Payment entity) + { + var payment = await base.UpdateAsync(entity); + await UpdateInvoiceAfterPaymentChangeAsync(payment.InvoiceId); + return payment; + } + + /// + /// Deletes a payment and automatically updates the associated invoice. + /// + public override async Task DeleteAsync(Guid id) + { + var payment = await GetByIdAsync(id); + if (payment != null) + { + var invoiceId = payment.InvoiceId; + var result = await base.DeleteAsync(id); + await UpdateInvoiceAfterPaymentChangeAsync(invoiceId); + return result; + } + return false; + } + + /// + /// Gets all payments for a specific invoice. + /// + public async Task> GetPaymentsByInvoiceIdAsync(Guid invoiceId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Payments + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Where(p => p.InvoiceId == invoiceId + && !p.IsDeleted + && p.OrganizationId == organizationId) + .OrderByDescending(p => p.PaidOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPaymentsByInvoiceId"); + throw; + } + } + + /// + /// Gets payments by payment method. + /// + public async Task> GetPaymentsByMethodAsync(string paymentMethod) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Payments + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Where(p => p.PaymentMethod == paymentMethod + && !p.IsDeleted + && p.OrganizationId == organizationId) + .OrderByDescending(p => p.PaidOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPaymentsByMethod"); + throw; + } + } + + /// + /// Gets payments within a specific date range. + /// + public async Task> GetPaymentsByDateRangeAsync(DateTime startDate, DateTime endDate) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Payments + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Where(p => p.PaidOn >= startDate + && p.PaidOn <= endDate + && !p.IsDeleted + && p.OrganizationId == organizationId) + .OrderByDescending(p => p.PaidOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPaymentsByDateRange"); + throw; + } + } + + /// + /// Gets a payment with all related entities loaded. + /// + public async Task GetPaymentWithRelationsAsync(Guid paymentId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Payments + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(p => p.Invoice) + .ThenInclude(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(p => p.Document) + .FirstOrDefaultAsync(p => p.Id == paymentId + && !p.IsDeleted + && p.OrganizationId == organizationId); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPaymentWithRelations"); + throw; + } + } + + /// + /// Calculates the total payments received within a date range. + /// + public async Task CalculateTotalPaymentsAsync(DateTime? startDate = null, DateTime? endDate = null) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var query = _context.Payments + .Where(p => !p.IsDeleted && p.OrganizationId == organizationId); + + if (startDate.HasValue) + { + query = query.Where(p => p.PaidOn >= startDate.Value); + } + + if (endDate.HasValue) + { + query = query.Where(p => p.PaidOn <= endDate.Value); + } + + return await query.SumAsync(p => p.Amount); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "CalculateTotalPayments"); + throw; + } + } + + /// + /// Gets payment summary grouped by payment method. + /// + public async Task> GetPaymentSummaryByMethodAsync(DateTime? startDate = null, DateTime? endDate = null) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var query = _context.Payments + .Where(p => !p.IsDeleted && p.OrganizationId == organizationId); + + if (startDate.HasValue) + { + query = query.Where(p => p.PaidOn >= startDate.Value); + } + + if (endDate.HasValue) + { + query = query.Where(p => p.PaidOn <= endDate.Value); + } + + return await query + .GroupBy(p => p.PaymentMethod) + .Select(g => new { Method = g.Key, Total = g.Sum(p => p.Amount) }) + .ToDictionaryAsync(x => x.Method, x => x.Total); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPaymentSummaryByMethod"); + throw; + } + } + + /// + /// Gets the total amount paid for a specific invoice. + /// + public async Task GetTotalPaidForInvoiceAsync(Guid invoiceId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Payments + .Where(p => p.InvoiceId == invoiceId + && !p.IsDeleted + && p.OrganizationId == organizationId) + .SumAsync(p => p.Amount); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetTotalPaidForInvoice"); + throw; + } + } + + /// + /// Updates the invoice status and paid amount after a payment change. + /// + private async Task UpdateInvoiceAfterPaymentChangeAsync(Guid invoiceId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var invoice = await _context.Invoices + .Include(i => i.Payments) + .FirstOrDefaultAsync(i => i.Id == invoiceId && i.OrganizationId == organizationId); + + if (invoice != null) + { + var totalPaid = invoice.Payments + .Where(p => !p.IsDeleted) + .Sum(p => p.Amount); + + invoice.AmountPaid = totalPaid; + + var totalDue = invoice.Amount + (invoice.LateFeeAmount ?? 0); + + // Update invoice status based on payment + if (totalPaid >= totalDue) + { + invoice.Status = ApplicationConstants.InvoiceStatuses.Paid; + invoice.PaidOn = invoice.Payments + .Where(p => !p.IsDeleted) + .OrderByDescending(p => p.PaidOn) + .FirstOrDefault()?.PaidOn ?? DateTime.UtcNow; + } + else if (totalPaid > 0 && invoice.Status != ApplicationConstants.InvoiceStatuses.Cancelled) + { + // Invoice is partially paid + if (invoice.DueOn < DateTime.Today) + { + invoice.Status = ApplicationConstants.InvoiceStatuses.Overdue; + } + else + { + invoice.Status = ApplicationConstants.InvoiceStatuses.Pending; + } + } + else if (invoice.Status != ApplicationConstants.InvoiceStatuses.Cancelled) + { + // No payments + if (invoice.DueOn < DateTime.Today) + { + invoice.Status = ApplicationConstants.InvoiceStatuses.Overdue; + } + else + { + invoice.Status = ApplicationConstants.InvoiceStatuses.Pending; + } + } + + var userId = await _userContext.GetUserIdAsync(); + invoice.LastModifiedBy = userId ?? "system"; + invoice.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + } + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "UpdateInvoiceAfterPaymentChange"); + throw; + } + } + } +} diff --git a/Aquiis.Professional/Application/Services/PdfGenerators/ChecklistPdfGenerator.cs b/Aquiis.Professional/Application/Services/PdfGenerators/ChecklistPdfGenerator.cs new file mode 100644 index 0000000..1d21717 --- /dev/null +++ b/Aquiis.Professional/Application/Services/PdfGenerators/ChecklistPdfGenerator.cs @@ -0,0 +1,248 @@ +using Aquiis.Professional.Core.Entities; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using QuestPDF.Drawing; + +namespace Aquiis.Professional.Application.Services.PdfGenerators; + +public class ChecklistPdfGenerator +{ + private static bool _fontsRegistered = false; + + public ChecklistPdfGenerator() + { + QuestPDF.Settings.License = LicenseType.Community; + + // Register fonts once + if (!_fontsRegistered) + { + try + { + // Register Lato fonts (from QuestPDF package) + var latoPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LatoFont"); + if (Directory.Exists(latoPath)) + { + FontManager.RegisterFont(File.OpenRead(Path.Combine(latoPath, "Lato-Regular.ttf"))); + FontManager.RegisterFont(File.OpenRead(Path.Combine(latoPath, "Lato-Bold.ttf"))); + FontManager.RegisterFont(File.OpenRead(Path.Combine(latoPath, "Lato-Italic.ttf"))); + FontManager.RegisterFont(File.OpenRead(Path.Combine(latoPath, "Lato-BoldItalic.ttf"))); + } + + // Register DejaVu fonts (custom fonts for Unicode support) + var dejaVuPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Fonts", "DejaVu"); + if (Directory.Exists(dejaVuPath)) + { + FontManager.RegisterFont(File.OpenRead(Path.Combine(dejaVuPath, "DejaVuSans.ttf"))); + FontManager.RegisterFont(File.OpenRead(Path.Combine(dejaVuPath, "DejaVuSans-Bold.ttf"))); + FontManager.RegisterFont(File.OpenRead(Path.Combine(dejaVuPath, "DejaVuSans-Oblique.ttf"))); + FontManager.RegisterFont(File.OpenRead(Path.Combine(dejaVuPath, "DejaVuSans-BoldOblique.ttf"))); + } + + _fontsRegistered = true; + } + catch + { + // If fonts aren't available, QuestPDF will fall back to default fonts + } + } + } + + public byte[] GenerateChecklistPdf(Checklist checklist) + { + var document = QuestPDF.Fluent.Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11).FontFamily("DejaVu Sans")); + + page.Header() + .Column(column => + { + column.Item().Text(text => + { + text.Span("CHECKLIST REPORT\n").FontSize(20).Bold(); + text.Span($"{checklist.Name}\n").FontSize(14).SemiBold(); + }); + + column.Item().PaddingTop(10).Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text($"Type: {checklist.ChecklistType}").FontSize(10); + col.Item().Text($"Status: {checklist.Status}").FontSize(10); + col.Item().Text($"Created: {checklist.CreatedOn:MMM dd, yyyy}").FontSize(10); + if (checklist.CompletedOn.HasValue) + { + col.Item().Text($"Completed: {checklist.CompletedOn:MMM dd, yyyy}").FontSize(10); + } + }); + + row.RelativeItem().Column(col => + { + if (checklist.Property != null) + { + col.Item().Text("Property:").FontSize(10).Bold(); + col.Item().Text($"{checklist.Property.Address ?? "N/A"}").FontSize(10); + col.Item().Text($"{checklist.Property.City ?? ""}, {checklist.Property.State ?? ""} {checklist.Property.ZipCode ?? ""}").FontSize(10); + } + if (checklist.Lease?.Tenant != null) + { + col.Item().Text($"Tenant: {checklist.Lease.Tenant.FirstName ?? ""} {checklist.Lease.Tenant.LastName ?? ""}").FontSize(10); + } + }); + }); + + column.Item().PaddingTop(5).LineHorizontal(1).LineColor(Colors.Grey.Medium); + }); + + page.Content() + .PaddingVertical(1, Unit.Centimetre) + .Column(column => + { + if (checklist.Items == null || !checklist.Items.Any()) + { + column.Item().Text("No items in this checklist.").Italic().FontSize(10); + return; + } + + // Group items by section + var groupedItems = checklist.Items + .OrderBy(i => i.ItemOrder) + .GroupBy(i => i.CategorySection ?? "General"); + + foreach (var group in groupedItems) + { + column.Item().PaddingBottom(5).Text(group.Key) + .FontSize(13) + .Bold() + .FontColor(Colors.Blue.Darken2); + + column.Item().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(25); // Checkbox + columns.RelativeColumn(3); // Item text + columns.RelativeColumn(1); // Value + columns.RelativeColumn(2); // Notes + }); + + // Header + table.Cell().Element(HeaderStyle).Text("✓"); + table.Cell().Element(HeaderStyle).Text("Item"); + table.Cell().Element(HeaderStyle).Text("Value"); + table.Cell().Element(HeaderStyle).Text("Notes"); + + // Items + foreach (var item in group) + { + table.Cell() + .Element(CellStyle) + .AlignCenter() + .Text(item.IsChecked ? "☑" : "☐") + .FontSize(12); + + table.Cell() + .Element(CellStyle) + .Text(item.ItemText); + + table.Cell() + .Element(CellStyle) + .Text(item.Value ?? "-") + .FontSize(10); + + table.Cell() + .Element(CellStyle) + .Text(item.Notes ?? "-") + .FontSize(9) + .Italic(); + } + }); + + column.Item().PaddingBottom(10); + } + + // General Notes Section + if (!string.IsNullOrWhiteSpace(checklist.GeneralNotes)) + { + column.Item().PaddingTop(10).LineHorizontal(1).LineColor(Colors.Grey.Medium); + + column.Item().PaddingTop(10).Column(col => + { + col.Item().Text("General Notes").FontSize(12).Bold(); + col.Item().PaddingTop(5).Border(1).BorderColor(Colors.Grey.Lighten2) + .Padding(10).Background(Colors.Grey.Lighten4) + .Text(checklist.GeneralNotes).FontSize(10); + }); + } + + // Summary + column.Item().PaddingTop(10).LineHorizontal(1).LineColor(Colors.Grey.Medium); + + column.Item().PaddingTop(10).Row(row => + { + var totalItems = checklist.Items.Count; + var checkedItems = checklist.Items.Count(i => i.IsChecked); + var itemsWithValues = checklist.Items.Count(i => !string.IsNullOrEmpty(i.Value)); + var itemsWithNotes = checklist.Items.Count(i => !string.IsNullOrEmpty(i.Notes)); + var progressPercent = totalItems > 0 ? (int)((checkedItems * 100.0) / totalItems) : 0; + + row.RelativeItem().Column(col => + { + col.Item().Text("Summary").FontSize(12).Bold(); + col.Item().Text($"Total Items: {totalItems}").FontSize(10); + col.Item().Text($"Checked: {checkedItems} ({progressPercent}%)").FontSize(10); + col.Item().Text($"Unchecked: {totalItems - checkedItems}").FontSize(10); + }); + + row.RelativeItem().Column(col => + { + col.Item().Text($"Items with Values: {itemsWithValues}").FontSize(10); + col.Item().Text($"Items with Notes: {itemsWithNotes}").FontSize(10); + if (checklist.CompletedBy != null) + { + col.Item().PaddingTop(5).Text($"Completed By: {checklist.CompletedBy}").FontSize(10); + } + }); + }); + }); + + page.Footer() + .AlignCenter() + .DefaultTextStyle(x => x.FontSize(9)) + .Text(text => + { + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" of "); + text.TotalPages(); + text.Span($" • Generated on {DateTime.Now:MMM dd, yyyy h:mm tt}"); + }); + }); + }); + + return document.GeneratePdf(); + } + + private static IContainer CellStyle(IContainer container) + { + return container + .Border(1) + .BorderColor(Colors.Grey.Lighten2) + .Padding(5); + } + + private static IContainer HeaderStyle(IContainer container) + { + return container + .Border(1) + .BorderColor(Colors.Grey.Medium) + .Background(Colors.Grey.Lighten3) + .Padding(5) + .DefaultTextStyle(x => x.FontSize(10).Bold()); + } +} diff --git a/Aquiis.Professional/Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs b/Aquiis.Professional/Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs new file mode 100644 index 0000000..286f0f4 --- /dev/null +++ b/Aquiis.Professional/Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs @@ -0,0 +1,453 @@ +using Aquiis.Professional.Core.Entities; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace Aquiis.Professional.Application.Services.PdfGenerators; + +public class FinancialReportPdfGenerator +{ + public FinancialReportPdfGenerator() + { + QuestPDF.Settings.License = LicenseType.Community; + } + + public byte[] GenerateIncomeStatementPdf(IncomeStatement statement) + { + + var document = QuestPDF.Fluent.Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11)); + + page.Header() + .Text(text => + { + text.Span("INCOME STATEMENT\n").FontSize(20).Bold(); + text.Span($"{(statement.PropertyName ?? "All Properties")}\n").FontSize(14).SemiBold(); + text.Span($"Period: {statement.StartDate:MMM dd, yyyy} - {statement.EndDate:MMM dd, yyyy}") + .FontSize(10).Italic(); + }); + + page.Content() + .PaddingVertical(1, Unit.Centimetre) + .Column(column => + { + column.Spacing(20); + + // Income Section + column.Item().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(3); + columns.RelativeColumn(1); + }); + + table.Cell().Element(HeaderStyle).Text("INCOME"); + table.Cell().Element(HeaderStyle).Text(""); + + table.Cell().PaddingLeft(15).Text("Rent Income"); + table.Cell().AlignRight().Text(statement.TotalRentIncome.ToString("C")); + + table.Cell().PaddingLeft(15).Text("Other Income"); + table.Cell().AlignRight().Text(statement.TotalOtherIncome.ToString("C")); + + table.Cell().Element(SubtotalStyle).Text("Total Income"); + table.Cell().Element(SubtotalStyle).AlignRight().Text(statement.TotalIncome.ToString("C")); + }); + + // Expenses Section + column.Item().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(3); + columns.RelativeColumn(1); + }); + + table.Cell().Element(HeaderStyle).Text("EXPENSES"); + table.Cell().Element(HeaderStyle).Text(""); + + table.Cell().PaddingLeft(15).Text("Maintenance & Repairs"); + table.Cell().AlignRight().Text(statement.MaintenanceExpenses.ToString("C")); + + table.Cell().PaddingLeft(15).Text("Utilities"); + table.Cell().AlignRight().Text(statement.UtilityExpenses.ToString("C")); + + table.Cell().PaddingLeft(15).Text("Insurance"); + table.Cell().AlignRight().Text(statement.InsuranceExpenses.ToString("C")); + + table.Cell().PaddingLeft(15).Text("Property Taxes"); + table.Cell().AlignRight().Text(statement.TaxExpenses.ToString("C")); + + table.Cell().PaddingLeft(15).Text("Management Fees"); + table.Cell().AlignRight().Text(statement.ManagementFees.ToString("C")); + + table.Cell().PaddingLeft(15).Text("Other Expenses"); + table.Cell().AlignRight().Text(statement.OtherExpenses.ToString("C")); + + table.Cell().Element(SubtotalStyle).Text("Total Expenses"); + table.Cell().Element(SubtotalStyle).AlignRight().Text(statement.TotalExpenses.ToString("C")); + }); + + // Net Income Section + column.Item().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(3); + columns.RelativeColumn(1); + }); + + table.Cell().Element(TotalStyle).Text("NET INCOME"); + table.Cell().Element(TotalStyle).AlignRight().Text(statement.NetIncome.ToString("C")); + + table.Cell().PaddingLeft(15).Text("Profit Margin"); + table.Cell().AlignRight().Text($"{statement.ProfitMargin:F2}%"); + }); + }); + + page.Footer() + .AlignCenter() + .Text(x => + { + x.Span("Generated on "); + x.Span(DateTime.Now.ToString("MMM dd, yyyy HH:mm")).SemiBold(); + }); + }); + }); + + return document.GeneratePdf(); + } + + public byte[] GenerateRentRollPdf(List rentRoll, DateTime asOfDate) + { + QuestPDF.Settings.License = LicenseType.Community; + + var document = QuestPDF.Fluent.Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter.Landscape()); + page.Margin(1, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(9)); + + page.Header() + .Text(text => + { + text.Span("RENT ROLL REPORT\n").FontSize(18).Bold(); + text.Span($"As of {asOfDate:MMM dd, yyyy}").FontSize(12).Italic(); + }); + + page.Content() + .PaddingVertical(0.5f, Unit.Centimetre) + .Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(2); + columns.RelativeColumn(2); + columns.RelativeColumn(2); + columns.RelativeColumn(1); + columns.RelativeColumn(2); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + }); + + // Header + table.Header(header => + { + header.Cell().Element(HeaderCellStyle).Text("Property"); + header.Cell().Element(HeaderCellStyle).Text("Address"); + header.Cell().Element(HeaderCellStyle).Text("Tenant"); + header.Cell().Element(HeaderCellStyle).Text("Status"); + header.Cell().Element(HeaderCellStyle).Text("Lease Period"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Rent"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Deposit"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Paid"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Due"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Balance"); + }); + + // Rows + foreach (var item in rentRoll) + { + table.Cell().Text(item.PropertyName); + table.Cell().Text(item.PropertyAddress); + table.Cell().Text(item.TenantName ?? "Vacant"); + table.Cell().Text(item.LeaseStatus); + table.Cell().Text($"{item.LeaseStartDate:MM/dd/yyyy} - {item.LeaseEndDate:MM/dd/yyyy}"); + table.Cell().AlignRight().Text(item.MonthlyRent.ToString("C")); + table.Cell().AlignRight().Text(item.SecurityDeposit.ToString("C")); + table.Cell().AlignRight().Text(item.TotalPaid.ToString("C")); + table.Cell().AlignRight().Text(item.TotalDue.ToString("C")); + table.Cell().AlignRight().Text(item.Balance.ToString("C")); + } + + // Footer + table.Footer(footer => + { + footer.Cell().ColumnSpan(5).Element(FooterCellStyle).Text("TOTALS"); + footer.Cell().Element(FooterCellStyle).AlignRight().Text(rentRoll.Sum(r => r.MonthlyRent).ToString("C")); + footer.Cell().Element(FooterCellStyle).AlignRight().Text(rentRoll.Sum(r => r.SecurityDeposit).ToString("C")); + footer.Cell().Element(FooterCellStyle).AlignRight().Text(rentRoll.Sum(r => r.TotalPaid).ToString("C")); + footer.Cell().Element(FooterCellStyle).AlignRight().Text(rentRoll.Sum(r => r.TotalDue).ToString("C")); + footer.Cell().Element(FooterCellStyle).AlignRight().Text(rentRoll.Sum(r => r.Balance).ToString("C")); + }); + }); + + page.Footer() + .AlignCenter() + .Text(x => + { + x.Span("Page "); + x.CurrentPageNumber(); + x.Span(" of "); + x.TotalPages(); + x.Span(" | Generated on "); + x.Span(DateTime.Now.ToString("MMM dd, yyyy HH:mm")); + }); + }); + }); + + return document.GeneratePdf(); + } + + public byte[] GeneratePropertyPerformancePdf(List performance, DateTime startDate, DateTime endDate) + { + QuestPDF.Settings.License = LicenseType.Community; + + var document = QuestPDF.Fluent.Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter.Landscape()); + page.Margin(1, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(10)); + + page.Header() + .Text(text => + { + text.Span("PROPERTY PERFORMANCE REPORT\n").FontSize(18).Bold(); + text.Span($"Period: {startDate:MMM dd, yyyy} - {endDate:MMM dd, yyyy}").FontSize(12).Italic(); + }); + + page.Content() + .PaddingVertical(0.5f, Unit.Centimetre) + .Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(2); + columns.RelativeColumn(2); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + }); + + // Header + table.Header(header => + { + header.Cell().Element(HeaderCellStyle).Text("Property"); + header.Cell().Element(HeaderCellStyle).Text("Address"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Income"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Expenses"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Net Income"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("ROI %"); + header.Cell().Element(HeaderCellStyle).AlignRight().Text("Occupancy %"); + }); + + // Rows + foreach (var item in performance) + { + table.Cell().Text(item.PropertyName); + table.Cell().Text(item.PropertyAddress); + table.Cell().AlignRight().Text(item.TotalIncome.ToString("C")); + table.Cell().AlignRight().Text(item.TotalExpenses.ToString("C")); + table.Cell().AlignRight().Text(item.NetIncome.ToString("C")); + table.Cell().AlignRight().Text($"{item.ROI:F2}%"); + table.Cell().AlignRight().Text($"{item.OccupancyRate:F1}%"); + } + + // Footer + table.Footer(footer => + { + footer.Cell().ColumnSpan(2).Element(FooterCellStyle).Text("TOTALS"); + footer.Cell().Element(FooterCellStyle).AlignRight().Text(performance.Sum(p => p.TotalIncome).ToString("C")); + footer.Cell().Element(FooterCellStyle).AlignRight().Text(performance.Sum(p => p.TotalExpenses).ToString("C")); + footer.Cell().Element(FooterCellStyle).AlignRight().Text(performance.Sum(p => p.NetIncome).ToString("C")); + footer.Cell().Element(FooterCellStyle).AlignRight().Text($"{performance.Average(p => p.ROI):F2}%"); + footer.Cell().Element(FooterCellStyle).AlignRight().Text($"{performance.Average(p => p.OccupancyRate):F1}%"); + }); + }); + + page.Footer() + .AlignCenter() + .Text(x => + { + x.Span("Generated on "); + x.Span(DateTime.Now.ToString("MMM dd, yyyy HH:mm")); + }); + }); + }); + + return document.GeneratePdf(); + } + + public byte[] GenerateTaxReportPdf(List taxReports) + { + QuestPDF.Settings.License = LicenseType.Community; + + var document = QuestPDF.Fluent.Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(10)); + + page.Header() + .Text(text => + { + text.Span("SCHEDULE E - SUPPLEMENTAL INCOME AND LOSS\n").FontSize(16).Bold(); + text.Span($"Tax Year {taxReports.First().Year}\n").FontSize(12).SemiBold(); + text.Span("Rental Real Estate and Royalties").FontSize(10).Italic(); + }); + + page.Content() + .PaddingVertical(1, Unit.Centimetre) + .Column(column => + { + foreach (var report in taxReports) + { + column.Item().PaddingBottom(15).Column(propertyColumn => + { + propertyColumn.Item().Text(report.PropertyName ?? "Property").FontSize(12).Bold(); + + propertyColumn.Item().PaddingTop(5).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(3); + columns.RelativeColumn(1); + }); + + table.Cell().Element(SectionHeaderStyle).Text("INCOME"); + table.Cell().Element(SectionHeaderStyle).Text(""); + + table.Cell().PaddingLeft(10).Text("3. Rents received"); + table.Cell().AlignRight().Text(report.TotalRentIncome.ToString("C")); + + table.Cell().Element(SectionHeaderStyle).PaddingTop(10).Text("EXPENSES"); + table.Cell().Element(SectionHeaderStyle).PaddingTop(10).Text(""); + + table.Cell().PaddingLeft(10).Text("5. Advertising"); + table.Cell().AlignRight().Text(report.Advertising.ToString("C")); + + table.Cell().PaddingLeft(10).Text("7. Cleaning and maintenance"); + table.Cell().AlignRight().Text(report.Cleaning.ToString("C")); + + table.Cell().PaddingLeft(10).Text("9. Insurance"); + table.Cell().AlignRight().Text(report.Insurance.ToString("C")); + + table.Cell().PaddingLeft(10).Text("11. Legal and professional fees"); + table.Cell().AlignRight().Text(report.Legal.ToString("C")); + + table.Cell().PaddingLeft(10).Text("12. Management fees"); + table.Cell().AlignRight().Text(report.Management.ToString("C")); + + table.Cell().PaddingLeft(10).Text("13. Mortgage interest"); + table.Cell().AlignRight().Text(report.MortgageInterest.ToString("C")); + + table.Cell().PaddingLeft(10).Text("14. Repairs"); + table.Cell().AlignRight().Text(report.Repairs.ToString("C")); + + table.Cell().PaddingLeft(10).Text("15. Supplies"); + table.Cell().AlignRight().Text(report.Supplies.ToString("C")); + + table.Cell().PaddingLeft(10).Text("16. Taxes"); + table.Cell().AlignRight().Text(report.Taxes.ToString("C")); + + table.Cell().PaddingLeft(10).Text("17. Utilities"); + table.Cell().AlignRight().Text(report.Utilities.ToString("C")); + + table.Cell().PaddingLeft(10).Text("18. Depreciation"); + table.Cell().AlignRight().Text(report.DepreciationAmount.ToString("C")); + + table.Cell().PaddingLeft(10).Text("19. Other"); + table.Cell().AlignRight().Text(report.Other.ToString("C")); + + table.Cell().Element(SubtotalStyle).Text("20. Total expenses"); + table.Cell().Element(SubtotalStyle).AlignRight().Text((report.TotalExpenses + report.DepreciationAmount).ToString("C")); + + table.Cell().Element(TotalStyle).PaddingTop(5).Text("21. Net rental income or (loss)"); + table.Cell().Element(TotalStyle).PaddingTop(5).AlignRight().Text(report.TaxableIncome.ToString("C")); + }); + }); + + if (taxReports.Count > 1 && report != taxReports.Last()) + { + column.Item().LineHorizontal(1).LineColor(Colors.Grey.Lighten2); + } + } + }); + + page.Footer() + .AlignCenter() + .Text(x => + { + x.Span("Page "); + x.CurrentPageNumber(); + x.Span(" | Generated on "); + x.Span(DateTime.Now.ToString("MMM dd, yyyy HH:mm")).SemiBold(); + x.Span("\nNote: This is an estimated report. Please consult with a tax professional for accurate filing."); + }); + }); + }); + + return document.GeneratePdf(); + } + + private static IContainer HeaderStyle(IContainer container) + { + return container.BorderBottom(1).BorderColor(Colors.Black).PaddingBottom(5).PaddingTop(10).DefaultTextStyle(x => x.SemiBold().FontSize(12)); + } + + private static IContainer SubtotalStyle(IContainer container) + { + return container.BorderTop(1).BorderColor(Colors.Grey.Medium).PaddingTop(5).PaddingBottom(5).DefaultTextStyle(x => x.SemiBold()); + } + + private static IContainer TotalStyle(IContainer container) + { + return container.BorderTop(2).BorderColor(Colors.Black).PaddingTop(8).DefaultTextStyle(x => x.Bold().FontSize(12)); + } + + private static IContainer HeaderCellStyle(IContainer container) + { + return container.BorderBottom(1).BorderColor(Colors.Black).PaddingBottom(5).DefaultTextStyle(x => x.SemiBold()); + } + + private static IContainer FooterCellStyle(IContainer container) + { + return container.BorderTop(2).BorderColor(Colors.Black).PaddingTop(5).DefaultTextStyle(x => x.Bold()); + } + + private static IContainer SectionHeaderStyle(IContainer container) + { + return container.DefaultTextStyle(x => x.SemiBold().FontSize(11)); + } +} diff --git a/Aquiis.Professional/Application/Services/PdfGenerators/InspectionPdfGenerator.cs b/Aquiis.Professional/Application/Services/PdfGenerators/InspectionPdfGenerator.cs new file mode 100644 index 0000000..8254a8d --- /dev/null +++ b/Aquiis.Professional/Application/Services/PdfGenerators/InspectionPdfGenerator.cs @@ -0,0 +1,362 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Application.Services.PdfGenerators; + +public class InspectionPdfGenerator +{ + public byte[] GenerateInspectionPdf(Inspection inspection) + { + QuestPDF.Settings.License = LicenseType.Community; + + var document = QuestPDF.Fluent.Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(10).FontFamily("Arial")); + + page.Header() + .Height(100) + .Background(Colors.Blue.Darken3) + .Padding(20) + .Column(column => + { + column.Item().Text("PROPERTY INSPECTION REPORT") + .FontSize(20) + .Bold() + .FontColor(Colors.White); + + column.Item().PaddingTop(5).Text(text => + { + text.Span("Inspection Date: ").FontColor(Colors.White); + text.Span(inspection.CompletedOn.ToString("MMMM dd, yyyy")) + .Bold() + .FontColor(Colors.White); + }); + }); + + page.Content() + .PaddingVertical(20) + .Column(column => + { + // Property Information + column.Item().Element(c => PropertySection(c, inspection)); + + // Inspection Details + column.Item().PaddingTop(15).Element(c => InspectionDetailsSection(c, inspection)); + + // Exterior + column.Item().PageBreak(); + column.Item().Element(c => SectionHeader(c, "EXTERIOR INSPECTION")); + column.Item().Element(c => ChecklistTable(c, GetExteriorItems(inspection))); + + // Interior + column.Item().PaddingTop(15).Element(c => SectionHeader(c, "INTERIOR INSPECTION")); + column.Item().Element(c => ChecklistTable(c, GetInteriorItems(inspection))); + + // Kitchen + column.Item().PaddingTop(15).Element(c => SectionHeader(c, "KITCHEN")); + column.Item().Element(c => ChecklistTable(c, GetKitchenItems(inspection))); + + // Bathroom + column.Item().PageBreak(); + column.Item().Element(c => SectionHeader(c, "BATHROOM")); + column.Item().Element(c => ChecklistTable(c, GetBathroomItems(inspection))); + + // Systems & Safety + column.Item().PaddingTop(15).Element(c => SectionHeader(c, "SYSTEMS & SAFETY")); + column.Item().Element(c => ChecklistTable(c, GetSystemsItems(inspection))); + + // Overall Assessment + column.Item().PageBreak(); + column.Item().Element(c => OverallAssessmentSection(c, inspection)); + }); + + page.Footer() + .Height(30) + .AlignCenter() + .DefaultTextStyle(x => x.FontSize(9).FontColor(Colors.Grey.Medium)) + .Text(text => + { + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" of "); + text.TotalPages(); + text.Span($" • Generated on {DateTime.Now:MMM dd, yyyy}"); + }); + }); + }); + + return document.GeneratePdf(); + } + + private void PropertySection(IContainer container, Inspection inspection) + { + container.Background(Colors.Grey.Lighten3) + .Padding(15) + .Column(column => + { + column.Item().Text("PROPERTY INFORMATION") + .FontSize(14) + .Bold() + .FontColor(Colors.Blue.Darken3); + + column.Item().PaddingTop(10).Text(text => + { + text.Span("Address: ").Bold(); + text.Span(inspection.Property?.Address ?? "N/A"); + }); + + column.Item().PaddingTop(5).Text(text => + { + text.Span("Location: ").Bold(); + text.Span($"{inspection.Property?.City}, {inspection.Property?.State} {inspection.Property?.ZipCode}"); + }); + + if (inspection.Property != null) + { + column.Item().PaddingTop(5).Text(text => + { + text.Span("Type: ").Bold(); + text.Span($"{inspection.Property.PropertyType} • "); + text.Span($"{inspection.Property.Bedrooms} bed • "); + text.Span($"{inspection.Property.Bathrooms} bath"); + }); + } + }); + } + + private void InspectionDetailsSection(IContainer container, Inspection inspection) + { + container.Border(1) + .BorderColor(Colors.Grey.Lighten1) + .Padding(15) + .Row(row => + { + row.RelativeItem().Column(column => + { + column.Item().Text("Inspection Type").FontSize(9).FontColor(Colors.Grey.Medium); + column.Item().PaddingTop(3).Text(inspection.InspectionType).Bold(); + }); + + row.RelativeItem().Column(column => + { + column.Item().Text("Overall Condition").FontSize(9).FontColor(Colors.Grey.Medium); + column.Item().PaddingTop(3).Text(inspection.OverallCondition) + .Bold() + .FontColor(GetConditionColor(inspection.OverallCondition)); + }); + + if (!string.IsNullOrEmpty(inspection.InspectedBy)) + { + row.RelativeItem().Column(column => + { + column.Item().Text("Inspected By").FontSize(9).FontColor(Colors.Grey.Medium); + column.Item().PaddingTop(3).Text(inspection.InspectedBy).Bold(); + }); + } + }); + } + + private void SectionHeader(IContainer container, string title) + { + container.Background(Colors.Blue.Lighten4) + .Padding(10) + .Text(title) + .FontSize(12) + .Bold() + .FontColor(Colors.Blue.Darken3); + } + + private void ChecklistTable(IContainer container, List<(string Label, bool IsGood, string? Notes)> items) + { + container.Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(2); + columns.RelativeColumn(1); + columns.RelativeColumn(3); + }); + + table.Header(header => + { + header.Cell().Background(Colors.Grey.Lighten2).Padding(8) + .Text("Item").Bold().FontSize(9); + header.Cell().Background(Colors.Grey.Lighten2).Padding(8) + .Text("Status").Bold().FontSize(9); + header.Cell().Background(Colors.Grey.Lighten2).Padding(8) + .Text("Notes").Bold().FontSize(9); + }); + + foreach (var item in items) + { + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(8) + .Text(item.Label); + + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(8) + .Text(item.IsGood ? "✓ Good" : "✗ Issue") + .FontColor(item.IsGood ? Colors.Green.Darken2 : Colors.Red.Darken1) + .Bold(); + + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2).Padding(8) + .Text(item.Notes ?? "-") + .FontSize(9) + .FontColor(Colors.Grey.Darken1); + } + }); + } + + private void OverallAssessmentSection(IContainer container, Inspection inspection) + { + container.Column(column => + { + column.Item().Element(c => SectionHeader(c, "OVERALL ASSESSMENT")); + + column.Item().PaddingTop(10).Border(1).BorderColor(Colors.Grey.Lighten1).Padding(15) + .Column(innerColumn => + { + innerColumn.Item().Text(text => + { + text.Span("Overall Condition: ").Bold(); + text.Span(inspection.OverallCondition) + .Bold() + .FontColor(GetConditionColor(inspection.OverallCondition)); + }); + + if (!string.IsNullOrEmpty(inspection.GeneralNotes)) + { + innerColumn.Item().PaddingTop(10).Text("General Notes:").Bold(); + innerColumn.Item().PaddingTop(5).Text(inspection.GeneralNotes); + } + + if (!string.IsNullOrEmpty(inspection.ActionItemsRequired)) + { + innerColumn.Item().PaddingTop(15) + .Background(Colors.Orange.Lighten4) + .Padding(10) + .Column(actionColumn => + { + actionColumn.Item().Text("⚠ ACTION ITEMS REQUIRED") + .Bold() + .FontColor(Colors.Orange.Darken2); + actionColumn.Item().PaddingTop(5) + .Text(inspection.ActionItemsRequired); + }); + } + }); + + // Summary Statistics + column.Item().PaddingTop(15).Background(Colors.Grey.Lighten4).Padding(15) + .Row(row => + { + var stats = GetInspectionStats(inspection); + + row.RelativeItem().Column(statColumn => + { + statColumn.Item().Text("Items Checked").FontSize(9).FontColor(Colors.Grey.Medium); + statColumn.Item().PaddingTop(3).Text(stats.TotalItems.ToString()).Bold().FontSize(16); + }); + + row.RelativeItem().Column(statColumn => + { + statColumn.Item().Text("Issues Found").FontSize(9).FontColor(Colors.Grey.Medium); + statColumn.Item().PaddingTop(3).Text(stats.IssuesCount.ToString()) + .Bold() + .FontSize(16) + .FontColor(Colors.Red.Darken1); + }); + + row.RelativeItem().Column(statColumn => + { + statColumn.Item().Text("Pass Rate").FontSize(9).FontColor(Colors.Grey.Medium); + statColumn.Item().PaddingTop(3).Text($"{stats.PassRate:F0}%") + .Bold() + .FontSize(16) + .FontColor(Colors.Green.Darken2); + }); + }); + }); + } + + private string GetConditionColor(string condition) => condition switch + { + "Excellent" => "#28a745", + "Good" => "#17a2b8", + "Fair" => "#ffc107", + "Poor" => "#dc3545", + _ => "#6c757d" + }; + + private (int TotalItems, int IssuesCount, double PassRate) GetInspectionStats(Inspection inspection) + { + var allItems = new List + { + inspection.ExteriorRoofGood, inspection.ExteriorGuttersGood, inspection.ExteriorSidingGood, + inspection.ExteriorWindowsGood, inspection.ExteriorDoorsGood, inspection.ExteriorFoundationGood, + inspection.LandscapingGood, inspection.InteriorWallsGood, inspection.InteriorCeilingsGood, + inspection.InteriorFloorsGood, inspection.InteriorDoorsGood, inspection.InteriorWindowsGood, + inspection.KitchenAppliancesGood, inspection.KitchenCabinetsGood, inspection.KitchenCountersGood, + inspection.KitchenSinkPlumbingGood, inspection.BathroomToiletGood, inspection.BathroomSinkGood, + inspection.BathroomTubShowerGood, inspection.BathroomVentilationGood, inspection.HvacSystemGood, + inspection.ElectricalSystemGood, inspection.PlumbingSystemGood, inspection.SmokeDetectorsGood, + inspection.CarbonMonoxideDetectorsGood + }; + + int total = allItems.Count; + int issues = allItems.Count(x => !x); + double passRate = ((total - issues) / (double)total) * 100; + + return (total, issues, passRate); + } + + private List<(string Label, bool IsGood, string? Notes)> GetExteriorItems(Inspection i) => new() + { + ("Roof", i.ExteriorRoofGood, i.ExteriorRoofNotes), + ("Gutters & Downspouts", i.ExteriorGuttersGood, i.ExteriorGuttersNotes), + ("Siding/Paint", i.ExteriorSidingGood, i.ExteriorSidingNotes), + ("Windows", i.ExteriorWindowsGood, i.ExteriorWindowsNotes), + ("Doors", i.ExteriorDoorsGood, i.ExteriorDoorsNotes), + ("Foundation", i.ExteriorFoundationGood, i.ExteriorFoundationNotes), + ("Landscaping & Drainage", i.LandscapingGood, i.LandscapingNotes) + }; + + private List<(string Label, bool IsGood, string? Notes)> GetInteriorItems(Inspection i) => new() + { + ("Walls", i.InteriorWallsGood, i.InteriorWallsNotes), + ("Ceilings", i.InteriorCeilingsGood, i.InteriorCeilingsNotes), + ("Floors", i.InteriorFloorsGood, i.InteriorFloorsNotes), + ("Doors", i.InteriorDoorsGood, i.InteriorDoorsNotes), + ("Windows", i.InteriorWindowsGood, i.InteriorWindowsNotes) + }; + + private List<(string Label, bool IsGood, string? Notes)> GetKitchenItems(Inspection i) => new() + { + ("Appliances", i.KitchenAppliancesGood, i.KitchenAppliancesNotes), + ("Cabinets & Drawers", i.KitchenCabinetsGood, i.KitchenCabinetsNotes), + ("Countertops", i.KitchenCountersGood, i.KitchenCountersNotes), + ("Sink & Plumbing", i.KitchenSinkPlumbingGood, i.KitchenSinkPlumbingNotes) + }; + + private List<(string Label, bool IsGood, string? Notes)> GetBathroomItems(Inspection i) => new() + { + ("Toilet", i.BathroomToiletGood, i.BathroomToiletNotes), + ("Sink & Vanity", i.BathroomSinkGood, i.BathroomSinkNotes), + ("Tub/Shower", i.BathroomTubShowerGood, i.BathroomTubShowerNotes), + ("Ventilation/Exhaust Fan", i.BathroomVentilationGood, i.BathroomVentilationNotes) + }; + + private List<(string Label, bool IsGood, string? Notes)> GetSystemsItems(Inspection i) => new() + { + ("HVAC System", i.HvacSystemGood, i.HvacSystemNotes), + ("Electrical System", i.ElectricalSystemGood, i.ElectricalSystemNotes), + ("Plumbing System", i.PlumbingSystemGood, i.PlumbingSystemNotes), + ("Smoke Detectors", i.SmokeDetectorsGood, i.SmokeDetectorsNotes), + ("Carbon Monoxide Detectors", i.CarbonMonoxideDetectorsGood, i.CarbonMonoxideDetectorsNotes) + }; +} diff --git a/Aquiis.Professional/Application/Services/PdfGenerators/InvoicePdfGenerator.cs b/Aquiis.Professional/Application/Services/PdfGenerators/InvoicePdfGenerator.cs new file mode 100644 index 0000000..4c6188a --- /dev/null +++ b/Aquiis.Professional/Application/Services/PdfGenerators/InvoicePdfGenerator.cs @@ -0,0 +1,244 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Application.Services.PdfGenerators +{ + public class InvoicePdfGenerator + { + public static byte[] GenerateInvoicePdf(Invoice invoice) + { + // Configure QuestPDF license + QuestPDF.Settings.License = LicenseType.Community; + + var document = QuestPDF.Fluent.Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Arial")); + + page.Header().Element(content => ComposeHeader(content, invoice)); + page.Content().Element(content => ComposeContent(content, invoice)); + page.Footer().AlignCenter().Text(x => + { + x.Span("Page "); + x.CurrentPageNumber(); + x.Span(" of "); + x.TotalPages(); + }); + }); + }); + + return document.GeneratePdf(); + } + + private static void ComposeHeader(IContainer container, Invoice invoice) + { + container.Column(column => + { + column.Item().Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text("INVOICE").FontSize(24).Bold(); + col.Item().PaddingTop(5).Text($"Invoice #: {invoice.InvoiceNumber}").FontSize(12).Bold(); + }); + + row.ConstantItem(150).Column(col => + { + col.Item().AlignRight().Text($"Date: {invoice.InvoicedOn:MMMM dd, yyyy}").FontSize(10); + col.Item().AlignRight().Text($"Due Date: {invoice.DueOn:MMMM dd, yyyy}").FontSize(10); + col.Item().PaddingTop(5).AlignRight() + .Background(GetStatusColor(invoice.Status)) + .Padding(5) + .Text(invoice.Status).FontColor(Colors.White).Bold(); + }); + }); + + column.Item().PaddingTop(10).LineHorizontal(2).LineColor(Colors.Grey.Darken2); + }); + } + + private static void ComposeContent(IContainer container, Invoice invoice) + { + container.PaddingVertical(20).Column(column => + { + column.Spacing(15); + + // Bill To Section + column.Item().Row(row => + { + row.RelativeItem().Element(c => ComposeBillTo(c, invoice)); + row.ConstantItem(20); + row.RelativeItem().Element(c => ComposePropertyInfo(c, invoice)); + }); + + // Invoice Details + column.Item().PaddingTop(10).Element(c => ComposeInvoiceDetails(c, invoice)); + + // Payments Section (if any) + if (invoice.Payments != null && invoice.Payments.Any(p => !p.IsDeleted)) + { + column.Item().PaddingTop(15).Element(c => ComposePaymentsSection(c, invoice)); + } + + // Total Section + column.Item().PaddingTop(20).Element(c => ComposeTotalSection(c, invoice)); + + // Notes Section + if (!string.IsNullOrWhiteSpace(invoice.Notes)) + { + column.Item().PaddingTop(20).Element(c => ComposeNotes(c, invoice)); + } + }); + } + + private static void ComposeBillTo(IContainer container, Invoice invoice) + { + container.Column(column => + { + column.Item().Text("BILL TO:").FontSize(10).Bold().FontColor(Colors.Grey.Darken1); + column.Item().PaddingTop(5).Column(col => + { + if (invoice.Lease?.Tenant != null) + { + col.Item().Text(invoice.Lease.Tenant.FullName ?? "N/A").FontSize(12).Bold(); + col.Item().Text(invoice.Lease.Tenant.Email ?? "").FontSize(10); + col.Item().Text(invoice.Lease.Tenant.PhoneNumber ?? "").FontSize(10); + } + }); + }); + } + + private static void ComposePropertyInfo(IContainer container, Invoice invoice) + { + container.Column(column => + { + column.Item().Text("PROPERTY:").FontSize(10).Bold().FontColor(Colors.Grey.Darken1); + column.Item().PaddingTop(5).Column(col => + { + if (invoice.Lease?.Property != null) + { + col.Item().Text(invoice.Lease.Property.Address ?? "N/A").FontSize(12).Bold(); + col.Item().Text($"{invoice.Lease.Property.City}, {invoice.Lease.Property.State} {invoice.Lease.Property.ZipCode}").FontSize(10); + } + }); + }); + } + + private static void ComposeInvoiceDetails(IContainer container, Invoice invoice) + { + container.Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(3); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + }); + + // Header + table.Header(header => + { + header.Cell().Background(Colors.Grey.Lighten2).Padding(8).Text("Description").Bold(); + header.Cell().Background(Colors.Grey.Lighten2).Padding(8).AlignRight().Text("Amount").Bold(); + header.Cell().Background(Colors.Grey.Lighten2).Padding(8).AlignRight().Text("Status").Bold(); + }); + + // Row + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten1).Padding(8) + .Text($"{invoice.Description}\nPeriod: {invoice.InvoicedOn:MMM dd, yyyy} - {invoice.DueOn:MMM dd, yyyy}"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten1).Padding(8) + .AlignRight().Text(invoice.Amount.ToString("C")).FontSize(12); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten1).Padding(8) + .AlignRight().Text(invoice.Status); + }); + } + + private static void ComposePaymentsSection(IContainer container, Invoice invoice) + { + container.Column(column => + { + column.Item().Text("PAYMENTS RECEIVED:").FontSize(12).Bold(); + column.Item().PaddingTop(5).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(2); + columns.RelativeColumn(2); + columns.RelativeColumn(1); + }); + + // Header + table.Header(header => + { + header.Cell().Background(Colors.Grey.Lighten3).Padding(5).Text("Date").Bold().FontSize(9); + header.Cell().Background(Colors.Grey.Lighten3).Padding(5).Text("Method").Bold().FontSize(9); + header.Cell().Background(Colors.Grey.Lighten3).Padding(5).AlignRight().Text("Amount").Bold().FontSize(9); + }); + + // Rows + foreach (var payment in invoice.Payments.Where(p => !p.IsDeleted).OrderBy(p => p.PaidOn)) + { + table.Cell().Padding(5).Text(payment.PaidOn.ToString("MMM dd, yyyy")).FontSize(9); + table.Cell().Padding(5).Text(payment.PaymentMethod ?? "N/A").FontSize(9); + table.Cell().Padding(5).AlignRight().Text(payment.Amount.ToString("C")).FontSize(9); + } + }); + }); + } + + private static void ComposeTotalSection(IContainer container, Invoice invoice) + { + container.AlignRight().Column(column => + { + column.Spacing(5); + + column.Item().BorderTop(1).BorderColor(Colors.Grey.Darken1).PaddingTop(10).Row(row => + { + row.ConstantItem(150).Text("Invoice Total:").FontSize(12); + row.ConstantItem(100).AlignRight().Text(invoice.Amount.ToString("C")).FontSize(12).Bold(); + }); + + column.Item().Row(row => + { + row.ConstantItem(150).Text("Paid Amount:").FontSize(12); + row.ConstantItem(100).AlignRight().Text(invoice.AmountPaid.ToString("C")).FontSize(12).FontColor(Colors.Green.Darken2); + }); + + column.Item().BorderTop(2).BorderColor(Colors.Grey.Darken2).PaddingTop(5).Row(row => + { + row.ConstantItem(150).Text("Balance Due:").FontSize(14).Bold(); + row.ConstantItem(100).AlignRight().Text((invoice.Amount - invoice.AmountPaid).ToString("C")) + .FontSize(14).Bold().FontColor(invoice.Status == "Paid" ? Colors.Green.Darken2 : Colors.Red.Darken2); + }); + }); + } + + private static void ComposeNotes(IContainer container, Invoice invoice) + { + container.Column(column => + { + column.Item().Text("NOTES:").FontSize(10).Bold().FontColor(Colors.Grey.Darken1); + column.Item().PaddingTop(5).Border(1).BorderColor(Colors.Grey.Lighten1).Padding(10) + .Text(invoice.Notes).FontSize(9); + }); + } + + private static string GetStatusColor(string status) + { + return status switch + { + "Paid" => Colors.Green.Darken2, + "Overdue" => Colors.Red.Darken2, + "Pending" => Colors.Orange.Darken1, + "Partially Paid" => Colors.Blue.Darken1, + _ => Colors.Grey.Darken1 + }; + } + } +} diff --git a/Aquiis.Professional/Application/Services/PdfGenerators/LeasePdfGenerator.cs b/Aquiis.Professional/Application/Services/PdfGenerators/LeasePdfGenerator.cs new file mode 100644 index 0000000..ba6ff8e --- /dev/null +++ b/Aquiis.Professional/Application/Services/PdfGenerators/LeasePdfGenerator.cs @@ -0,0 +1,262 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Application.Services.PdfGenerators +{ + public class LeasePdfGenerator + { + public static async Task GenerateLeasePdf(Lease lease) + { + // Configure QuestPDF license + QuestPDF.Settings.License = LicenseType.Community; + + var document = QuestPDF.Fluent.Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Arial")); + + page.Header().Element(ComposeHeader); + page.Content().Element(content => ComposeContent(content, lease)); + page.Footer().AlignCenter().Text(x => + { + x.CurrentPageNumber(); + x.Span(" / "); + x.TotalPages(); + }); + }); + }); + + return await Task.FromResult(document.GeneratePdf()); + } + + private static void ComposeHeader(IContainer container) + { + container.Row(row => + { + row.RelativeItem().Column(column => + { + column.Item().Text("RESIDENTIAL LEASE AGREEMENT").FontSize(18).Bold(); + column.Item().PaddingTop(5).Text($"Generated: {DateTime.Now:MMMM dd, yyyy}").FontSize(9); + }); + }); + } + + private static void ComposeContent(IContainer container, Lease lease) + { + container.PaddingVertical(20).Column(column => + { + column.Spacing(15); + + // Property Information Section + column.Item().Element(c => ComposeSectionHeader(c, "PROPERTY INFORMATION")); + column.Item().Element(c => ComposePropertyInfo(c, lease)); + + // Tenant Information Section + column.Item().Element(c => ComposeSectionHeader(c, "TENANT INFORMATION")); + column.Item().Element(c => ComposeTenantInfo(c, lease)); + + // Lease Terms Section + column.Item().Element(c => ComposeSectionHeader(c, "LEASE TERMS")); + column.Item().Element(c => ComposeLeaseTerms(c, lease)); + + // Financial Information Section + column.Item().Element(c => ComposeSectionHeader(c, "FINANCIAL TERMS")); + column.Item().Element(c => ComposeFinancialInfo(c, lease)); + + // Additional Terms Section + if (!string.IsNullOrWhiteSpace(lease.Terms)) + { + column.Item().Element(c => ComposeSectionHeader(c, "ADDITIONAL TERMS AND CONDITIONS")); + column.Item().Element(c => ComposeAdditionalTerms(c, lease)); + } + + // Signatures Section + column.Item().PaddingTop(30).Element(ComposeSignatures); + }); + } + + private static void ComposeSectionHeader(IContainer container, string title) + { + container.Background(Colors.Grey.Lighten3).Padding(8).Text(title).FontSize(12).Bold(); + } + + private static void ComposePropertyInfo(IContainer container, Lease lease) + { + container.Padding(10).Column(column => + { + column.Spacing(5); + + if (lease.Property != null) + { + column.Item().Row(row => + { + row.ConstantItem(120).Text("Address:").Bold(); + row.RelativeItem().Text(lease.Property.Address ?? "N/A"); + }); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("City, State:").Bold(); + row.RelativeItem().Text($"{lease.Property.City}, {lease.Property.State} {lease.Property.ZipCode}"); + }); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("Property Type:").Bold(); + row.RelativeItem().Text(lease.Property.PropertyType ?? "N/A"); + }); + + if (lease.Property.Bedrooms > 0 || lease.Property.Bathrooms > 0) + { + column.Item().Row(row => + { + row.ConstantItem(120).Text("Bedrooms/Baths:").Bold(); + row.RelativeItem().Text($"{lease.Property.Bedrooms} bed / {lease.Property.Bathrooms} bath"); + }); + } + } + }); + } + + private static void ComposeTenantInfo(IContainer container, Lease lease) + { + container.Padding(10).Column(column => + { + column.Spacing(5); + + if (lease.Tenant != null) + { + column.Item().Row(row => + { + row.ConstantItem(120).Text("Name:").Bold(); + row.RelativeItem().Text(lease.Tenant.FullName ?? "N/A"); + }); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("Email:").Bold(); + row.RelativeItem().Text(lease.Tenant.Email ?? "N/A"); + }); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("Phone:").Bold(); + row.RelativeItem().Text(lease.Tenant.PhoneNumber ?? "N/A"); + }); + } + }); + } + + private static void ComposeLeaseTerms(IContainer container, Lease lease) + { + container.Padding(10).Column(column => + { + column.Spacing(5); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("Lease Start Date:").Bold(); + row.RelativeItem().Text(lease.StartDate.ToString("MMMM dd, yyyy")); + }); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("Lease End Date:").Bold(); + row.RelativeItem().Text(lease.EndDate.ToString("MMMM dd, yyyy")); + }); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("Lease Duration:").Bold(); + row.RelativeItem().Text($"{(lease.EndDate - lease.StartDate).Days} days"); + }); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("Lease Status:").Bold(); + row.RelativeItem().Text(lease.Status ?? "N/A"); + }); + }); + } + + private static void ComposeFinancialInfo(IContainer container, Lease lease) + { + container.Padding(10).Column(column => + { + column.Spacing(5); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("Monthly Rent:").Bold(); + row.RelativeItem().Text(lease.MonthlyRent.ToString("C")); + }); + + column.Item().Row(row => + { + row.ConstantItem(120).Text("Security Deposit:").Bold(); + row.RelativeItem().Text(lease.SecurityDeposit.ToString("C")); + }); + + var totalRent = lease.MonthlyRent * ((lease.EndDate - lease.StartDate).Days / 30.0m); + column.Item().Row(row => + { + row.ConstantItem(120).Text("Total Rent:").Bold(); + row.RelativeItem().Text($"{totalRent:C} (approximate)"); + }); + }); + } + + private static void ComposeAdditionalTerms(IContainer container, Lease lease) + { + container.Padding(10).Text(lease.Terms).FontSize(10); + } + + private static void ComposeSignatures(IContainer container) + { + container.Column(column => + { + column.Spacing(30); + + column.Item().Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().BorderBottom(1).BorderColor(Colors.Black).PaddingBottom(2).Text(""); + col.Item().PaddingTop(5).Text("Landlord Signature").FontSize(9); + }); + + row.ConstantItem(50); + + row.RelativeItem().Column(col => + { + col.Item().BorderBottom(1).BorderColor(Colors.Black).PaddingBottom(2).Text(""); + col.Item().PaddingTop(5).Text("Date").FontSize(9); + }); + }); + + column.Item().Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().BorderBottom(1).BorderColor(Colors.Black).PaddingBottom(2).Text(""); + col.Item().PaddingTop(5).Text("Tenant Signature").FontSize(9); + }); + + row.ConstantItem(50); + + row.RelativeItem().Column(col => + { + col.Item().BorderBottom(1).BorderColor(Colors.Black).PaddingBottom(2).Text(""); + col.Item().PaddingTop(5).Text("Date").FontSize(9); + }); + }); + }); + } + } +} diff --git a/Aquiis.Professional/Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs b/Aquiis.Professional/Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs new file mode 100644 index 0000000..064dd10 --- /dev/null +++ b/Aquiis.Professional/Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs @@ -0,0 +1,238 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Professional.Core.Entities; +using PdfDocument = QuestPDF.Fluent.Document; + +namespace Aquiis.Professional.Application.Services.PdfGenerators +{ + public class LeaseRenewalPdfGenerator + { + public byte[] GenerateRenewalOfferLetter(Lease lease, Property property, Tenant tenant) + { + // Configure QuestPDF license + QuestPDF.Settings.License = LicenseType.Community; + + var document = PdfDocument.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter); + page.Margin(50); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Arial")); + + page.Header() + .Height(100) + .Background(Colors.Grey.Lighten3) + .Padding(20) + .Column(column => + { + column.Item().Text("LEASE RENEWAL OFFER") + .FontSize(20) + .Bold() + .FontColor(Colors.Blue.Darken2); + + column.Item().PaddingTop(5).Text(DateTime.Now.ToString("MMMM dd, yyyy")) + .FontSize(10) + .FontColor(Colors.Grey.Darken1); + }); + + page.Content() + .PaddingVertical(20) + .Column(column => + { + // Tenant Information + column.Item().PaddingBottom(20).Column(c => + { + c.Item().Text("Dear " + tenant.FullName + ",") + .FontSize(12) + .Bold(); + + c.Item().PaddingTop(10).Text(text => + { + text.Line("RE: Lease Renewal Offer"); + text.Span("Property Address: ").Bold(); + text.Span(property.Address); + text.Line(""); + text.Span(property.City + ", " + property.State + " " + property.ZipCode); + }); + }); + + // Introduction + column.Item().PaddingBottom(15).Text(text => + { + text.Span("We hope you have enjoyed living at "); + text.Span(property.Address).Bold(); + text.Span(". As your current lease is approaching its expiration date on "); + text.Span(lease.EndDate.ToString("MMMM dd, yyyy")).Bold(); + text.Span(", we would like to offer you the opportunity to renew your lease."); + }); + + // Current Lease Details + column.Item().PaddingBottom(20).Column(c => + { + c.Item().Text("Current Lease Information:") + .FontSize(12) + .Bold() + .FontColor(Colors.Blue.Darken2); + + c.Item().PaddingTop(10).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(2); + columns.RelativeColumn(3); + }); + + // Header + table.Cell().Background(Colors.Grey.Lighten2) + .Padding(8).Text("Detail").Bold(); + table.Cell().Background(Colors.Grey.Lighten2) + .Padding(8).Text("Information").Bold(); + + // Rows + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text("Lease Start Date"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text(lease.StartDate.ToString("MMMM dd, yyyy")); + + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text("Lease End Date"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text(lease.EndDate.ToString("MMMM dd, yyyy")); + + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text("Current Monthly Rent"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text(lease.MonthlyRent.ToString("C")); + + table.Cell().Padding(8).Text("Security Deposit"); + table.Cell().Padding(8).Text(lease.SecurityDeposit.ToString("C")); + }); + }); + + // Renewal Offer Details + column.Item().PaddingBottom(20).Column(c => + { + c.Item().Text("Renewal Offer Details:") + .FontSize(12) + .Bold() + .FontColor(Colors.Blue.Darken2); + + c.Item().PaddingTop(10).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(2); + columns.RelativeColumn(3); + }); + + // Header + table.Cell().Background(Colors.Grey.Lighten2) + .Padding(8).Text("Detail").Bold(); + table.Cell().Background(Colors.Grey.Lighten2) + .Padding(8).Text("Proposed Terms").Bold(); + + // Rows + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text("New Lease Start Date"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text(lease.EndDate.AddDays(1).ToString("MMMM dd, yyyy")); + + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text("New Lease End Date"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text(lease.EndDate.AddYears(1).ToString("MMMM dd, yyyy")); + + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text("Proposed Monthly Rent"); + table.Cell().BorderBottom(1).BorderColor(Colors.Grey.Lighten2) + .Padding(8).Text(text => + { + text.Span((lease.ProposedRenewalRent ?? lease.MonthlyRent).ToString("C")).Bold(); + + if (lease.ProposedRenewalRent.HasValue && lease.ProposedRenewalRent != lease.MonthlyRent) + { + var increase = lease.ProposedRenewalRent.Value - lease.MonthlyRent; + var percentage = (increase / lease.MonthlyRent) * 100; + text.Span(" ("); + text.Span(increase > 0 ? "+" : ""); + text.Span(increase.ToString("C") + ", "); + text.Span(percentage.ToString("F1") + "%"); + text.Span(")").FontSize(9).Italic(); + } + }); + + table.Cell().Padding(8).Text("Lease Term"); + table.Cell().Padding(8).Text("12 months"); + }); + }); + + // Renewal Notes + if (!string.IsNullOrEmpty(lease.RenewalNotes)) + { + column.Item().PaddingBottom(15).Column(c => + { + c.Item().Text("Additional Information:") + .FontSize(12) + .Bold() + .FontColor(Colors.Blue.Darken2); + + c.Item().PaddingTop(8) + .PaddingLeft(10) + .Text(lease.RenewalNotes) + .Italic(); + }); + } + + // Response Instructions + column.Item().PaddingBottom(15).Column(c => + { + c.Item().Text("Next Steps:") + .FontSize(12) + .Bold() + .FontColor(Colors.Blue.Darken2); + + c.Item().PaddingTop(8).Text(text => + { + text.Line("Please review this renewal offer carefully. We would appreciate your response by " + + lease.EndDate.AddDays(-45).ToString("MMMM dd, yyyy") + "."); + text.Line(""); + text.Line("To accept this renewal offer, please:"); + text.Line(" • Contact our office at your earliest convenience"); + text.Line(" • Sign and return the new lease agreement"); + text.Line(" • Continue to maintain the property in excellent condition"); + }); + }); + + // Closing + column.Item().PaddingTop(20).Column(c => + { + c.Item().Text("We value you as a tenant and hope you will choose to renew your lease. " + + "If you have any questions or concerns, please do not hesitate to contact us."); + + c.Item().PaddingTop(15).Text("Sincerely,"); + c.Item().PaddingTop(30).Text("Property Management") + .Bold(); + }); + }); + + page.Footer() + .Height(50) + .AlignCenter() + .Text(text => + { + text.Span("This is an official lease renewal offer. Please retain this document for your records."); + text.Line(""); + text.Span("Generated on " + DateTime.Now.ToString("MMMM dd, yyyy 'at' h:mm tt")) + .FontSize(8) + .FontColor(Colors.Grey.Darken1); + }); + }); + }); + + return document.GeneratePdf(); + } + } +} diff --git a/Aquiis.Professional/Application/Services/PdfGenerators/PaymentPdfGenerator.cs b/Aquiis.Professional/Application/Services/PdfGenerators/PaymentPdfGenerator.cs new file mode 100644 index 0000000..0c72be0 --- /dev/null +++ b/Aquiis.Professional/Application/Services/PdfGenerators/PaymentPdfGenerator.cs @@ -0,0 +1,256 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Application.Services.PdfGenerators +{ + public class PaymentPdfGenerator + { + public static byte[] GeneratePaymentReceipt(Payment payment) + { + // Configure QuestPDF license + QuestPDF.Settings.License = LicenseType.Community; + + var document = QuestPDF.Fluent.Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.Letter); + page.Margin(2, Unit.Centimetre); + page.PageColor(Colors.White); + page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Arial")); + + page.Header().Element(content => ComposeHeader(content, payment)); + page.Content().Element(content => ComposeContent(content, payment)); + page.Footer().AlignCenter().Text(x => + { + x.Span("Generated: "); + x.Span(DateTime.Now.ToString("MMMM dd, yyyy hh:mm tt")); + }); + }); + }); + + return document.GeneratePdf(); + } + + private static void ComposeHeader(IContainer container, Payment payment) + { + container.Column(column => + { + column.Item().Row(row => + { + row.RelativeItem().Column(col => + { + col.Item().Text("PAYMENT RECEIPT").FontSize(24).Bold(); + col.Item().PaddingTop(5).Text($"Receipt Date: {payment.PaidOn:MMMM dd, yyyy}").FontSize(12); + }); + + row.ConstantItem(150).Column(col => + { + col.Item().AlignRight() + .Background(Colors.Green.Darken2) + .Padding(10) + .Text("PAID").FontColor(Colors.White).FontSize(16).Bold(); + }); + }); + + column.Item().PaddingTop(10).LineHorizontal(2).LineColor(Colors.Grey.Darken2); + }); + } + + private static void ComposeContent(IContainer container, Payment payment) + { + container.PaddingVertical(20).Column(column => + { + column.Spacing(20); + + // Payment Amount (Prominent) + column.Item().Background(Colors.Grey.Lighten3).Padding(20).Column(col => + { + col.Item().AlignCenter().Text("AMOUNT PAID").FontSize(14).FontColor(Colors.Grey.Darken1); + col.Item().AlignCenter().Text(payment.Amount.ToString("C")).FontSize(32).Bold().FontColor(Colors.Green.Darken2); + }); + + // Payment Information + column.Item().Element(c => ComposePaymentInfo(c, payment)); + + // Invoice Information + if (payment.Invoice != null) + { + column.Item().Element(c => ComposeInvoiceInfo(c, payment)); + } + + // Tenant and Property Information + column.Item().Row(row => + { + row.RelativeItem().Element(c => ComposeTenantInfo(c, payment)); + row.ConstantItem(20); + row.RelativeItem().Element(c => ComposePropertyInfo(c, payment)); + }); + + // Additional Information + if (!string.IsNullOrWhiteSpace(payment.Notes)) + { + column.Item().Element(c => ComposeNotes(c, payment)); + } + + // Footer Message + column.Item().PaddingTop(30).AlignCenter().Text("Thank you for your payment!") + .FontSize(14).Italic().FontColor(Colors.Grey.Darken1); + }); + } + + private static void ComposePaymentInfo(IContainer container, Payment payment) + { + container.Column(column => + { + column.Item().Background(Colors.Blue.Lighten4).Padding(10).Text("PAYMENT DETAILS").FontSize(12).Bold(); + column.Item().Border(1).BorderColor(Colors.Grey.Lighten1).Padding(15).Column(col => + { + col.Spacing(8); + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Payment Date:").Bold(); + row.RelativeItem().Text(payment.PaidOn.ToString("MMMM dd, yyyy")); + }); + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Payment Method:").Bold(); + row.RelativeItem().Text(payment.PaymentMethod ?? "N/A"); + }); + + if (!string.IsNullOrWhiteSpace(payment.Invoice.InvoiceNumber)) + { + col.Item().Row(row => + { + row.ConstantItem(150).Text("Transaction Reference:").Bold(); + row.RelativeItem().Text(payment.Invoice.InvoiceNumber); + }); + } + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Amount Paid:").Bold(); + row.RelativeItem().Text(payment.Amount.ToString("C")).FontSize(14).FontColor(Colors.Green.Darken2).Bold(); + }); + }); + }); + } + + private static void ComposeInvoiceInfo(IContainer container, Payment payment) + { + container.Column(column => + { + column.Item().Background(Colors.Grey.Lighten3).Padding(10).Text("INVOICE INFORMATION").FontSize(12).Bold(); + column.Item().Border(1).BorderColor(Colors.Grey.Lighten1).Padding(15).Column(col => + { + col.Spacing(8); + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Invoice Number:").Bold(); + row.RelativeItem().Text(payment.Invoice!.InvoiceNumber ?? "N/A"); + }); + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Invoice Date:").Bold(); + row.RelativeItem().Text(payment.Invoice.InvoicedOn.ToString("MMMM dd, yyyy")); + }); + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Due Date:").Bold(); + row.RelativeItem().Text(payment.Invoice.DueOn.ToString("MMMM dd, yyyy")); + }); + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Invoice Total:").Bold(); + row.RelativeItem().Text(payment.Invoice.Amount.ToString("C")); + }); + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Total Paid:").Bold(); + row.RelativeItem().Text(payment.Invoice.AmountPaid.ToString("C")); + }); + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Balance Remaining:").Bold(); + row.RelativeItem().Text((payment.Invoice.Amount - payment.Invoice.AmountPaid).ToString("C")) + .FontColor(payment.Invoice.Status == "Paid" ? Colors.Green.Darken2 : Colors.Orange.Darken1); + }); + + col.Item().Row(row => + { + row.ConstantItem(150).Text("Invoice Status:").Bold(); + row.RelativeItem().Text(payment.Invoice.Status ?? "N/A") + .FontColor(payment.Invoice.Status == "Paid" ? Colors.Green.Darken2 : Colors.Grey.Darken1); + }); + }); + }); + } + + private static void ComposeTenantInfo(IContainer container, Payment payment) + { + container.Column(column => + { + column.Item().Text("TENANT INFORMATION").FontSize(10).Bold().FontColor(Colors.Grey.Darken1); + column.Item().PaddingTop(5).Column(col => + { + if (payment.Invoice?.Lease?.Tenant != null) + { + var tenant = payment.Invoice.Lease.Tenant; + col.Item().Text(tenant.FullName ?? "N/A").FontSize(12).Bold(); + col.Item().Text(tenant.Email ?? "").FontSize(10); + col.Item().Text(tenant.PhoneNumber ?? "").FontSize(10); + } + else + { + col.Item().Text("N/A").FontSize(10); + } + }); + }); + } + + private static void ComposePropertyInfo(IContainer container, Payment payment) + { + container.Column(column => + { + column.Item().Text("PROPERTY INFORMATION").FontSize(10).Bold().FontColor(Colors.Grey.Darken1); + column.Item().PaddingTop(5).Column(col => + { + if (payment.Invoice?.Lease?.Property != null) + { + var property = payment.Invoice.Lease.Property; + col.Item().Text(property.Address ?? "N/A").FontSize(12).Bold(); + col.Item().Text($"{property.City}, {property.State} {property.ZipCode}").FontSize(10); + if (!string.IsNullOrWhiteSpace(property.PropertyType)) + { + col.Item().Text($"Type: {property.PropertyType}").FontSize(10); + } + } + else + { + col.Item().Text("N/A").FontSize(10); + } + }); + }); + } + + private static void ComposeNotes(IContainer container, Payment payment) + { + container.Column(column => + { + column.Item().Text("NOTES:").FontSize(10).Bold().FontColor(Colors.Grey.Darken1); + column.Item().PaddingTop(5).Border(1).BorderColor(Colors.Grey.Lighten1).Padding(10) + .Text(payment.Notes).FontSize(9); + }); + } + } +} diff --git a/Aquiis.Professional/Application/Services/PropertyManagementService.cs b/Aquiis.Professional/Application/Services/PropertyManagementService.cs new file mode 100644 index 0000000..baab68b --- /dev/null +++ b/Aquiis.Professional/Application/Services/PropertyManagementService.cs @@ -0,0 +1,2677 @@ +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Shared.Components.Account; +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Shared.Services; +using System.Security.Claims; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + public class PropertyManagementService + { + private readonly ApplicationDbContext _dbContext; + private readonly UserManager _userManager; + private readonly ApplicationSettings _applicationSettings; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly UserContextService _userContext; + private readonly CalendarEventService _calendarEventService; + private readonly ChecklistService _checklistService; + + public PropertyManagementService( + ApplicationDbContext dbContext, + UserManager userManager, + IOptions settings, + IHttpContextAccessor httpContextAccessor, + UserContextService userContext, + CalendarEventService calendarEventService, + ChecklistService checklistService) + { + _dbContext = dbContext; + _userManager = userManager; + _applicationSettings = settings.Value; + _httpContextAccessor = httpContextAccessor; + _userContext = userContext; + _calendarEventService = calendarEventService; + _checklistService = checklistService; + } + + #region Properties + public async Task> GetPropertiesAsync() + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Properties + .Include(p => p.Leases) + .Include(p => p.Documents) + .Where(p => !p.IsDeleted && p.OrganizationId == organizationId) + .ToListAsync(); + } + + public async Task GetPropertyByIdAsync(Guid propertyId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Properties + .Include(p => p.Leases) + .Include(p => p.Documents) + .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == organizationId && !p.IsDeleted); + } + + public async Task> SearchPropertiesByAddressAsync(string searchTerm) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrWhiteSpace(searchTerm)) + { + return await _dbContext.Properties + .Where(p => !p.IsDeleted && p.OrganizationId == organizationId) + .OrderBy(p => p.Address) + .Take(20) + .ToListAsync(); + } + + return await _dbContext.Properties + .Where(p => !p.IsDeleted && + p.OrganizationId == organizationId && + (p.Address.Contains(searchTerm) || + p.City.Contains(searchTerm) || + p.State.Contains(searchTerm) || + p.ZipCode.Contains(searchTerm))) + .OrderBy(p => p.Address) + .Take(20) + .ToListAsync(); + } + + public async Task AddPropertyAsync(Property property) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Set tracking fields automatically + property.Id = Guid.NewGuid(); + property.OrganizationId = organizationId!.Value; + property.CreatedBy = _userId; + property.CreatedOn = DateTime.UtcNow; + + // Set initial routine inspection due date to 30 days from creation + property.NextRoutineInspectionDueDate = DateTime.Today.AddDays(30); + + await _dbContext.Properties.AddAsync(property); + await _dbContext.SaveChangesAsync(); + + // Create calendar event for the first routine inspection + await CreateRoutineInspectionCalendarEventAsync(property); + } + + public async Task UpdatePropertyAsync(Property property) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify property belongs to active organization + var existing = await _dbContext.Properties + .FirstOrDefaultAsync(p => p.Id == property.Id && p.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Property {property.Id} not found in active organization."); + } + + // Set tracking fields automatically + property.LastModifiedBy = _userId; + property.LastModifiedOn = DateTime.UtcNow; + property.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(property); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeletePropertyAsync(Guid propertyId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (_applicationSettings.SoftDeleteEnabled) + { + await SoftDeletePropertyAsync(propertyId); + return; + } + else + { + var property = await _dbContext.Properties + .FirstOrDefaultAsync(p => p.Id == propertyId && + p.OrganizationId == organizationId); + + if (property != null) + { + _dbContext.Properties.Remove(property); + await _dbContext.SaveChangesAsync(); + } + } + } + + private async Task SoftDeletePropertyAsync(Guid propertyId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var property = await _dbContext.Properties + .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == organizationId); + + if (property != null && !property.IsDeleted) + { + property.IsDeleted = true; + property.LastModifiedOn = DateTime.UtcNow; + property.LastModifiedBy = _userId; + _dbContext.Properties.Update(property); + await _dbContext.SaveChangesAsync(); + + var leases = await GetLeasesByPropertyIdAsync(propertyId); + foreach (var lease in leases) + { + lease.Status = ApplicationConstants.LeaseStatuses.Terminated; + lease.LastModifiedOn = DateTime.UtcNow; + lease.LastModifiedBy = _userId; + await UpdateLeaseAsync(lease); + + var tenants = await GetTenantsByLeaseIdAsync(lease.Id); + foreach (var tenant in tenants) + { + var tenantLeases = await GetLeasesByTenantIdAsync(tenant.Id); + tenantLeases = tenantLeases.Where(l => l.PropertyId != propertyId && !l.IsDeleted).ToList(); + + if(tenantLeases.Count == 0) // Only this lease + { + tenant.IsActive = false; + tenant.LastModifiedBy = _userId; + tenant.LastModifiedOn = DateTime.UtcNow; + await UpdateTenantAsync(tenant); + } + } + + } + + } + } + #endregion + + #region Tenants + + public async Task> GetTenantsAsync() + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Tenants + .Include(t => t.Leases) + .Where(t => !t.IsDeleted && t.OrganizationId == organizationId) + .ToListAsync(); + } + + public async Task> GetTenantsByLeaseIdAsync(Guid leaseId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var leases = await _dbContext.Leases + .Include(l => l.Tenant) + .Where(l => l.Id == leaseId && l.Tenant!.OrganizationId == organizationId && !l.IsDeleted && !l.Tenant.IsDeleted) + .ToListAsync(); + + var tenantIds = leases.Select(l => l.TenantId).Distinct().ToList(); + + return await _dbContext.Tenants + .Where(t => tenantIds.Contains(t.Id) && t.OrganizationId == organizationId && !t.IsDeleted) + .ToListAsync(); + } + public async Task> GetTenantsByPropertyIdAsync(Guid propertyId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var leases = await _dbContext.Leases + .Include(l => l.Tenant) + .Where(l => l.PropertyId == propertyId && l.Tenant!.OrganizationId == organizationId && !l.IsDeleted && !l.Tenant.IsDeleted) + .ToListAsync(); + + var tenantIds = leases.Select(l => l.TenantId).Distinct().ToList(); + + return await _dbContext.Tenants + .Where(t => tenantIds.Contains(t.Id) && t.OrganizationId == organizationId && !t.IsDeleted) + .ToListAsync(); + } + + public async Task GetTenantByIdAsync(Guid tenantId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Tenants + .Include(t => t.Leases) + .FirstOrDefaultAsync(t => t.Id == tenantId && t.OrganizationId == organizationId && !t.IsDeleted); + } + + public async Task GetTenantByIdentificationNumberAsync(string identificationNumber) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Tenants + .Include(t => t.Leases) + .FirstOrDefaultAsync(t => t.IdentificationNumber == identificationNumber && t.OrganizationId == organizationId && !t.IsDeleted); + } + + public async Task AddTenantAsync(Tenant tenant) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Set tracking fields automatically + tenant.Id = Guid.NewGuid(); + tenant.OrganizationId = organizationId!.Value; + tenant.CreatedBy = _userId; + tenant.CreatedOn = DateTime.UtcNow; + + await _dbContext.Tenants.AddAsync(tenant); + await _dbContext.SaveChangesAsync(); + + return tenant; + } + + public async Task UpdateTenantAsync(Tenant tenant) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify tenant belongs to active organization + var existing = await _dbContext.Tenants + .FirstOrDefaultAsync(t => t.Id == tenant.Id && t.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Tenant {tenant.Id} not found in active organization."); + } + + // Set tracking fields automatically + tenant.LastModifiedOn = DateTime.UtcNow; + tenant.LastModifiedBy = _userId; + tenant.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(tenant); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteTenantAsync(Tenant tenant) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + if (_applicationSettings.SoftDeleteEnabled) + { + await SoftDeleteTenantAsync(tenant); + return; + } + else + { + if (tenant != null) + { + _dbContext.Tenants.Remove(tenant); + await _dbContext.SaveChangesAsync(); + } + } + } + + private async Task SoftDeleteTenantAsync(Tenant tenant) + { + var userId = await _userContext.GetUserIdAsync(); + + if (tenant != null && !tenant.IsDeleted && !string.IsNullOrEmpty(userId)) + { + tenant.IsDeleted = true; + tenant.LastModifiedOn = DateTime.UtcNow; + tenant.LastModifiedBy = userId; + _dbContext.Tenants.Update(tenant); + await _dbContext.SaveChangesAsync(); + } + } + + #endregion + + #region Leases + + public async Task> GetLeasesAsync() + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => !l.IsDeleted && !l.Tenant!.IsDeleted && !l.Property.IsDeleted && l.Property.OrganizationId == organizationId) + .ToListAsync(); + } + public async Task GetLeaseByIdAsync(Guid leaseId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .FirstOrDefaultAsync(l => l.Id == leaseId && !l.IsDeleted && (l.Tenant == null || !l.Tenant.IsDeleted) && !l.Property.IsDeleted && l.Property.OrganizationId == organizationId); + } + + public async Task> GetLeasesByPropertyIdAsync(Guid propertyId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var leases = await _dbContext.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.PropertyId == propertyId && !l.IsDeleted && l.Property.OrganizationId == organizationId) + .ToListAsync(); + + return leases; + } + + public async Task> GetCurrentAndUpcomingLeasesByPropertyIdAsync(Guid propertyId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.PropertyId == propertyId + && !l.IsDeleted + && l.Property.OrganizationId == organizationId + && (l.Status == ApplicationConstants.LeaseStatuses.Pending + || l.Status == ApplicationConstants.LeaseStatuses.Active)) + .ToListAsync(); + } + + public async Task> GetActiveLeasesByPropertyIdAsync(Guid propertyId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var leases = await _dbContext.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.PropertyId == propertyId && !l.IsDeleted && !l.Tenant!.IsDeleted && !l.Property.IsDeleted && l.Property.OrganizationId == organizationId) + .ToListAsync(); + + return leases + .Where(l => l.IsActive) + .ToList(); + } + + + public async Task> GetLeasesByTenantIdAsync(Guid tenantId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.TenantId == tenantId && !l.Tenant!.IsDeleted && !l.IsDeleted && l.Property.OrganizationId == organizationId) + .ToListAsync(); + } + + public async Task AddLeaseAsync(Lease lease) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var property = await GetPropertyByIdAsync(lease.PropertyId); + if(property is null || property.OrganizationId != organizationId) + return lease; + + // Set tracking fields automatically + lease.Id = Guid.NewGuid(); + lease.OrganizationId = organizationId!.Value; + lease.CreatedBy = _userId; + lease.CreatedOn = DateTime.UtcNow; + + await _dbContext.Leases.AddAsync(lease); + + property.IsAvailable = false; + property.LastModifiedOn = DateTime.UtcNow; + property.LastModifiedBy = _userId; + + _dbContext.Properties.Update(property); + + await _dbContext.SaveChangesAsync(); + + return lease; + } + + public async Task UpdateLeaseAsync(Lease lease) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify lease belongs to active organization + var existing = await _dbContext.Leases + .Include(l => l.Property) + .FirstOrDefaultAsync(l => l.Id == lease.Id && l.Property.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Lease {lease.Id} not found in active organization."); + } + + // Set tracking fields automatically + lease.LastModifiedOn = DateTime.UtcNow; + lease.LastModifiedBy = _userId; + + _dbContext.Entry(existing).CurrentValues.SetValues(lease); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteLeaseAsync(Guid leaseId) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if( !await _dbContext.Leases.AnyAsync(l => l.Id == leaseId && l.Property.OrganizationId == organizationId)) + { + throw new UnauthorizedAccessException("User does not have access to this lease."); + } + + if (_applicationSettings.SoftDeleteEnabled) + { + await SoftDeleteLeaseAsync(leaseId); + return; + } + else + { + var lease = await _dbContext.Leases.FirstOrDefaultAsync(l => l.Id == leaseId); + if (lease != null) + { + _dbContext.Leases.Remove(lease); + await _dbContext.SaveChangesAsync(); + } + } + } + + private async Task SoftDeleteLeaseAsync(Guid leaseId) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + // Handle the case when the user is not authenticated + throw new UnauthorizedAccessException("User is not authenticated."); + } + + + var lease = await _dbContext.Leases.FirstOrDefaultAsync(l => l.Id == leaseId && l.Property.OrganizationId == organizationId); + if (lease != null && !lease.IsDeleted && !string.IsNullOrEmpty(userId)) + { + lease.IsDeleted = true; + lease.LastModifiedOn = DateTime.UtcNow; + lease.LastModifiedBy = userId; + _dbContext.Leases.Update(lease); + await _dbContext.SaveChangesAsync(); + } + } + + #endregion + + #region Invoices + + public async Task> GetInvoicesAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Payments) + .Where(i => !i.IsDeleted && i.Lease.Property.OrganizationId == organizationId) + .OrderByDescending(i => i.DueOn) + .ToListAsync(); + } + + public async Task GetInvoiceByIdAsync(Guid invoiceId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + + return await _dbContext.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Payments) + .FirstOrDefaultAsync(i => i.Id == invoiceId + && !i.IsDeleted + && i.Lease.Property.OrganizationId == organizationId); + } + + public async Task> GetInvoicesByLeaseIdAsync(Guid leaseId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Payments) + .Where(i => i.LeaseId == leaseId + && !i.IsDeleted + && i.Lease.Property.OrganizationId == organizationId) + .OrderByDescending(i => i.DueOn) + .ToListAsync(); + } + + public async Task AddInvoiceAsync(Invoice invoice) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var lease = await _dbContext.Leases + .Include(l => l.Property) + .FirstOrDefaultAsync(l => l.Id == invoice.LeaseId && !l.IsDeleted); + + if (lease == null || lease.Property.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User does not have access to this lease."); + } + + // Set tracking fields automatically + invoice.Id = Guid.NewGuid(); + invoice.OrganizationId = organizationId!.Value; + invoice.CreatedBy = _userId; + invoice.CreatedOn = DateTime.UtcNow; + + await _dbContext.Invoices.AddAsync(invoice); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateInvoiceAsync(Invoice invoice) + { + var userId = await _userContext.GetUserIdAsync(); + if (userId == null) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify invoice belongs to active organization + var existing = await _dbContext.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .FirstOrDefaultAsync(i => i.Id == invoice.Id && i.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Invoice {invoice.Id} not found in active organization."); + } + + // Set tracking fields automatically + invoice.LastModifiedOn = DateTime.UtcNow; + invoice.LastModifiedBy = userId; + invoice.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(invoice); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteInvoiceAsync(Invoice invoice) + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + if (_applicationSettings.SoftDeleteEnabled) + { + invoice.IsDeleted = true; + invoice.LastModifiedOn = DateTime.UtcNow; + invoice.LastModifiedBy = userId; + _dbContext.Invoices.Update(invoice); + } + else + { + _dbContext.Invoices.Remove(invoice); + } + await _dbContext.SaveChangesAsync(); + } + + public async Task GenerateInvoiceNumberAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var invoiceCount = await _dbContext.Invoices + .Where(i => i.OrganizationId == organizationId) + .CountAsync(); + + var nextNumber = invoiceCount + 1; + return $"INV-{DateTime.Now:yyyyMM}-{nextNumber:D5}"; + } + + #endregion + + #region Payments + + public async Task> GetPaymentsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Payments + .Include(p => p.Invoice) + .ThenInclude(i => i!.Lease) + .ThenInclude(l => l!.Property) + .Include(p => p.Invoice) + .ThenInclude(i => i!.Lease) + .ThenInclude(l => l!.Tenant) + .Where(p => !p.IsDeleted && p.Invoice.Lease.Property.OrganizationId == organizationId) + .OrderByDescending(p => p.PaidOn) + .ToListAsync(); + } + + + public async Task GetPaymentByIdAsync(Guid paymentId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.Payments + .Include(p => p.Invoice) + .ThenInclude(i => i!.Lease) + .ThenInclude(l => l!.Property) + .Include(p => p.Invoice) + .ThenInclude(i => i!.Lease) + .ThenInclude(l => l!.Tenant) + .FirstOrDefaultAsync(p => p.Id == paymentId && !p.IsDeleted && p.Invoice.Lease.Property.OrganizationId == organizationId); + } + + public async Task> GetPaymentsByInvoiceIdAsync(Guid invoiceId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.Payments + .Include(p => p.Invoice) + .Where(p => p.InvoiceId == invoiceId && !p.IsDeleted && p.Invoice.Lease.Property.OrganizationId == organizationId) + .OrderByDescending(p => p.PaidOn) + .ToListAsync(); + } + + public async Task AddPaymentAsync(Payment payment) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Set tracking fields automatically + payment.Id = Guid.NewGuid(); + payment.OrganizationId = organizationId!.Value; + payment.CreatedBy = _userId; + payment.CreatedOn = DateTime.UtcNow; + + await _dbContext.Payments.AddAsync(payment); + await _dbContext.SaveChangesAsync(); + + // Update invoice paid amount + await UpdateInvoicePaidAmountAsync(payment.InvoiceId); + } + + public async Task UpdatePaymentAsync(Payment payment) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify payment belongs to active organization + var existing = await _dbContext.Payments + .FirstOrDefaultAsync(p => p.Id == payment.Id && p.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Payment {payment.Id} not found in active organization."); + } + + // Set tracking fields automatically + payment.OrganizationId = organizationId!.Value; + payment.LastModifiedOn = DateTime.UtcNow; + payment.LastModifiedBy = _userId; + + _dbContext.Entry(existing).CurrentValues.SetValues(payment); + await _dbContext.SaveChangesAsync(); + + // Update invoice paid amount + await UpdateInvoicePaidAmountAsync(payment.InvoiceId); + } + + public async Task DeletePaymentAsync(Payment payment) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrEmpty(userId) || payment.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var invoiceId = payment.InvoiceId; + + if (_applicationSettings.SoftDeleteEnabled) + { + payment.IsDeleted = true; + payment.LastModifiedOn = DateTime.UtcNow; + payment.LastModifiedBy = userId; + _dbContext.Payments.Update(payment); + } + else + { + _dbContext.Payments.Remove(payment); + } + await _dbContext.SaveChangesAsync(); + + // Update invoice paid amount + await UpdateInvoicePaidAmountAsync(invoiceId); + } + + private async Task UpdateInvoicePaidAmountAsync(Guid invoiceId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var invoice = await _dbContext.Invoices.Where(i => i.Id == invoiceId && i.OrganizationId == organizationId).FirstOrDefaultAsync(); + if (invoice != null) + { + var totalPaid = await _dbContext.Payments + .Where(p => p.InvoiceId == invoiceId && !p.IsDeleted && p.OrganizationId == organizationId) + .SumAsync(p => p.Amount); + + invoice.AmountPaid = totalPaid; + + // Update invoice status based on payment + if (totalPaid >= invoice.Amount) + { + invoice.Status = "Paid"; + invoice.PaidOn = DateTime.UtcNow; + } + else if (totalPaid > 0) + { + invoice.Status = "Partial"; + } + + await _dbContext.SaveChangesAsync(); + } + } + + #endregion + + #region Documents + + public async Task> GetDocumentsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .ThenInclude(l => l!.Property) + .Include(d => d.Lease) + .ThenInclude(l => l!.Tenant) + .Include(d => d.Invoice) + .Include(d => d.Payment) + .Where(d => !d.IsDeleted && d.OrganizationId == organizationId && d.Property != null && !d.Property.IsDeleted) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + + public async Task GetDocumentByIdAsync(Guid documentId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + + return await _dbContext.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .ThenInclude(l => l!.Property) + .Include(d => d.Lease) + .ThenInclude(l => l!.Tenant) + .Include(d => d.Invoice) + .Include(d => d.Payment) + .FirstOrDefaultAsync(d => d.Id == documentId && !d.IsDeleted && d.OrganizationId == organizationId); + } + + public async Task> GetDocumentsByLeaseIdAsync(Guid leaseId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Documents + .Include(d => d.Lease) + .ThenInclude(l => l!.Property) + .Include(d => d.Lease) + .ThenInclude(l => l!.Tenant) + .Where(d => d.LeaseId == leaseId && !d.IsDeleted && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + + public async Task> GetDocumentsByPropertyIdAsync(Guid propertyId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .Where(d => d.PropertyId == propertyId && !d.IsDeleted && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + + public async Task> GetDocumentsByTenantIdAsync(Guid tenantId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Documents + .Include(d => d.Property) + .Include(d => d.Tenant) + .Include(d => d.Lease) + .Where(d => d.TenantId == tenantId && !d.IsDeleted && d.OrganizationId == organizationId) + .OrderByDescending(d => d.CreatedOn) + .ToListAsync(); + } + + public async Task AddDocumentAsync(Document document) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var _userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + document.Id = Guid.NewGuid(); + document.OrganizationId = organizationId!.Value; + document.CreatedBy = _userId; + document.CreatedOn = DateTime.UtcNow; + _dbContext.Documents.Add(document); + await _dbContext.SaveChangesAsync(); + return document; + } + + public async Task UpdateDocumentAsync(Document document) + { + var _userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + // Security: Verify document belongs to active organization + var existing = await _dbContext.Documents + .FirstOrDefaultAsync(d => d.Id == document.Id && d.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Document {document.Id} not found in active organization."); + } + + // Set tracking fields automatically + document.LastModifiedBy = _userId; + document.LastModifiedOn = DateTime.UtcNow; + document.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(document); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteDocumentAsync(Document document) + { + + var _userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrEmpty(_userId) || document.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + if (!_applicationSettings.SoftDeleteEnabled) + { + _dbContext.Documents.Remove(document); + } + else + { + document.IsDeleted = true; + document.LastModifiedBy = _userId; + document.LastModifiedOn = DateTime.UtcNow; + _dbContext.Documents.Update(document); + + // Clear reverse foreign keys in related entities + // Since soft delete doesn't trigger DB cascade, we need to manually clear DocumentId + + // Clear Inspection.DocumentId if any inspection links to this document + var inspection = await _dbContext.Inspections + .FirstOrDefaultAsync(i => i.DocumentId == document.Id); + if (inspection != null) + { + inspection.DocumentId = null; + inspection.LastModifiedBy = _userId; + inspection.LastModifiedOn = DateTime.UtcNow; + _dbContext.Inspections.Update(inspection); + } + + // Clear Lease.DocumentId if any lease links to this document + var lease = await _dbContext.Leases + .FirstOrDefaultAsync(l => l.DocumentId == document.Id); + if (lease != null) + { + lease.DocumentId = null; + lease.LastModifiedBy = _userId; + lease.LastModifiedOn = DateTime.UtcNow; + _dbContext.Leases.Update(lease); + } + + // Clear Invoice.DocumentId if any invoice links to this document + if (document.InvoiceId != null) + { + var invoice = await _dbContext.Invoices + .FirstOrDefaultAsync(i => i.Id == document.InvoiceId.Value && i.DocumentId == document.Id); + if (invoice != null) + { + invoice.DocumentId = null; + invoice.LastModifiedBy = _userId; + invoice.LastModifiedOn = DateTime.UtcNow; + _dbContext.Invoices.Update(invoice); + } + } + + // Clear Payment.DocumentId if any payment links to this document + if (document.PaymentId != null) + { + var payment = await _dbContext.Payments + .FirstOrDefaultAsync(p => p.Id == document.PaymentId.Value && p.DocumentId == document.Id); + if (payment != null) + { + payment.DocumentId = null; + payment.LastModifiedBy = _userId; + payment.LastModifiedOn = DateTime.UtcNow; + _dbContext.Payments.Update(payment); + } + } + } + await _dbContext.SaveChangesAsync(); + } + + #endregion + + #region Inspections + + public async Task> GetInspectionsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Inspections + .Include(i => i.Property) + .Include(i => i.Lease) + .ThenInclude(l => l!.Tenant) + .Where(i => !i.IsDeleted && i.OrganizationId == organizationId) + .OrderByDescending(i => i.CompletedOn) + .ToListAsync(); + } + + public async Task> GetInspectionsByPropertyIdAsync(Guid propertyId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Inspections + .Include(i => i.Property) + .Include(i => i.Lease) + .ThenInclude(l => l!.Tenant) + .Where(i => i.PropertyId == propertyId && !i.IsDeleted && i.OrganizationId == organizationId) + .OrderByDescending(i => i.CompletedOn) + .ToListAsync(); + } + + public async Task GetInspectionByIdAsync(Guid inspectionId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Inspections + .Include(i => i.Property) + .Include(i => i.Lease) + .ThenInclude(l => l!.Tenant) + .FirstOrDefaultAsync(i => i.Id == inspectionId && !i.IsDeleted && i.OrganizationId == organizationId); + } + + public async Task AddInspectionAsync(Inspection inspection) + { + + var _userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + inspection.Id = Guid.NewGuid(); + inspection.OrganizationId = organizationId!.Value; + inspection.CreatedBy = _userId; + inspection.CreatedOn = DateTime.UtcNow; + await _dbContext.Inspections.AddAsync(inspection); + await _dbContext.SaveChangesAsync(); + + // Create calendar event for the inspection + await _calendarEventService.CreateOrUpdateEventAsync(inspection); + + // Update property inspection tracking if this is a routine inspection + if (inspection.InspectionType == "Routine") + { + // Find and update/delete the original property-based routine inspection calendar event + var propertyBasedEvent = await _dbContext.CalendarEvents + .FirstOrDefaultAsync(e => + e.PropertyId == inspection.PropertyId && + e.SourceEntityType == "Property" && + e.EventType == CalendarEventTypes.Inspection && + !e.IsDeleted); + + if (propertyBasedEvent != null) + { + // Remove the old property-based event since we now have an actual inspection record + _dbContext.CalendarEvents.Remove(propertyBasedEvent); + await _dbContext.SaveChangesAsync(); + } + + await UpdatePropertyInspectionTrackingAsync( + inspection.PropertyId, + inspection.CompletedOn); + } + } + + public async Task UpdateInspectionAsync(Inspection inspection) + { + var _userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + // Security: Verify inspection belongs to active organization + var existing = await _dbContext.Inspections + .FirstOrDefaultAsync(i => i.Id == inspection.Id && i.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Inspection {inspection.Id} not found in active organization."); + } + + // Set tracking fields automatically + inspection.LastModifiedBy = _userId; + inspection.LastModifiedOn = DateTime.UtcNow; + inspection.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(inspection); + await _dbContext.SaveChangesAsync(); + + // Update calendar event + await _calendarEventService.CreateOrUpdateEventAsync(inspection); + } + + public async Task DeleteInspectionAsync(Guid inspectionId) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (string.IsNullOrEmpty(userId) || !organizationId.HasValue || organizationId == Guid.Empty) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var inspection = await _dbContext.Inspections.FindAsync(inspectionId); + if (inspection != null && !inspection.IsDeleted) + { + if (_applicationSettings.SoftDeleteEnabled) + { + inspection.IsDeleted = true; + inspection.LastModifiedOn = DateTime.UtcNow; + inspection.LastModifiedBy = userId; + _dbContext.Inspections.Update(inspection); + } + else + { + _dbContext.Inspections.Remove(inspection); + } + await _dbContext.SaveChangesAsync(); + + // Delete associated calendar event + await _calendarEventService.DeleteEventAsync(inspection.CalendarEventId); + } + } + + #endregion + + #region Inspection Tracking + + /// + /// Updates property inspection tracking after a routine inspection is completed + /// + public async Task UpdatePropertyInspectionTrackingAsync(Guid propertyId, DateTime inspectionDate, int intervalMonths = 12) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var property = await _dbContext.Properties.FindAsync(propertyId); + if (property == null || property.IsDeleted || property.OrganizationId != organizationId) + { + throw new InvalidOperationException("Property not found."); + } + + property.LastRoutineInspectionDate = inspectionDate; + property.NextRoutineInspectionDueDate = inspectionDate.AddMonths(intervalMonths); + property.RoutineInspectionIntervalMonths = intervalMonths; + property.LastModifiedOn = DateTime.UtcNow; + + var userId = await _userContext.GetUserIdAsync(); + property.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + + _dbContext.Properties.Update(property); + await _dbContext.SaveChangesAsync(); + } + + /// + /// Gets properties with overdue routine inspections + /// + public async Task> GetPropertiesWithOverdueInspectionsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Properties + .Where(p => p.OrganizationId == organizationId && + !p.IsDeleted && + p.NextRoutineInspectionDueDate.HasValue && + p.NextRoutineInspectionDueDate.Value < DateTime.Today) + .OrderBy(p => p.NextRoutineInspectionDueDate) + .ToListAsync(); + } + + /// + /// Gets properties with inspections due within specified days + /// + public async Task> GetPropertiesWithInspectionsDueSoonAsync(int daysAhead = 30) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var dueDate = DateTime.Today.AddDays(daysAhead); + + return await _dbContext.Properties + .Where(p => p.OrganizationId == organizationId && + !p.IsDeleted && + p.NextRoutineInspectionDueDate.HasValue && + p.NextRoutineInspectionDueDate.Value >= DateTime.Today && + p.NextRoutineInspectionDueDate.Value <= dueDate) + .OrderBy(p => p.NextRoutineInspectionDueDate) + .ToListAsync(); + } + + /// + /// Gets count of properties with overdue inspections + /// + public async Task GetOverdueInspectionCountAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.Properties + .CountAsync(p => p.OrganizationId == organizationId && + !p.IsDeleted && + p.NextRoutineInspectionDueDate.HasValue && + p.NextRoutineInspectionDueDate.Value < DateTime.Today); + } + + /// + /// Initializes inspection tracking for a property (sets first inspection due date) + /// + public async Task InitializePropertyInspectionTrackingAsync(Guid propertyId, int intervalMonths = 12) + { + var property = await _dbContext.Properties.FindAsync(propertyId); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (property == null || property.IsDeleted || property.OrganizationId != organizationId) + { + throw new InvalidOperationException("Property not found."); + } + + if (!property.NextRoutineInspectionDueDate.HasValue) + { + property.NextRoutineInspectionDueDate = DateTime.Today.AddMonths(intervalMonths); + property.RoutineInspectionIntervalMonths = intervalMonths; + property.LastModifiedOn = DateTime.UtcNow; + + var userId = await _userContext.GetUserIdAsync(); + property.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + + _dbContext.Properties.Update(property); + await _dbContext.SaveChangesAsync(); + } + } + + /// + /// Creates a calendar event for a routine property inspection + /// + private async Task CreateRoutineInspectionCalendarEventAsync(Property property) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (property == null || property.IsDeleted || property.OrganizationId != organizationId) + { + throw new InvalidOperationException("Property not found."); + } + + if (!property.NextRoutineInspectionDueDate.HasValue) + { + return; + } + + + var userId = await _userContext.GetUserIdAsync(); + + var calendarEvent = new CalendarEvent + { + Id = Guid.NewGuid(), + Title = $"Routine Inspection - {property.Address}", + Description = $"Routine inspection due for property at {property.Address}, {property.City}, {property.State}", + StartOn = property.NextRoutineInspectionDueDate.Value, + DurationMinutes = 60, // Default 1 hour for inspection + EventType = CalendarEventTypes.Inspection, + Status = "Scheduled", + PropertyId = property.Id, + Location = $"{property.Address}, {property.City}, {property.State} {property.ZipCode}", + Color = CalendarEventTypes.GetColor(CalendarEventTypes.Inspection), + Icon = CalendarEventTypes.GetIcon(CalendarEventTypes.Inspection), + OrganizationId = property.OrganizationId, + CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId, + CreatedOn = DateTime.UtcNow, + SourceEntityType = "Property", + SourceEntityId = property.Id + }; + + _dbContext.CalendarEvents.Add(calendarEvent); + await _dbContext.SaveChangesAsync(); + } + + #endregion + + #region Maintenance Requests + + public async Task> GetMaintenanceRequestsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.OrganizationId == organizationId && !m.IsDeleted) + .OrderByDescending(m => m.RequestedOn) + .ToListAsync(); + } + + public async Task> GetMaintenanceRequestsByPropertyAsync(Guid propertyId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.PropertyId == propertyId && m.OrganizationId == organizationId && !m.IsDeleted) + .OrderByDescending(m => m.RequestedOn) + .ToListAsync(); + } + + public async Task> GetMaintenanceRequestsByLeaseAsync(Guid leaseId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.LeaseId == leaseId && m.OrganizationId == organizationId && !m.IsDeleted) + .OrderByDescending(m => m.RequestedOn) + .ToListAsync(); + } + + public async Task> GetMaintenanceRequestsByStatusAsync(string status) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.Status == status && m.OrganizationId == organizationId && !m.IsDeleted) + .OrderByDescending(m => m.RequestedOn) + .ToListAsync(); + } + + public async Task> GetMaintenanceRequestsByPriorityAsync(string priority) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.Priority == priority && m.OrganizationId == organizationId && !m.IsDeleted) + .OrderByDescending(m => m.RequestedOn) + .ToListAsync(); + } + + public async Task> GetOverdueMaintenanceRequestsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var today = DateTime.Today; + + return await _dbContext.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .Where(m => m.OrganizationId == organizationId && + !m.IsDeleted && + m.Status != "Completed" && + m.Status != "Cancelled" && + m.ScheduledOn.HasValue && + m.ScheduledOn.Value.Date < today) + .OrderBy(m => m.ScheduledOn) + .ToListAsync(); + } + + public async Task GetOpenMaintenanceRequestCountAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.MaintenanceRequests + .Where(m => m.OrganizationId == organizationId && + !m.IsDeleted && + m.Status != "Completed" && + m.Status != "Cancelled") + .CountAsync(); + } + + public async Task GetUrgentMaintenanceRequestCountAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.MaintenanceRequests + .Where(m => m.OrganizationId == organizationId && + !m.IsDeleted && + m.Priority == "Urgent" && + m.Status != "Completed" && + m.Status != "Cancelled") + .CountAsync(); + } + + public async Task GetMaintenanceRequestByIdAsync(Guid id) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _dbContext.MaintenanceRequests + .Include(m => m.Property) + .Include(m => m.Lease) + .FirstOrDefaultAsync(m => m.Id == id && m.OrganizationId == organizationId && !m.IsDeleted); + } + + public async Task AddMaintenanceRequestAsync(MaintenanceRequest maintenanceRequest) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + // Set tracking fields automatically + maintenanceRequest.Id = Guid.NewGuid(); + maintenanceRequest.OrganizationId = organizationId!.Value; + maintenanceRequest.CreatedBy = _userId; + maintenanceRequest.CreatedOn = DateTime.UtcNow; + + await _dbContext.MaintenanceRequests.AddAsync(maintenanceRequest); + await _dbContext.SaveChangesAsync(); + + // Create calendar event for the maintenance request + await _calendarEventService.CreateOrUpdateEventAsync(maintenanceRequest); + } + + public async Task UpdateMaintenanceRequestAsync(MaintenanceRequest maintenanceRequest) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify maintenance request belongs to active organization + var existing = await _dbContext.MaintenanceRequests + .FirstOrDefaultAsync(m => m.Id == maintenanceRequest.Id && m.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Maintenance request {maintenanceRequest.Id} not found in active organization."); + } + + // Set tracking fields automatically + maintenanceRequest.LastModifiedBy = _userId; + maintenanceRequest.LastModifiedOn = DateTime.UtcNow; + maintenanceRequest.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(maintenanceRequest); + await _dbContext.SaveChangesAsync(); + + // Update calendar event + await _calendarEventService.CreateOrUpdateEventAsync(maintenanceRequest); + } + + public async Task DeleteMaintenanceRequestAsync(Guid id) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var maintenanceRequest = await _dbContext.MaintenanceRequests + .FirstOrDefaultAsync(m => m.Id == id && m.OrganizationId == organizationId); + + if (maintenanceRequest != null) + { + maintenanceRequest.IsDeleted = true; + maintenanceRequest.LastModifiedOn = DateTime.Now; + maintenanceRequest.LastModifiedBy = _userId; + + _dbContext.MaintenanceRequests.Update(maintenanceRequest); + await _dbContext.SaveChangesAsync(); + + // Delete associated calendar event + await _calendarEventService.DeleteEventAsync(maintenanceRequest.CalendarEventId); + } + } + + public async Task UpdateMaintenanceRequestStatusAsync(Guid id, string status) + { + var _userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(_userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var maintenanceRequest = await _dbContext.MaintenanceRequests + .FirstOrDefaultAsync(m => m.Id == id && m.OrganizationId == organizationId && !m.IsDeleted); + + if (maintenanceRequest != null) + { + maintenanceRequest.Status = status; + maintenanceRequest.LastModifiedOn = DateTime.Now; + maintenanceRequest.LastModifiedBy = _userId; + + if (status == "Completed") + { + maintenanceRequest.CompletedOn = DateTime.Today; + } + + _dbContext.MaintenanceRequests.Update(maintenanceRequest); + await _dbContext.SaveChangesAsync(); + } + } + + #endregion + + #region Organization Settings + + /// + /// Gets the organization settings for the current user's organization. + /// If no settings exist, creates default settings. + /// + public async Task GetOrganizationSettingsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (!organizationId.HasValue || organizationId == Guid.Empty) + { + throw new InvalidOperationException("Organization ID not found for current user"); + } + + var settings = await _dbContext.OrganizationSettings + .Where(s => !s.IsDeleted && s.OrganizationId == organizationId) + .FirstOrDefaultAsync(); + + // Create default settings if they don't exist + if (settings == null) + { + var userId = await _userContext.GetUserIdAsync(); + settings = new OrganizationSettings + { + OrganizationId = organizationId.Value, // This should be set to the actual organization ID + LateFeeEnabled = true, + LateFeeAutoApply = true, + LateFeeGracePeriodDays = 3, + LateFeePercentage = 0.05m, + MaxLateFeeAmount = 50.00m, + PaymentReminderEnabled = true, + PaymentReminderDaysBefore = 3, + CreatedOn = DateTime.UtcNow, + CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId + }; + + await _dbContext.OrganizationSettings.AddAsync(settings); + await _dbContext.SaveChangesAsync(); + } + + return settings; + } + + public async Task GetOrganizationSettingsByOrgIdAsync(Guid organizationId) + { + var settings = await _dbContext.OrganizationSettings + .Where(s => !s.IsDeleted && s.OrganizationId == organizationId) + .FirstOrDefaultAsync(); + + return settings; + } + + /// + /// Updates the organization settings for the current user's organization. + /// + public async Task UpdateOrganizationSettingsAsync(OrganizationSettings settings) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue || organizationId == Guid.Empty) + { + throw new InvalidOperationException("Organization ID not found for current user"); + } + if (settings.OrganizationId != organizationId.Value) + { + throw new InvalidOperationException("Cannot update settings for a different organization"); + } + var userId = await _userContext.GetUserIdAsync(); + + settings.LastModifiedOn = DateTime.UtcNow; + settings.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + _dbContext.OrganizationSettings.Update(settings); + await _dbContext.SaveChangesAsync(); + } + + #endregion + + #region PreLeaseOperations + + #region ProspectiveTenant CRUD + + public async Task> GetAllProspectiveTenantsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.ProspectiveTenants + .Where(pt => pt.OrganizationId == organizationId && !pt.IsDeleted) + .Include(pt => pt.InterestedProperty) + .Include(pt => pt.Tours) + .Include(pt => pt.Applications) + .OrderByDescending(pt => pt.CreatedOn) + .ToListAsync(); + } + + public async Task GetProspectiveTenantByIdAsync(Guid id) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.ProspectiveTenants + .Where(pt => pt.Id == id && pt.OrganizationId == organizationId && !pt.IsDeleted) + .Include(pt => pt.InterestedProperty) + .Include(pt => pt.Tours) + .Include(pt => pt.Applications) + .FirstOrDefaultAsync(); + } + + public async Task CreateProspectiveTenantAsync(ProspectiveTenant prospectiveTenant) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync(); + + prospectiveTenant.Id = Guid.NewGuid(); + prospectiveTenant.OrganizationId = organizationId!.Value; + prospectiveTenant.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + prospectiveTenant.CreatedOn = DateTime.UtcNow; + prospectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.Lead; + prospectiveTenant.FirstContactedOn = DateTime.UtcNow; + + _dbContext.ProspectiveTenants.Add(prospectiveTenant); + await _dbContext.SaveChangesAsync(); + return prospectiveTenant; + } + + public async Task UpdateProspectiveTenantAsync(ProspectiveTenant prospectiveTenant) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify prospective tenant belongs to active organization + var existing = await _dbContext.ProspectiveTenants + .FirstOrDefaultAsync(p => p.Id == prospectiveTenant.Id && p.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Prospective tenant {prospectiveTenant.Id} not found in active organization."); + } + + // Set tracking fields automatically + prospectiveTenant.LastModifiedOn = DateTime.UtcNow; + prospectiveTenant.LastModifiedBy = userId; + prospectiveTenant.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(prospectiveTenant); + await _dbContext.SaveChangesAsync(); + return prospectiveTenant; + } + + public async Task DeleteProspectiveTenantAsync(Guid id) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync(); + var prospectiveTenant = await GetProspectiveTenantByIdAsync(id); + + if(prospectiveTenant == null) + { + throw new InvalidOperationException("Prospective tenant not found."); + } + + if (prospectiveTenant.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User is not authorized to delete this prospective tenant."); + } + prospectiveTenant.IsDeleted = true; + prospectiveTenant.LastModifiedOn = DateTime.UtcNow; + prospectiveTenant.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + await _dbContext.SaveChangesAsync(); + } + + #endregion + + #region Tour CRUD + + public async Task> GetAllToursAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.Tours + .Where(s => s.OrganizationId == organizationId && !s.IsDeleted) + .Include(s => s.ProspectiveTenant) + .Include(s => s.Property) + .Include(s => s.Checklist) + .OrderBy(s => s.ScheduledOn) + .ToListAsync(); + } + + public async Task> GetToursByProspectiveIdAsync(Guid prospectiveTenantId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.Tours + .Where(s => s.ProspectiveTenantId == prospectiveTenantId && s.OrganizationId == organizationId && !s.IsDeleted) + .Include(s => s.ProspectiveTenant) + .Include(s => s.Property) + .Include(s => s.Checklist) + .OrderBy(s => s.ScheduledOn) + .ToListAsync(); + } + + public async Task GetTourByIdAsync(Guid id) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.Tours + .Where(s => s.Id == id && s.OrganizationId == organizationId && !s.IsDeleted) + .Include(s => s.ProspectiveTenant) + .Include(s => s.Property) + .Include(s => s.Checklist) + .FirstOrDefaultAsync(); + } + + public async Task CreateTourAsync(Tour tour, Guid? templateId = null) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync(); + + tour.Id = Guid.NewGuid(); + tour.OrganizationId = organizationId!.Value; + tour.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + tour.CreatedOn = DateTime.UtcNow; + tour.Status = ApplicationConstants.TourStatuses.Scheduled; + + // Get prospect information for checklist + var prospective = await _dbContext.ProspectiveTenants + .Include(p => p.InterestedProperty) + .FirstOrDefaultAsync(p => p.Id == tour.ProspectiveTenantId); + + // Find the specified template, or fall back to default "Property Tour" template + ChecklistTemplate? tourTemplate = null; + + if (templateId.HasValue) + { + // Use the specified template + tourTemplate = await _dbContext.ChecklistTemplates + .FirstOrDefaultAsync(t => t.Id == templateId.Value && + (t.OrganizationId == tour.OrganizationId || t.IsSystemTemplate) && + !t.IsDeleted); + } + + // Fall back to default "Property Tour" template if not specified or not found + if (tourTemplate == null) + { + tourTemplate = await _dbContext.ChecklistTemplates + .FirstOrDefaultAsync(t => t.Name == "Property Tour" && + (t.OrganizationId == tour.OrganizationId || t.IsSystemTemplate) && + !t.IsDeleted); + } + + if (tourTemplate != null && prospective != null) + { + // Create checklist from template + var checklist = await _checklistService.CreateChecklistFromTemplateAsync(tourTemplate.Id); + + // Customize checklist with prospect information + checklist.Name = $"Property Tour - {prospective.FullName}"; + checklist.PropertyId = tour.PropertyId; + checklist.GeneralNotes = $"Prospect: {prospective.FullName}\n" + + $"Email: {prospective.Email}\n" + + $"Phone: {prospective.Phone}\n" + + $"Scheduled: {tour.ScheduledOn:MMM dd, yyyy h:mm tt}"; + + // Link tour to checklist + tour.ChecklistId = checklist.Id; + } + + _dbContext.Tours.Add(tour); + await _dbContext.SaveChangesAsync(); + + // Create calendar event for the tour + await _calendarEventService.CreateOrUpdateEventAsync(tour); + + // Update ProspectiveTenant status + if (prospective != null && prospective.Status == ApplicationConstants.ProspectiveStatuses.Lead) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.TourScheduled; + prospective.LastModifiedOn = DateTime.UtcNow; + await _dbContext.SaveChangesAsync(); + } + + return tour; + } + + public async Task UpdateTourAsync(Tour tour) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify tour belongs to active organization + var existing = await _dbContext.Tours + .FirstOrDefaultAsync(t => t.Id == tour.Id && t.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Tour {tour.Id} not found in active organization."); + } + + // Set tracking fields automatically + tour.LastModifiedBy = userId; + tour.LastModifiedOn = DateTime.UtcNow; + tour.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(tour); + await _dbContext.SaveChangesAsync(); + + // Update calendar event + await _calendarEventService.CreateOrUpdateEventAsync(tour); + + return tour; + } + + public async Task DeleteTourAsync(Guid id) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if(string.IsNullOrEmpty(userId) || !organizationId.HasValue || organizationId == Guid.Empty) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + + var tour = await GetTourByIdAsync(id); + + if(tour == null) + { + throw new InvalidOperationException("Tour not found."); + } + + if (tour.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User is not authorized to delete this tour."); + } + + tour.IsDeleted = true; + tour.LastModifiedOn = DateTime.UtcNow; + tour.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + await _dbContext.SaveChangesAsync(); + + // Delete associated calendar event + await _calendarEventService.DeleteEventAsync(tour.CalendarEventId); + } + + public async Task CancelTourAsync(Guid tourId) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var tour = await GetTourByIdAsync(tourId); + + if(tour == null) + { + throw new InvalidOperationException("Tour not found."); + } + + if(string.IsNullOrEmpty(userId) || !organizationId.HasValue || tour.OrganizationId != organizationId.Value) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + // Update tour status to cancelled + tour.Status = ApplicationConstants.TourStatuses.Cancelled; + tour.LastModifiedOn = DateTime.UtcNow; + tour.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + await _dbContext.SaveChangesAsync(); + + // Update calendar event status + await _calendarEventService.CreateOrUpdateEventAsync(tour); + + // Check if prospect has any other scheduled tours + var prospective = await _dbContext.ProspectiveTenants.FindAsync(tour.ProspectiveTenantId); + if (prospective != null && prospective.Status == ApplicationConstants.ProspectiveStatuses.TourScheduled) + { + var hasOtherScheduledTours = await _dbContext.Tours + .AnyAsync(s => s.ProspectiveTenantId == tour.ProspectiveTenantId + && s.Id != tourId + && !s.IsDeleted + && s.Status == ApplicationConstants.TourStatuses.Scheduled); + + // If no other scheduled tours, revert prospect status to Lead + if (!hasOtherScheduledTours) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Lead; + prospective.LastModifiedOn = DateTime.UtcNow; + prospective.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + await _dbContext.SaveChangesAsync(); + } + } + + return true; + } + + public async Task CompleteTourAsync(Guid tourId, string? feedback = null, string? interestLevel = null) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var tour = await GetTourByIdAsync(tourId); + if (tour == null) return false; + + if(string.IsNullOrEmpty(userId) || !organizationId.HasValue || tour.OrganizationId != organizationId.Value) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + // Update tour status and feedback + tour.Status = ApplicationConstants.TourStatuses.Completed; + tour.Feedback = feedback; + tour.InterestLevel = interestLevel; + tour.ConductedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + tour.LastModifiedOn = DateTime.UtcNow; + tour.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + + // Update calendar event status + if (tour.CalendarEventId.HasValue) + { + var calendarEvent = await _dbContext.CalendarEvents + .FirstOrDefaultAsync(e => e.Id == tour.CalendarEventId.Value); + if (calendarEvent != null) + { + calendarEvent.Status = ApplicationConstants.TourStatuses.Completed; + calendarEvent.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + calendarEvent.LastModifiedOn = DateTime.UtcNow; + } + } + + await _dbContext.SaveChangesAsync(); + + return true; + } + + public async Task MarkTourAsNoShowAsync(Guid tourId) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var tour = await GetTourByIdAsync(tourId); + if (tour == null) return false; + + if(string.IsNullOrEmpty(userId) || !organizationId.HasValue || tour.OrganizationId != organizationId.Value) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + // Update tour status to NoShow + tour.Status = ApplicationConstants.TourStatuses.NoShow; + tour.LastModifiedOn = DateTime.UtcNow; + tour.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + // Update calendar event status + if (tour.CalendarEventId.HasValue) + { + var calendarEvent = await _dbContext.CalendarEvents + .FirstOrDefaultAsync(e => e.Id == tour.CalendarEventId.Value); + if (calendarEvent != null) + { + calendarEvent.Status = ApplicationConstants.TourStatuses.NoShow; + calendarEvent.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + calendarEvent.LastModifiedOn = DateTime.UtcNow; + } + } + + await _dbContext.SaveChangesAsync(); + return true; + } + + #endregion + + #region RentalApplication CRUD + + public async Task> GetAllRentalApplicationsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.RentalApplications + .Where(ra => ra.OrganizationId == organizationId && !ra.IsDeleted) + .Include(ra => ra.ProspectiveTenant) + .Include(ra => ra.Property) + .Include(ra => ra.Screening) + .OrderByDescending(ra => ra.AppliedOn) + .ToListAsync(); + } + + public async Task GetRentalApplicationByIdAsync(Guid id) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.RentalApplications + .Where(ra => ra.Id == id && ra.OrganizationId == organizationId && !ra.IsDeleted) + .Include(ra => ra.ProspectiveTenant) + .Include(ra => ra.Property) + .Include(ra => ra.Screening) + .FirstOrDefaultAsync(); + } + + public async Task GetApplicationByProspectiveIdAsync(Guid prospectiveTenantId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.RentalApplications + .Where(ra => ra.ProspectiveTenantId == prospectiveTenantId && ra.OrganizationId == organizationId && !ra.IsDeleted) + .Include(ra => ra.Property) + .Include(ra => ra.Screening) + .FirstOrDefaultAsync(); + } + + public async Task CreateRentalApplicationAsync(RentalApplication application) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync(); + + application.Id = Guid.NewGuid(); + application.OrganizationId = organizationId!.Value; + application.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + application.CreatedOn = DateTime.UtcNow; + application.AppliedOn = DateTime.UtcNow; + application.Status = ApplicationConstants.ApplicationStatuses.Submitted; + + // Get organization settings for fee and expiration defaults + var orgSettings = await _dbContext.OrganizationSettings + .FirstOrDefaultAsync(s => s.OrganizationId == application.OrganizationId && !s.IsDeleted); + + if (orgSettings != null) + { + // Set application fee if not already set and fees are enabled + if (orgSettings.ApplicationFeeEnabled && application.ApplicationFee == 0) + { + application.ApplicationFee = orgSettings.DefaultApplicationFee; + } + + // Set expiration date if not already set + if (application.ExpiresOn == null) + { + application.ExpiresOn = application.AppliedOn.AddDays(orgSettings.ApplicationExpirationDays); + } + } + else + { + // Fallback defaults if no settings found + if (application.ApplicationFee == 0) + { + application.ApplicationFee = 50.00m; // Default fee + } + if (application.ExpiresOn == null) + { + application.ExpiresOn = application.AppliedOn.AddDays(30); // Default 30 days + } + } + + _dbContext.RentalApplications.Add(application); + await _dbContext.SaveChangesAsync(); + + // Update property status to ApplicationPending + var property = await _dbContext.Properties.FindAsync(application.PropertyId); + if (property != null && property.Status == ApplicationConstants.PropertyStatuses.Available) + { + property.Status = ApplicationConstants.PropertyStatuses.ApplicationPending; + property.LastModifiedOn = DateTime.UtcNow; + property.LastModifiedBy = application.CreatedBy; + await _dbContext.SaveChangesAsync(); + } + + // Update ProspectiveTenant status + var prospective = await _dbContext.ProspectiveTenants.FindAsync(application.ProspectiveTenantId); + if (prospective != null) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Applied; + prospective.LastModifiedOn = DateTime.UtcNow; + await _dbContext.SaveChangesAsync(); + } + + return application; + } + + public async Task UpdateRentalApplicationAsync(RentalApplication application) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify rental application belongs to active organization + var existing = await _dbContext.RentalApplications + .FirstOrDefaultAsync(r => r.Id == application.Id && r.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Rental application {application.Id} not found in active organization."); + } + + // Set tracking fields automatically + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + application.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(application); + await _dbContext.SaveChangesAsync(); + return application; + } + + public async Task DeleteRentalApplicationAsync(Guid id) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync(); + + var application = await GetRentalApplicationByIdAsync(id); + + if(application == null) + { + throw new InvalidOperationException("Rental application not found."); + } + + if (application.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User is not authorized to delete this rental application."); + } + application.IsDeleted = true; + application.LastModifiedOn = DateTime.UtcNow; + application.LastModifiedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + await _dbContext.SaveChangesAsync(); + } + + #endregion + + #region ApplicationScreening CRUD + + public async Task GetScreeningByApplicationIdAsync(Guid rentalApplicationId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.ApplicationScreenings + .Where(asc => asc.RentalApplicationId == rentalApplicationId && asc.OrganizationId == organizationId && !asc.IsDeleted) + .Include(asc => asc.RentalApplication) + .FirstOrDefaultAsync(); + } + + public async Task CreateScreeningAsync(ApplicationScreening screening) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync(); + + screening.Id = Guid.NewGuid(); + screening.OrganizationId = organizationId!.Value; + screening.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + screening.CreatedOn = DateTime.UtcNow; + screening.OverallResult = ApplicationConstants.ScreeningResults.Pending; + + _dbContext.ApplicationScreenings.Add(screening); + await _dbContext.SaveChangesAsync(); + + // Update application and prospective tenant status + var application = await _dbContext.RentalApplications.FindAsync(screening.RentalApplicationId); + if (application != null) + { + application.Status = ApplicationConstants.ApplicationStatuses.Screening; + application.LastModifiedOn = DateTime.UtcNow; + + var prospective = await _dbContext.ProspectiveTenants.FindAsync(application.ProspectiveTenantId); + if (prospective != null) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Screening; + prospective.LastModifiedOn = DateTime.UtcNow; + } + + await _dbContext.SaveChangesAsync(); + } + + return screening; + } + + public async Task UpdateScreeningAsync(ApplicationScreening screening) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify screening belongs to active organization + var existing = await _dbContext.ApplicationScreenings + .FirstOrDefaultAsync(s => s.Id == screening.Id && s.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Application screening {screening.Id} not found in active organization."); + } + + // Set tracking fields automatically + screening.LastModifiedOn = DateTime.UtcNow; + screening.LastModifiedBy = userId; + screening.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(screening); + await _dbContext.SaveChangesAsync(); + return screening; + } + + #endregion + + #region Business Logic + + public async Task ApproveApplicationAsync(Guid applicationId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync() ?? string.Empty; + + var application = await GetRentalApplicationByIdAsync(applicationId); + if (application == null) return false; + + if (application.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User is not authorized to approve this rental application."); + } + + application.Status = ApplicationConstants.ApplicationStatuses.Approved; + application.DecidedOn = DateTime.UtcNow; + application.DecisionBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + application.LastModifiedBy = userId; + + _dbContext.RentalApplications.Update(application); + + var prospective = await _dbContext.ProspectiveTenants.FindAsync(application.ProspectiveTenantId); + if (prospective != null) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Approved; + prospective.LastModifiedOn = DateTime.UtcNow; + prospective.LastModifiedBy = userId; + _dbContext.ProspectiveTenants.Update(prospective); + } + + await _dbContext.SaveChangesAsync(); + return true; + } + + public async Task DenyApplicationAsync(Guid applicationId, string reason) + { + var userId = await _userContext.GetUserIdAsync() ?? string.Empty; + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var application = await GetRentalApplicationByIdAsync(applicationId); + if (application == null) return false; + if (application.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User is not authorized to deny this rental application."); + } + application.Status = ApplicationConstants.ApplicationStatuses.Denied; + application.DecidedOn = DateTime.UtcNow; + application.DecisionBy = userId; + application.DenialReason = reason; + application.LastModifiedOn = DateTime.UtcNow; + application.LastModifiedBy = userId; + + var prospective = await _dbContext.ProspectiveTenants.FindAsync(application.ProspectiveTenantId); + if (prospective != null) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Denied; + prospective.LastModifiedOn = DateTime.UtcNow; + prospective.LastModifiedBy = userId; + } + + await _dbContext.SaveChangesAsync(); + return true; + } + + public async Task WithdrawApplicationAsync(Guid applicationId, string? reason = null) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync() ?? string.Empty; + + var application = await GetRentalApplicationByIdAsync(applicationId); + if (application == null) return false; + + if (application.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User is not authorized to withdraw this rental application."); + } + + application.Status = ApplicationConstants.ApplicationStatuses.Withdrawn; + application.DecidedOn = DateTime.UtcNow; + application.DecisionBy = userId; + application.DenialReason = reason; // Reusing this field for withdrawal reason + application.LastModifiedOn = DateTime.UtcNow; + application.LastModifiedBy = userId; + + var prospective = await _dbContext.ProspectiveTenants.FindAsync(application.ProspectiveTenantId); + + + if (prospective != null) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Withdrawn; + prospective.LastModifiedOn = DateTime.UtcNow; + prospective.LastModifiedBy = userId; + } + + // If there's a lease offer, mark it as withdrawn too + var leaseOffer = await GetLeaseOfferByApplicationIdAsync(applicationId); + if (leaseOffer != null) + { + leaseOffer.Status = "Withdrawn"; + leaseOffer.RespondedOn = DateTime.UtcNow; + leaseOffer.ResponseNotes = reason ?? "Application withdrawn"; + leaseOffer.LastModifiedOn = DateTime.UtcNow; + leaseOffer.LastModifiedBy = userId; + } + + // Update property status back to available if it was in lease pending + var property = await _dbContext.Properties.FindAsync(application.PropertyId); + if (property != null && property.Status == ApplicationConstants.PropertyStatuses.LeasePending) + { + property.Status = ApplicationConstants.PropertyStatuses.Available; + property.LastModifiedOn = DateTime.UtcNow; + property.LastModifiedBy = userId; + } + + await _dbContext.SaveChangesAsync(); + return true; + } + + public async Task> GetProspectivesByStatusAsync(string status) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.ProspectiveTenants + .Where(pt => pt.Status == status && pt.OrganizationId == organizationId && !pt.IsDeleted) + .Include(pt => pt.InterestedProperty) + .OrderByDescending(pt => pt.CreatedOn) + .ToListAsync(); + } + + public async Task> GetUpcomingToursAsync(int days = 7) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var startDate = DateTime.UtcNow; + var endDate = startDate.AddDays(days); + + return await _dbContext.Tours + .Where(s => s.OrganizationId == organizationId + && !s.IsDeleted + && s.Status == ApplicationConstants.TourStatuses.Scheduled + && s.ScheduledOn >= startDate + && s.ScheduledOn <= endDate) + .Include(s => s.ProspectiveTenant) + .Include(s => s.Property) + .Include(s => s.Checklist) + .OrderBy(s => s.ScheduledOn) + .ToListAsync(); + } + + public async Task> GetPendingApplicationsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.RentalApplications + .Where(ra => ra.OrganizationId == organizationId + && !ra.IsDeleted + && (ra.Status == ApplicationConstants.ApplicationStatuses.Submitted + || ra.Status == ApplicationConstants.ApplicationStatuses.UnderReview + || ra.Status == ApplicationConstants.ApplicationStatuses.Screening)) + .Include(ra => ra.ProspectiveTenant) + .Include(ra => ra.Property) + .Include(ra => ra.Screening) + .OrderBy(ra => ra.AppliedOn) + .ToListAsync(); + } + + #endregion + + #region Lease Offers + + public async Task CreateLeaseOfferAsync(LeaseOffer leaseOffer) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var userId = await _userContext.GetUserIdAsync(); + + leaseOffer.Id = Guid.NewGuid(); + leaseOffer.OrganizationId = organizationId!.Value; + leaseOffer.CreatedBy = string.IsNullOrEmpty(userId) ? string.Empty : userId; + leaseOffer.CreatedOn = DateTime.UtcNow; + _dbContext.LeaseOffers.Add(leaseOffer); + await _dbContext.SaveChangesAsync(); + return leaseOffer; + } + + public async Task GetLeaseOfferByIdAsync(Guid leaseOfferId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.LeaseOffers + .Include(lo => lo.RentalApplication) + .Include(lo => lo.Property) + .Include(lo => lo.ProspectiveTenant) + .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && lo.OrganizationId == organizationId && !lo.IsDeleted); + } + + public async Task GetLeaseOfferByApplicationIdAsync(Guid applicationId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.LeaseOffers + .Include(lo => lo.RentalApplication) + .Include(lo => lo.Property) + .Include(lo => lo.ProspectiveTenant) + .FirstOrDefaultAsync(lo => lo.RentalApplicationId == applicationId && lo.OrganizationId == organizationId && !lo.IsDeleted); + } + + public async Task> GetLeaseOffersByPropertyIdAsync(Guid propertyId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + return await _dbContext.LeaseOffers + .Include(lo => lo.RentalApplication) + .Include(lo => lo.Property) + .Include(lo => lo.ProspectiveTenant) + .Where(lo => lo.PropertyId == propertyId && lo.OrganizationId == organizationId && !lo.IsDeleted) + .OrderByDescending(lo => lo.OfferedOn) + .ToListAsync(); + } + + public async Task UpdateLeaseOfferAsync(LeaseOffer leaseOffer) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify lease offer belongs to active organization + var existing = await _dbContext.LeaseOffers + .FirstOrDefaultAsync(l => l.Id == leaseOffer.Id && l.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Lease offer {leaseOffer.Id} not found in active organization."); + } + + // Set tracking fields automatically + leaseOffer.LastModifiedBy = userId; + leaseOffer.LastModifiedOn = DateTime.UtcNow; + leaseOffer.OrganizationId = organizationId!.Value; // Prevent org hijacking + + _dbContext.Entry(existing).CurrentValues.SetValues(leaseOffer); + await _dbContext.SaveChangesAsync(); + return leaseOffer; + } + + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Application/Services/PropertyService.cs b/Aquiis.Professional/Application/Services/PropertyService.cs new file mode 100644 index 0000000..c9132ad --- /dev/null +++ b/Aquiis.Professional/Application/Services/PropertyService.cs @@ -0,0 +1,382 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing Property entities. + /// Inherits common CRUD operations from BaseService and adds property-specific business logic. + /// + public class PropertyService : BaseService + { + private readonly CalendarEventService _calendarEventService; + private readonly ApplicationSettings _appSettings; + + public PropertyService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings, + CalendarEventService calendarEventService) + : base(context, logger, userContext, settings) + { + _calendarEventService = calendarEventService; + _appSettings = settings.Value; + } + + #region Overrides with Property-Specific Logic + + /// + /// Creates a new property with initial routine inspection scheduling. + /// + public override async Task CreateAsync(Property property) + { + // Set initial routine inspection due date to 30 days from creation + property.NextRoutineInspectionDueDate = DateTime.Today.AddDays(30); + + // Call base create (handles audit fields, org assignment, validation) + var createdProperty = await base.CreateAsync(property); + + // Create calendar event for the first routine inspection + await CreateRoutineInspectionCalendarEventAsync(createdProperty); + + return createdProperty; + } + + /// + /// Retrieves a property by ID with related entities (Leases, Documents). + /// + public async Task GetPropertyWithRelationsAsync(Guid propertyId) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Properties + .Include(p => p.Leases) + .Include(p => p.Documents) + .FirstOrDefaultAsync(p => p.Id == propertyId && + p.OrganizationId == organizationId && + !p.IsDeleted); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPropertyWithRelations"); + throw; + } + } + + /// + /// Retrieves all properties with related entities. + /// + public async Task> GetPropertiesWithRelationsAsync() + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Properties + .Include(p => p.Leases) + .Include(p => p.Documents) + .Where(p => !p.IsDeleted && p.OrganizationId == organizationId) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPropertiesWithRelations"); + throw; + } + } + + /// + /// Validates property data before create/update operations. + /// + protected override async Task ValidateEntityAsync(Property property) + { + // Validate required address + if (string.IsNullOrWhiteSpace(property.Address)) + { + throw new ValidationException("Property address is required."); + } + + // Check for duplicate address in same organization + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var exists = await _context.Properties + .AnyAsync(p => p.Address == property.Address && + p.City == property.City && + p.State == property.State && + p.ZipCode == property.ZipCode && + p.Id != property.Id && + p.OrganizationId == organizationId && + !p.IsDeleted); + + if (exists) + { + throw new ValidationException($"A property with address '{property.Address}' already exists in this location."); + } + + await base.ValidateEntityAsync(property); + } + + #endregion + + #region Business Logic Methods + + /// + /// Searches properties by address, city, state, or zip code. + /// + public async Task> SearchPropertiesByAddressAsync(string searchTerm) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrWhiteSpace(searchTerm)) + { + return await _context.Properties + .Where(p => !p.IsDeleted && p.OrganizationId == organizationId) + .OrderBy(p => p.Address) + .Take(20) + .ToListAsync(); + } + + return await _context.Properties + .Where(p => !p.IsDeleted && + p.OrganizationId == organizationId && + (p.Address.Contains(searchTerm) || + p.City.Contains(searchTerm) || + p.State.Contains(searchTerm) || + p.ZipCode.Contains(searchTerm))) + .OrderBy(p => p.Address) + .Take(20) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "SearchPropertiesByAddress"); + throw; + } + } + + /// + /// Retrieves all vacant properties (no active leases). + /// + public async Task> GetVacantPropertiesAsync() + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Properties + .Where(p => !p.IsDeleted && + p.IsAvailable && + p.OrganizationId == organizationId) + .Where(p => !_context.Leases.Any(l => + l.PropertyId == p.Id && + l.Status == Core.Constants.ApplicationConstants.LeaseStatuses.Active && + !l.IsDeleted)) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetVacantProperties"); + throw; + } + } + + /// + /// Calculates the overall occupancy rate for the organization. + /// + public async Task CalculateOccupancyRateAsync() + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var totalProperties = await _context.Properties + .CountAsync(p => !p.IsDeleted && p.IsAvailable && p.OrganizationId == organizationId); + + if (totalProperties == 0) + { + return 0; + } + + var occupiedProperties = await _context.Properties + .CountAsync(p => !p.IsDeleted && + p.IsAvailable && + p.OrganizationId == organizationId && + _context.Leases.Any(l => + l.PropertyId == p.Id && + l.Status == Core.Constants.ApplicationConstants.LeaseStatuses.Active && + !l.IsDeleted)); + + return (decimal)occupiedProperties / totalProperties * 100; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "CalculateOccupancyRate"); + throw; + } + } + + /// + /// Retrieves properties that need routine inspection. + /// + public async Task> GetPropertiesDueForInspectionAsync(int daysAhead = 7) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var cutoffDate = DateTime.Today.AddDays(daysAhead); + + return await _context.Properties + .Where(p => !p.IsDeleted && + p.OrganizationId == organizationId && + p.NextRoutineInspectionDueDate.HasValue && + p.NextRoutineInspectionDueDate.Value <= cutoffDate) + .OrderBy(p => p.NextRoutineInspectionDueDate) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPropertiesDueForInspection"); + throw; + } + } + + #endregion + + #region Helper Methods + + /// + /// Creates a calendar event for routine property inspection. + /// + private async Task CreateRoutineInspectionCalendarEventAsync(Property property) + { + if (!property.NextRoutineInspectionDueDate.HasValue) + { + return; + } + + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var calendarEvent = new CalendarEvent + { + Id = Guid.NewGuid(), + Title = $"Routine Inspection - {property.Address}", + Description = $"Scheduled routine inspection for property at {property.Address}", + StartOn = property.NextRoutineInspectionDueDate.Value, + EndOn = property.NextRoutineInspectionDueDate.Value.AddHours(1), + DurationMinutes = 60, + Location = property.Address, + SourceEntityType = nameof(Property), + SourceEntityId = property.Id, + PropertyId = property.Id, + OrganizationId = organizationId!.Value, + CreatedBy = userId!, + CreatedOn = DateTime.UtcNow, + EventType = "Inspection", + Status = "Scheduled" + }; + + await _calendarEventService.CreateCustomEventAsync(calendarEvent); + } + + /// + /// Gets properties with overdue routine inspections. + /// + public async Task> GetPropertiesWithOverdueInspectionsAsync() + { + try + { + var organizationId = await _userContext.GetOrganizationIdAsync(); + + return await _context.Properties + .Where(p => p.OrganizationId == organizationId && + !p.IsDeleted && + p.NextRoutineInspectionDueDate.HasValue && + p.NextRoutineInspectionDueDate.Value < DateTime.Today) + .OrderBy(p => p.NextRoutineInspectionDueDate) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPropertiesWithOverdueInspections"); + throw; + } + } + + /// + /// Gets properties with inspections due within specified days. + /// + public async Task> GetPropertiesWithInspectionsDueSoonAsync(int daysAhead = 30) + { + try + { + var organizationId = await _userContext.GetOrganizationIdAsync(); + var dueDate = DateTime.Today.AddDays(daysAhead); + + return await _context.Properties + .Where(p => p.OrganizationId == organizationId && + !p.IsDeleted && + p.NextRoutineInspectionDueDate.HasValue && + p.NextRoutineInspectionDueDate.Value >= DateTime.Today && + p.NextRoutineInspectionDueDate.Value <= dueDate) + .OrderBy(p => p.NextRoutineInspectionDueDate) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPropertiesWithInspectionsDueSoon"); + throw; + } + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/ProspectiveTenantService.cs b/Aquiis.Professional/Application/Services/ProspectiveTenantService.cs new file mode 100644 index 0000000..5a6e93f --- /dev/null +++ b/Aquiis.Professional/Application/Services/ProspectiveTenantService.cs @@ -0,0 +1,218 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing ProspectiveTenant entities. + /// Inherits common CRUD operations from BaseService and adds prospective tenant-specific business logic. + /// + public class ProspectiveTenantService : BaseService + { + public ProspectiveTenantService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings) + : base(context, logger, userContext, settings) + { + } + + #region Overrides with ProspectiveTenant-Specific Logic + + /// + /// Validates a prospective tenant entity before create/update operations. + /// + protected override async Task ValidateEntityAsync(ProspectiveTenant entity) + { + var errors = new List(); + + // Required field validation + if (string.IsNullOrWhiteSpace(entity.FirstName)) + { + errors.Add("FirstName is required"); + } + + if (string.IsNullOrWhiteSpace(entity.LastName)) + { + errors.Add("LastName is required"); + } + + if (string.IsNullOrWhiteSpace(entity.Email) && string.IsNullOrWhiteSpace(entity.Phone)) + { + errors.Add("Either Email or Phone is required"); + } + + // Email format validation + if (!string.IsNullOrWhiteSpace(entity.Email) && !entity.Email.Contains("@")) + { + errors.Add("Email must be a valid email address"); + } + + if (errors.Any()) + { + throw new ValidationException(string.Join("; ", errors)); + } + + await base.ValidateEntityAsync(entity); + } + + /// + /// Sets default values for create operations. + /// + protected override async Task SetCreateDefaultsAsync(ProspectiveTenant entity) + { + entity = await base.SetCreateDefaultsAsync(entity); + + // Set default status if not already set + if (string.IsNullOrWhiteSpace(entity.Status)) + { + entity.Status = ApplicationConstants.ProspectiveStatuses.Lead; + } + + // Set first contacted date if not already set + if (entity.FirstContactedOn == DateTime.MinValue) + { + entity.FirstContactedOn = DateTime.UtcNow; + } + + return entity; + } + + #endregion + + #region Retrieval Methods + + /// + /// Gets a prospective tenant with all related entities. + /// + public async Task GetProspectiveTenantWithRelationsAsync(Guid prospectiveTenantId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.ProspectiveTenants + .Include(pt => pt.InterestedProperty) + .Include(pt => pt.Tours) + .Include(pt => pt.Applications) + .FirstOrDefaultAsync(pt => pt.Id == prospectiveTenantId + && !pt.IsDeleted + && pt.OrganizationId == organizationId); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetProspectiveTenantWithRelations"); + throw; + } + } + + /// + /// Gets all prospective tenants with related entities. + /// + public async Task> GetProspectiveTenantsWithRelationsAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.ProspectiveTenants + .Include(pt => pt.InterestedProperty) + .Include(pt => pt.Tours) + .Include(pt => pt.Applications) + .Where(pt => !pt.IsDeleted && pt.OrganizationId == organizationId) + .OrderByDescending(pt => pt.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetProspectiveTenantsWithRelations"); + throw; + } + } + + #endregion + + #region Business Logic Methods + + /// + /// Gets prospective tenants by status. + /// + public async Task> GetProspectivesByStatusAsync(string status) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.ProspectiveTenants + .Where(pt => pt.Status == status + && !pt.IsDeleted + && pt.OrganizationId == organizationId) + .Include(pt => pt.InterestedProperty) + .OrderByDescending(pt => pt.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetProspectivesByStatus"); + throw; + } + } + + /// + /// Gets prospective tenants interested in a specific property. + /// + public async Task> GetProspectivesByPropertyIdAsync(Guid propertyId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.ProspectiveTenants + .Where(pt => pt.InterestedPropertyId == propertyId + && !pt.IsDeleted + && pt.OrganizationId == organizationId) + .Include(pt => pt.InterestedProperty) + .Include(pt => pt.Tours) + .OrderByDescending(pt => pt.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetProspectivesByPropertyId"); + throw; + } + } + + /// + /// Updates a prospective tenant's status. + /// + public async Task UpdateStatusAsync(Guid prospectiveTenantId, string newStatus) + { + try + { + var prospect = await GetByIdAsync(prospectiveTenantId); + if (prospect == null) + { + throw new InvalidOperationException($"Prospective tenant {prospectiveTenantId} not found"); + } + + prospect.Status = newStatus; + return await UpdateAsync(prospect); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "UpdateStatus"); + throw; + } + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/RentalApplicationService.cs b/Aquiis.Professional/Application/Services/RentalApplicationService.cs new file mode 100644 index 0000000..4c54208 --- /dev/null +++ b/Aquiis.Professional/Application/Services/RentalApplicationService.cs @@ -0,0 +1,273 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing RentalApplication entities. + /// Inherits common CRUD operations from BaseService and adds rental application-specific business logic. + /// + public class RentalApplicationService : BaseService + { + public RentalApplicationService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings) + : base(context, logger, userContext, settings) + { + } + + #region Overrides with RentalApplication-Specific Logic + + /// + /// Validates a rental application entity before create/update operations. + /// + protected override async Task ValidateEntityAsync(RentalApplication entity) + { + var errors = new List(); + + // Required field validation + if (entity.ProspectiveTenantId == Guid.Empty) + { + errors.Add("ProspectiveTenantId is required"); + } + + if (entity.PropertyId == Guid.Empty) + { + errors.Add("PropertyId is required"); + } + + if (entity.ApplicationFee < 0) + { + errors.Add("ApplicationFee cannot be negative"); + } + + if (errors.Any()) + { + throw new ValidationException(string.Join("; ", errors)); + } + + await base.ValidateEntityAsync(entity); + } + + /// + /// Sets default values for create operations. + /// + protected override async Task SetCreateDefaultsAsync(RentalApplication entity) + { + entity = await base.SetCreateDefaultsAsync(entity); + + // Set default status if not already set + if (string.IsNullOrWhiteSpace(entity.Status)) + { + entity.Status = ApplicationConstants.ApplicationStatuses.Submitted; + } + + // Set applied date if not already set + if (entity.AppliedOn == DateTime.MinValue) + { + entity.AppliedOn = DateTime.UtcNow; + } + + // Get organization settings for fee and expiration defaults + var orgSettings = await _context.OrganizationSettings + .FirstOrDefaultAsync(s => s.OrganizationId == entity.OrganizationId && !s.IsDeleted); + + if (orgSettings != null) + { + // Set application fee if not already set and fees are enabled + if (orgSettings.ApplicationFeeEnabled && entity.ApplicationFee == 0) + { + entity.ApplicationFee = orgSettings.DefaultApplicationFee; + } + + // Set expiration date if not already set + if (entity.ExpiresOn == null) + { + entity.ExpiresOn = entity.AppliedOn.AddDays(orgSettings.ApplicationExpirationDays); + } + } + else + { + // Fallback defaults if no settings found + if (entity.ApplicationFee == 0) + { + entity.ApplicationFee = 50.00m; // Default fee + } + if (entity.ExpiresOn == null) + { + entity.ExpiresOn = entity.AppliedOn.AddDays(30); // Default 30 days + } + } + + return entity; + } + + /// + /// Post-create hook to update related entities. + /// + protected override async Task AfterCreateAsync(RentalApplication entity) + { + await base.AfterCreateAsync(entity); + + // Update property status to ApplicationPending + var property = await _context.Properties.FindAsync(entity.PropertyId); + if (property != null && property.Status == ApplicationConstants.PropertyStatuses.Available) + { + property.Status = ApplicationConstants.PropertyStatuses.ApplicationPending; + property.LastModifiedOn = DateTime.UtcNow; + property.LastModifiedBy = entity.CreatedBy; + await _context.SaveChangesAsync(); + } + + // Update ProspectiveTenant status + var prospective = await _context.ProspectiveTenants.FindAsync(entity.ProspectiveTenantId); + if (prospective != null) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Applied; + prospective.LastModifiedOn = DateTime.UtcNow; + await _context.SaveChangesAsync(); + } + } + + #endregion + + #region Retrieval Methods + + /// + /// Gets a rental application with all related entities. + /// + public async Task GetRentalApplicationWithRelationsAsync(Guid applicationId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.RentalApplications + .Include(ra => ra.ProspectiveTenant) + .Include(ra => ra.Property) + .Include(ra => ra.Screening) + .FirstOrDefaultAsync(ra => ra.Id == applicationId + && !ra.IsDeleted + && ra.OrganizationId == organizationId); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetRentalApplicationWithRelations"); + throw; + } + } + + /// + /// Gets all rental applications with related entities. + /// + public async Task> GetRentalApplicationsWithRelationsAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.RentalApplications + .Include(ra => ra.ProspectiveTenant) + .Include(ra => ra.Property) + .Include(ra => ra.Screening) + .Where(ra => !ra.IsDeleted && ra.OrganizationId == organizationId) + .OrderByDescending(ra => ra.AppliedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetRentalApplicationsWithRelations"); + throw; + } + } + + #endregion + + #region Business Logic Methods + + /// + /// Gets rental application by prospective tenant ID. + /// + public async Task GetApplicationByProspectiveIdAsync(Guid prospectiveTenantId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.RentalApplications + .Include(ra => ra.Property) + .Include(ra => ra.Screening) + .FirstOrDefaultAsync(ra => ra.ProspectiveTenantId == prospectiveTenantId + && !ra.IsDeleted + && ra.OrganizationId == organizationId); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetApplicationByProspectiveId"); + throw; + } + } + + /// + /// Gets pending rental applications. + /// + public async Task> GetPendingApplicationsAsync() + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.RentalApplications + .Include(ra => ra.ProspectiveTenant) + .Include(ra => ra.Property) + .Include(ra => ra.Screening) + .Where(ra => !ra.IsDeleted + && ra.OrganizationId == organizationId + && (ra.Status == ApplicationConstants.ApplicationStatuses.Submitted + || ra.Status == ApplicationConstants.ApplicationStatuses.Screening)) + .OrderByDescending(ra => ra.AppliedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetPendingApplications"); + throw; + } + } + + /// + /// Gets rental applications by property ID. + /// + public async Task> GetApplicationsByPropertyIdAsync(Guid propertyId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.RentalApplications + .Include(ra => ra.ProspectiveTenant) + .Include(ra => ra.Screening) + .Where(ra => ra.PropertyId == propertyId + && !ra.IsDeleted + && ra.OrganizationId == organizationId) + .OrderByDescending(ra => ra.AppliedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetApplicationsByPropertyId"); + throw; + } + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/SMSSettingsService.cs b/Aquiis.Professional/Application/Services/SMSSettingsService.cs new file mode 100644 index 0000000..9bf37a4 --- /dev/null +++ b/Aquiis.Professional/Application/Services/SMSSettingsService.cs @@ -0,0 +1,127 @@ +using System; +using System.Threading.Tasks; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Infrastructure.Services; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + public class SMSSettingsService : BaseService + { + private readonly TwilioSMSService _smsService; + + public SMSSettingsService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings, + TwilioSMSService smsService) + : base(context, logger, userContext, settings) + { + _smsService = smsService; + } + + public async Task GetOrCreateSettingsAsync() + { + var orgId = await _userContext.GetActiveOrganizationIdAsync(); + if (orgId == null) + { + throw new UnauthorizedAccessException("No active organization"); + } + + var settings = await _dbSet + .FirstOrDefaultAsync(s => s.OrganizationId == orgId && !s.IsDeleted); + + if (settings == null) + { + settings = new OrganizationSMSSettings + { + Id = Guid.NewGuid(), + OrganizationId = orgId.Value, + IsSMSEnabled = false, + CostPerSMS = 0.0075m, // Approximate US cost + CreatedBy = await _userContext.GetUserIdAsync() ?? string.Empty, + CreatedOn = DateTime.UtcNow + }; + await CreateAsync(settings); + } + + return settings; + } + + public async Task UpdateTwilioConfigAsync( + string accountSid, + string authToken, + string phoneNumber) + { + // Verify credentials work before saving + if (!await _smsService.VerifyTwilioCredentialsAsync(accountSid, authToken, phoneNumber)) + { + return OperationResult.FailureResult( + "Invalid Twilio credentials or phone number. Please verify your Account SID, Auth Token, and phone number."); + } + + var settings = await GetOrCreateSettingsAsync(); + + settings.TwilioAccountSidEncrypted = _smsService.EncryptAccountSid(accountSid); + settings.TwilioAuthTokenEncrypted = _smsService.EncryptAuthToken(authToken); + settings.TwilioPhoneNumber = phoneNumber; + settings.IsSMSEnabled = true; + settings.IsVerified = true; + settings.LastVerifiedOn = DateTime.UtcNow; + settings.LastError = null; + + await UpdateAsync(settings); + + return OperationResult.SuccessResult("Twilio configuration saved successfully"); + } + + public async Task DisableSMSAsync() + { + var settings = await GetOrCreateSettingsAsync(); + settings.IsSMSEnabled = false; + await UpdateAsync(settings); + + return OperationResult.SuccessResult("SMS notifications disabled"); + } + + public async Task EnableSMSAsync() + { + var settings = await GetOrCreateSettingsAsync(); + + if (string.IsNullOrEmpty(settings.TwilioAccountSidEncrypted)) + { + return OperationResult.FailureResult( + "Twilio credentials not configured. Please configure Twilio first."); + } + + settings.IsSMSEnabled = true; + await UpdateAsync(settings); + + return OperationResult.SuccessResult("SMS notifications enabled"); + } + + public async Task TestSMSConfigurationAsync(string testPhoneNumber) + { + try + { + await _smsService.SendSMSAsync( + testPhoneNumber, + "Aquiis SMS Configuration Test: This message confirms your Twilio integration is working correctly."); + + return OperationResult.SuccessResult("Test SMS sent successfully! Check your phone."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Test SMS failed"); + return OperationResult.FailureResult($"Failed to send test SMS: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Application/Services/ScheduledTaskService.cs b/Aquiis.Professional/Application/Services/ScheduledTaskService.cs new file mode 100644 index 0000000..8eb883d --- /dev/null +++ b/Aquiis.Professional/Application/Services/ScheduledTaskService.cs @@ -0,0 +1,802 @@ +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Shared.Services; +using Aquiis.Professional.Application.Services.Workflows; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Professional.Application.Services +{ + public class ScheduledTaskService : BackgroundService + { + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private Timer? _timer; + private Timer? _dailyTimer; + private Timer? _hourlyTimer; + + public ScheduledTaskService( + ILogger logger, + IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Scheduled Task Service is starting."); + + // Run immediately on startup + await DoWork(stoppingToken); + + // Then run daily at 2 AM + _timer = new Timer( + async _ => await DoWork(stoppingToken), + null, + TimeSpan.FromMinutes(GetMinutesUntil2AM()), + TimeSpan.FromHours(24)); + + await Task.CompletedTask; + + // Calculate time until next midnight for daily tasks + var now = DateTime.Now; + var nextMidnight = now.Date.AddDays(1); + var timeUntilMidnight = nextMidnight - now; + + // Start daily timer (executes at midnight) + _dailyTimer = new Timer( + async _ => await ExecuteDailyTasks(), + null, + timeUntilMidnight, + TimeSpan.FromDays(1)); + + // Start hourly timer (executes every hour) + _hourlyTimer = new Timer( + async _ => await ExecuteHourlyTasks(), + null, + TimeSpan.Zero, // Start immediately + TimeSpan.FromHours(1)); + + _logger.LogInformation("Scheduled Task Service started. Daily tasks will run at midnight, hourly tasks every hour."); + + // Keep the service running + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + } + } + + private async Task DoWork(CancellationToken stoppingToken) + { + try + { + _logger.LogInformation("Running scheduled tasks at {time}", DateTimeOffset.Now); + + using (var scope = _serviceProvider.CreateScope()) + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + var toastService = scope.ServiceProvider.GetRequiredService(); + var organizationService = scope.ServiceProvider.GetRequiredService(); + + // Get all distinct organization IDs from OrganizationSettings + var organizations = await dbContext.OrganizationSettings + .Where(s => !s.IsDeleted) + .Select(s => s.OrganizationId) + .Distinct() + .ToListAsync(stoppingToken); + + foreach (var organizationId in organizations) + { + // Get settings for this organization + var settings = await organizationService.GetOrganizationSettingsByOrgIdAsync(organizationId); + + if (settings == null) + { + _logger.LogWarning("No settings found for organization {OrganizationId}, skipping", organizationId); + continue; + } + + // Task 1: Apply late fees to overdue invoices (if enabled) + if (settings.LateFeeEnabled && settings.LateFeeAutoApply) + { + await ApplyLateFees(dbContext, toastService, organizationId, settings, stoppingToken); + } + + // Task 2: Update invoice statuses + await UpdateInvoiceStatuses(dbContext, organizationId, stoppingToken); + + // Task 3: Send payment reminders (if enabled) + if (settings.PaymentReminderEnabled) + { + await SendPaymentReminders(dbContext, organizationId, settings, stoppingToken); + } + + // Task 4: Check for expiring leases and send renewal notifications + await CheckLeaseRenewals(dbContext, organizationId, stoppingToken); + + // Task 5: Expire overdue leases using workflow service (with audit logging) + var expiredLeaseCount = await ExpireOverdueLeases(scope, organizationId); + if (expiredLeaseCount > 0) + { + _logger.LogInformation( + "Expired {Count} overdue lease(s) for organization {OrganizationId}", + expiredLeaseCount, organizationId); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred executing scheduled tasks."); + } + } + + private async Task ApplyLateFees( + ApplicationDbContext dbContext, + ToastService toastService, + Guid organizationId, + OrganizationSettings settings, + CancellationToken stoppingToken) + { + try + { + var today = DateTime.Today; + + // Find overdue invoices that haven't been charged a late fee yet + var overdueInvoices = await dbContext.Invoices + .Include(i => i.Lease) + .Where(i => !i.IsDeleted && + i.OrganizationId == organizationId && + i.Status == "Pending" && + i.DueOn < today.AddDays(-settings.LateFeeGracePeriodDays) && + (i.LateFeeApplied == null || !i.LateFeeApplied.Value)) + .ToListAsync(stoppingToken); + + foreach (var invoice in overdueInvoices) + { + var lateFee = Math.Min(invoice.Amount * settings.LateFeePercentage, settings.MaxLateFeeAmount); + + invoice.LateFeeAmount = lateFee; + invoice.LateFeeApplied = true; + invoice.LateFeeAppliedOn = DateTime.UtcNow; + invoice.Amount += lateFee; + invoice.Status = "Overdue"; + invoice.LastModifiedOn = DateTime.UtcNow; + invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task + invoice.Notes = string.IsNullOrEmpty(invoice.Notes) + ? $"Late fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}" + : $"{invoice.Notes}\nLate fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}"; + + _logger.LogInformation( + "Applied late fee of {LateFee:C} to invoice {InvoiceNumber} (ID: {InvoiceId}) for organization {OrganizationId}", + lateFee, invoice.InvoiceNumber, invoice.Id, organizationId); + } + + if (overdueInvoices.Any()) + { + await dbContext.SaveChangesAsync(stoppingToken); + _logger.LogInformation("Applied late fees to {Count} invoices for organization {OrganizationId}", + overdueInvoices.Count, organizationId); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error applying late fees for organization {OrganizationId}", organizationId); + } + } + + private async Task UpdateInvoiceStatuses(ApplicationDbContext dbContext, Guid organizationId, CancellationToken stoppingToken) + { + try + { + var today = DateTime.Today; + + // Update pending invoices that are now overdue (and haven't had late fees applied) + var newlyOverdueInvoices = await dbContext.Invoices + .Where(i => !i.IsDeleted && + i.OrganizationId == organizationId && + i.Status == "Pending" && + i.DueOn < today && + (i.LateFeeApplied == null || !i.LateFeeApplied.Value)) + .ToListAsync(stoppingToken); + + foreach (var invoice in newlyOverdueInvoices) + { + invoice.Status = "Overdue"; + invoice.LastModifiedOn = DateTime.UtcNow; + invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task + } + + if (newlyOverdueInvoices.Any()) + { + await dbContext.SaveChangesAsync(stoppingToken); + _logger.LogInformation("Updated {Count} invoices to Overdue status for organization {OrganizationId}", + newlyOverdueInvoices.Count, organizationId); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating invoice statuses for organization {OrganizationId}", organizationId); + } + } + + private async Task SendPaymentReminders( + ApplicationDbContext dbContext, + Guid organizationId, + OrganizationSettings settings, + CancellationToken stoppingToken) + { + try + { + var today = DateTime.Today; + + // Find invoices due soon + var upcomingInvoices = await dbContext.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Where(i => !i.IsDeleted && + i.OrganizationId == organizationId && + i.Status == "Pending" && + i.DueOn >= today && + i.DueOn <= today.AddDays(settings.PaymentReminderDaysBefore) && + (i.ReminderSent == null || !i.ReminderSent.Value)) + .ToListAsync(stoppingToken); + + foreach (var invoice in upcomingInvoices) + { + // TODO: Integrate with email service when implemented + // For now, just log the reminder + _logger.LogInformation( + "Payment reminder needed for invoice {InvoiceNumber} due {DueDate} for tenant {TenantName} in organization {OrganizationId}", + invoice.InvoiceNumber, + invoice.DueOn.ToString("MMM dd, yyyy"), + invoice.Lease?.Tenant?.FullName ?? "Unknown", + organizationId); + + invoice.ReminderSent = true; + invoice.ReminderSentOn = DateTime.UtcNow; + invoice.LastModifiedOn = DateTime.UtcNow; + invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task + } + + if (upcomingInvoices.Any()) + { + await dbContext.SaveChangesAsync(stoppingToken); + _logger.LogInformation("Marked {Count} invoices as reminder sent for organization {OrganizationId}", + upcomingInvoices.Count, organizationId); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending payment reminders for organization {OrganizationId}", organizationId); + } + } + + private async Task CheckLeaseRenewals(ApplicationDbContext dbContext, Guid organizationId, CancellationToken stoppingToken) + { + try + { + var today = DateTime.Today; + + // Check for leases expiring in 90 days (initial notification) + var leasesExpiring90Days = await dbContext.Leases + .Include(l => l.Tenant) + .Include(l => l.Property) + .Where(l => !l.IsDeleted && + l.OrganizationId == organizationId && + l.Status == "Active" && + l.EndDate >= today.AddDays(85) && + l.EndDate <= today.AddDays(95) && + (l.RenewalNotificationSent == null || !l.RenewalNotificationSent.Value)) + .ToListAsync(stoppingToken); + + foreach (var lease in leasesExpiring90Days) + { + // TODO: Send email notification when email service is integrated + _logger.LogInformation( + "Lease expiring in 90 days: Lease ID {LeaseId}, Property: {PropertyAddress}, Tenant: {TenantName}, End Date: {EndDate}", + lease.Id, + lease.Property?.Address ?? "Unknown", + lease.Tenant?.FullName ?? "Unknown", + lease.EndDate.ToString("MMM dd, yyyy")); + + lease.RenewalNotificationSent = true; + lease.RenewalNotificationSentOn = DateTime.UtcNow; + lease.RenewalStatus = "Pending"; + lease.LastModifiedOn = DateTime.UtcNow; + lease.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task + } + + // Check for leases expiring in 60 days (reminder) + var leasesExpiring60Days = await dbContext.Leases + .Include(l => l.Tenant) + .Include(l => l.Property) + .Where(l => !l.IsDeleted && + l.OrganizationId == organizationId && + l.Status == "Active" && + l.EndDate >= today.AddDays(55) && + l.EndDate <= today.AddDays(65) && + l.RenewalNotificationSent == true && + l.RenewalReminderSentOn == null) + .ToListAsync(stoppingToken); + + foreach (var lease in leasesExpiring60Days) + { + // TODO: Send reminder email + _logger.LogInformation( + "Lease expiring in 60 days (reminder): Lease ID {LeaseId}, Property: {PropertyAddress}, Tenant: {TenantName}, End Date: {EndDate}", + lease.Id, + lease.Property?.Address ?? "Unknown", + lease.Tenant?.FullName ?? "Unknown", + lease.EndDate.ToString("MMM dd, yyyy")); + + lease.RenewalReminderSentOn = DateTime.UtcNow; + lease.LastModifiedOn = DateTime.UtcNow; + lease.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task + } + + // Check for leases expiring in 30 days (final reminder) + var leasesExpiring30Days = await dbContext.Leases + .Include(l => l.Tenant) + .Include(l => l.Property) + .Where(l => !l.IsDeleted && + l.OrganizationId == organizationId && + l.Status == "Active" && + l.EndDate >= today.AddDays(25) && + l.EndDate <= today.AddDays(35) && + l.RenewalStatus == "Pending") + .ToListAsync(stoppingToken); + + foreach (var lease in leasesExpiring30Days) + { + // TODO: Send final reminder + _logger.LogInformation( + "Lease expiring in 30 days (final reminder): Lease ID {LeaseId}, Property: {PropertyAddress}, Tenant: {TenantName}, End Date: {EndDate}", + lease.Id, + lease.Property?.Address ?? "Unknown", + lease.Tenant?.FullName ?? "Unknown", + lease.EndDate.ToString("MMM dd, yyyy")); + } + + // Note: Lease expiration is now handled by ExpireOverdueLeases() + // which uses LeaseWorkflowService for proper audit logging + + var totalUpdated = leasesExpiring90Days.Count + leasesExpiring60Days.Count + + leasesExpiring30Days.Count; + + if (totalUpdated > 0) + { + await dbContext.SaveChangesAsync(stoppingToken); + _logger.LogInformation( + "Processed {Count} lease renewal notifications for organization {OrganizationId}: {Initial} initial, {Reminder60} 60-day, {Reminder30} 30-day reminders", + totalUpdated, + organizationId, + leasesExpiring90Days.Count, + leasesExpiring60Days.Count, + leasesExpiring30Days.Count); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error checking lease renewals for organization {OrganizationId}", organizationId); + } + } + + private async Task ExecuteDailyTasks() + { + _logger.LogInformation("Executing daily tasks at {Time}", DateTime.Now); + + try + { + using var scope = _serviceProvider.CreateScope(); + var paymentService = scope.ServiceProvider.GetRequiredService(); + var propertyService = scope.ServiceProvider.GetRequiredService(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + // Calculate daily payment totals + var today = DateTime.Today; + var todayPayments = await paymentService.GetAllAsync(); + var dailyTotal = todayPayments + .Where(p => p.PaidOn.Date == today && !p.IsDeleted) + .Sum(p => p.Amount); + + _logger.LogInformation("Daily Payment Total for {Date}: ${Amount:N2}", + today.ToString("yyyy-MM-dd"), + dailyTotal); + + // Check for overdue routine inspections + var overdueInspections = await propertyService.GetPropertiesWithOverdueInspectionsAsync(); + if (overdueInspections.Any()) + { + _logger.LogWarning("{Count} propert(ies) have overdue routine inspections", + overdueInspections.Count); + + foreach (var property in overdueInspections.Take(5)) // Log first 5 + { + var daysOverdue = (DateTime.Today - property.NextRoutineInspectionDueDate!.Value).Days; + _logger.LogWarning("Property {Address} - Inspection overdue by {Days} days (Due: {DueDate})", + property.Address, + daysOverdue, + property.NextRoutineInspectionDueDate.Value.ToString("yyyy-MM-dd")); + } + } + + // Check for inspections due soon (within 30 days) + var dueSoonInspections = await propertyService.GetPropertiesWithInspectionsDueSoonAsync(30); + if (dueSoonInspections.Any()) + { + _logger.LogInformation("{Count} propert(ies) have routine inspections due within 30 days", + dueSoonInspections.Count); + } + + // Check for expired rental applications + var expiredApplicationsCount = await ExpireOldApplications(dbContext); + if (expiredApplicationsCount > 0) + { + _logger.LogInformation("Expired {Count} rental application(s) that passed their expiration date", + expiredApplicationsCount); + } + + // Check for expired lease offers (uses workflow service for audit logging) + var expiredLeaseOffersCount = await ExpireOldLeaseOffers(scope); + if (expiredLeaseOffersCount > 0) + { + _logger.LogInformation("Expired {Count} lease offer(s) that passed their expiration date", + expiredLeaseOffersCount); + } + + // Check for year-end dividend calculation (runs in first week of January) + if (today.Month == 1 && today.Day <= 7) + { + await ProcessYearEndDividends(scope, today.Year - 1); + } + + // Additional daily tasks: + // - Generate daily reports + // - Send payment reminders + // - Check for overdue invoices + // - Archive old records + // - Send summary emails to property managers + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing daily tasks"); + } + } + + private async Task ExecuteHourlyTasks() + { + _logger.LogInformation("Executing hourly tasks at {Time}", DateTime.Now); + + try + { + using var scope = _serviceProvider.CreateScope(); + var tourService = scope.ServiceProvider.GetRequiredService(); + var leaseService = scope.ServiceProvider.GetRequiredService(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + // Get all organizations + var organizations = await dbContext.OrganizationSettings + .Where(s => !s.IsDeleted) + .ToListAsync(); + + int totalMarkedNoShow = 0; + + foreach (var orgSettings in organizations) + { + var organizationId = orgSettings.OrganizationId; + var gracePeriodHours = orgSettings.TourNoShowGracePeriodHours; + + // Check for tours that should be marked as no-show + var cutoffTime = DateTime.Now.AddHours(-gracePeriodHours); + + // Query tours directly for this organization (bypass user context) + var potentialNoShowTours = await dbContext.Tours + .Where(t => t.OrganizationId == organizationId && !t.IsDeleted) + .Include(t => t.ProspectiveTenant) + .Include(t => t.Property) + .ToListAsync(); + + var noShowTours = potentialNoShowTours + .Where(t => t.Status == ApplicationConstants.TourStatuses.Scheduled && + t.ScheduledOn < cutoffTime) + .ToList(); + + foreach (var tour in noShowTours) + { + await tourService.MarkTourAsNoShowAsync(tour.Id); + totalMarkedNoShow++; + + _logger.LogInformation( + "Marked tour {TourId} as No Show - Scheduled: {ScheduledTime}, Grace period: {Hours} hours", + tour.Id, + tour.ScheduledOn.ToString("yyyy-MM-dd HH:mm"), + gracePeriodHours); + } + } + + if (totalMarkedNoShow > 0) + { + _logger.LogInformation("Marked {Count} tour(s) as No Show across all organizations", totalMarkedNoShow); + } + + // Example hourly task: Check for upcoming lease expirations + var httpContextAccessor = scope.ServiceProvider.GetRequiredService(); + var userId = httpContextAccessor.HttpContext?.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (!string.IsNullOrEmpty(userId)) + { + var upcomingLeases = await leaseService.GetAllAsync(); + var expiringIn30Days = upcomingLeases + .Where(l => l.EndDate >= DateTime.Today && + l.EndDate <= DateTime.Today.AddDays(30) && + !l.IsDeleted) + .Count(); + + if (expiringIn30Days > 0) + { + _logger.LogInformation("{Count} lease(s) expiring in the next 30 days", expiringIn30Days); + } + } + + // You can add more hourly tasks here: + // - Check for maintenance requests + // - Update lease statuses + // - Send notifications + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing hourly tasks"); + } + } + + private double GetMinutesUntil2AM() + { + var now = DateTime.Now; + var next2AM = DateTime.Today.AddDays(1).AddHours(2); + + if (now.Hour < 2) + { + next2AM = DateTime.Today.AddHours(2); + } + + return (next2AM - now).TotalMinutes; + } + + private async Task ExpireOldApplications(ApplicationDbContext dbContext) + { + try + { + // Find all applications that are expired but not yet marked as such + var expiredApplications = await dbContext.RentalApplications + .Where(a => !a.IsDeleted && + (a.Status == ApplicationConstants.ApplicationStatuses.Submitted || + a.Status == ApplicationConstants.ApplicationStatuses.UnderReview || + a.Status == ApplicationConstants.ApplicationStatuses.Screening) && + a.ExpiresOn.HasValue && + a.ExpiresOn.Value < DateTime.UtcNow) + .ToListAsync(); + + foreach (var application in expiredApplications) + { + application.Status = ApplicationConstants.ApplicationStatuses.Expired; + application.LastModifiedOn = DateTime.UtcNow; + application.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task + + _logger.LogInformation("Expired application {ApplicationId} for property {PropertyId} (Expired on: {ExpirationDate})", + application.Id, + application.PropertyId, + application.ExpiresOn!.Value.ToString("yyyy-MM-dd")); + } + + if (expiredApplications.Any()) + { + await dbContext.SaveChangesAsync(); + } + + return expiredApplications.Count; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error expiring old applications"); + return 0; + } + } + + /// + /// Expires lease offers that have passed their expiration date. + /// Uses ApplicationWorkflowService for proper audit logging. + /// + private async Task ExpireOldLeaseOffers(IServiceScope scope) + { + try + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + var workflowService = scope.ServiceProvider.GetRequiredService(); + + // Find all pending lease offers that have expired + var expiredOffers = await dbContext.LeaseOffers + .Where(lo => !lo.IsDeleted && + lo.Status == "Pending" && + lo.ExpiresOn < DateTime.UtcNow) + .ToListAsync(); + + var expiredCount = 0; + + foreach (var offer in expiredOffers) + { + try + { + var result = await workflowService.ExpireLeaseOfferAsync(offer.Id); + + if (result.Success) + { + expiredCount++; + _logger.LogInformation( + "Expired lease offer {LeaseOfferId} for property {PropertyId} (Expired on: {ExpirationDate})", + offer.Id, + offer.PropertyId, + offer.ExpiresOn.ToString("yyyy-MM-dd")); + } + else + { + _logger.LogWarning( + "Failed to expire lease offer {LeaseOfferId}: {Errors}", + offer.Id, + string.Join(", ", result.Errors)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error expiring lease offer {LeaseOfferId}", offer.Id); + } + } + + return expiredCount; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error expiring old lease offers"); + return 0; + } + } + + /// + /// Processes year-end security deposit dividend calculations. + /// Runs in the first week of January for the previous year. + /// + private async Task ProcessYearEndDividends(IServiceScope scope, int year) + { + try + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + var securityDepositService = scope.ServiceProvider.GetRequiredService(); + + // Get all organizations that have security deposit investment enabled + var organizations = await dbContext.OrganizationSettings + .Where(s => !s.IsDeleted && s.SecurityDepositInvestmentEnabled) + .Select(s => s.OrganizationId) + .Distinct() + .ToListAsync(); + + foreach (var organizationId in organizations) + { + try + { + // Check if pool exists and has performance recorded + var pool = await dbContext.SecurityDepositInvestmentPools + .FirstOrDefaultAsync(p => p.OrganizationId == organizationId && + p.Year == year && + !p.IsDeleted); + + if (pool == null) + { + _logger.LogInformation( + "No investment pool found for organization {OrganizationId} for year {Year}", + organizationId, year); + continue; + } + + if (pool.Status == "Distributed" || pool.Status == "Closed") + { + _logger.LogInformation( + "Dividends already processed for organization {OrganizationId} for year {Year}", + organizationId, year); + continue; + } + + if (pool.TotalEarnings == 0) + { + _logger.LogInformation( + "No earnings recorded for organization {OrganizationId} for year {Year}. " + + "Please record investment performance before dividend calculation.", + organizationId, year); + continue; + } + + // Calculate dividends + var dividends = await securityDepositService.CalculateDividendsAsync(year); + + if (dividends.Any()) + { + _logger.LogInformation( + "Calculated {Count} dividend(s) for organization {OrganizationId} for year {Year}. " + + "Total tenant share: ${TenantShare:N2}", + dividends.Count, + organizationId, + year, + dividends.Sum(d => d.DividendAmount)); + } + else + { + _logger.LogInformation( + "No dividends to calculate for organization {OrganizationId} for year {Year}", + organizationId, year); + } + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error processing dividends for organization {OrganizationId} for year {Year}", + organizationId, year); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing year-end dividends for year {Year}", year); + } + } + + /// + /// Expires leases that have passed their end date using LeaseWorkflowService. + /// This provides proper audit logging for lease expiration. + /// + private async Task ExpireOverdueLeases(IServiceScope scope, Guid organizationId) + { + try + { + var leaseWorkflowService = scope.ServiceProvider.GetRequiredService(); + var result = await leaseWorkflowService.ExpireOverdueLeaseAsync(); + + if (result.Success) + { + return result.Data; + } + else + { + _logger.LogWarning( + "Failed to expire overdue leases for organization {OrganizationId}: {Errors}", + organizationId, + string.Join(", ", result.Errors)); + return 0; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error expiring overdue leases for organization {OrganizationId}", organizationId); + return 0; + } + } + + public override Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Scheduled Task Service is stopping."); + _timer?.Dispose(); + _dailyTimer?.Change(Timeout.Infinite, 0); + _hourlyTimer?.Change(Timeout.Infinite, 0); + return base.StopAsync(stoppingToken); + } + + public override void Dispose() + { + _timer?.Dispose(); + _dailyTimer?.Dispose(); + _hourlyTimer?.Dispose(); + base.Dispose(); + } + } +} diff --git a/Aquiis.Professional/Application/Services/SchemaValidationService.cs b/Aquiis.Professional/Application/Services/SchemaValidationService.cs new file mode 100644 index 0000000..1863189 --- /dev/null +++ b/Aquiis.Professional/Application/Services/SchemaValidationService.cs @@ -0,0 +1,131 @@ +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Constants; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + public class SchemaValidationService + { + private readonly ApplicationDbContext _dbContext; + private readonly ApplicationSettings _settings; + private readonly ILogger _logger; + + public SchemaValidationService( + ApplicationDbContext dbContext, + IOptions settings, + ILogger logger) + { + _dbContext = dbContext; + _settings = settings.Value; + _logger = logger; + } + + /// + /// Validates that the database schema version matches the application's expected version + /// + public async Task<(bool IsValid, string Message, string? DatabaseVersion)> ValidateSchemaVersionAsync() + { + try + { + // Get the current schema version from database + var currentVersion = await _dbContext.SchemaVersions + .OrderByDescending(v => v.AppliedOn) + .FirstOrDefaultAsync(); + + if (currentVersion == null) + { + _logger.LogWarning("No schema version records found in database"); + return (false, "No schema version found. Database may be corrupted or incomplete.", null); + } + + var expectedVersion = _settings.SchemaVersion; + var dbVersion = currentVersion.Version; + + if (dbVersion != expectedVersion) + { + _logger.LogWarning("Schema version mismatch. Expected: {Expected}, Database: {Actual}", + expectedVersion, dbVersion); + return (false, + $"Schema version mismatch! Application expects v{expectedVersion} but database is v{dbVersion}. Please update the application or restore a compatible backup.", + dbVersion); + } + + _logger.LogInformation("Schema version validated successfully: {Version}", dbVersion); + return (true, $"Schema version {dbVersion} is valid", dbVersion); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error validating schema version"); + return (false, $"Error validating schema: {ex.Message}", null); + } + } + + /// + /// Updates or creates the schema version record + /// + public async Task UpdateSchemaVersionAsync(string version, string description = "") + { + try + { + _logger.LogInformation("Creating schema version record: Version={Version}, Description={Description}", version, description); + + var schemaVersion = new SchemaVersion + { + Version = version, + AppliedOn = DateTime.UtcNow, + Description = description + }; + + _dbContext.SchemaVersions.Add(schemaVersion); + _logger.LogInformation("Schema version entity added to context, saving changes..."); + + var saved = await _dbContext.SaveChangesAsync(); + _logger.LogInformation("SaveChanges completed. Rows affected: {Count}", saved); + + _logger.LogInformation("Schema version updated to {Version}", version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update schema version"); + throw; + } + } + + /// + /// Gets the current database schema version + /// + public async Task GetCurrentSchemaVersionAsync() + { + try + { + // Check if table exists first + var tableExists = await _dbContext.Database.ExecuteSqlRawAsync( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='SchemaVersions'") >= 0; + + if (!tableExists) + { + _logger.LogWarning("SchemaVersions table does not exist"); + return null; + } + + var currentVersion = await _dbContext.SchemaVersions + .OrderByDescending(v => v.AppliedOn) + .FirstOrDefaultAsync(); + + if (currentVersion == null) + { + _logger.LogInformation("SchemaVersions table exists but has no records"); + } + + return currentVersion?.Version; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting current schema version"); + return null; + } + } + } +} diff --git a/Aquiis.Professional/Application/Services/ScreeningService.cs b/Aquiis.Professional/Application/Services/ScreeningService.cs new file mode 100644 index 0000000..807fa35 --- /dev/null +++ b/Aquiis.Professional/Application/Services/ScreeningService.cs @@ -0,0 +1,237 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing ApplicationScreening entities. + /// Inherits common CRUD operations from BaseService and adds screening-specific business logic. + /// + public class ScreeningService : BaseService + { + public ScreeningService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings) + : base(context, logger, userContext, settings) + { + } + + #region Overrides with Screening-Specific Logic + + /// + /// Validates an application screening entity before create/update operations. + /// + protected override async Task ValidateEntityAsync(ApplicationScreening entity) + { + var errors = new List(); + + // Required field validation + if (entity.RentalApplicationId == Guid.Empty) + { + errors.Add("RentalApplicationId is required"); + } + + if (errors.Any()) + { + throw new ValidationException(string.Join("; ", errors)); + } + + await base.ValidateEntityAsync(entity); + } + + /// + /// Sets default values for create operations. + /// + protected override async Task SetCreateDefaultsAsync(ApplicationScreening entity) + { + entity = await base.SetCreateDefaultsAsync(entity); + + // Set default overall result if not already set + if (string.IsNullOrWhiteSpace(entity.OverallResult)) + { + entity.OverallResult = ApplicationConstants.ScreeningResults.Pending; + } + + return entity; + } + + /// + /// Post-create hook to update related application and prospective tenant status. + /// + protected override async Task AfterCreateAsync(ApplicationScreening entity) + { + await base.AfterCreateAsync(entity); + + // Update application and prospective tenant status + var application = await _context.RentalApplications.FindAsync(entity.RentalApplicationId); + if (application != null) + { + application.Status = ApplicationConstants.ApplicationStatuses.Screening; + application.LastModifiedOn = DateTime.UtcNow; + + var prospective = await _context.ProspectiveTenants.FindAsync(application.ProspectiveTenantId); + if (prospective != null) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Screening; + prospective.LastModifiedOn = DateTime.UtcNow; + } + + await _context.SaveChangesAsync(); + } + } + + #endregion + + #region Retrieval Methods + + /// + /// Gets a screening with related rental application. + /// + public async Task GetScreeningWithRelationsAsync(Guid screeningId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.ApplicationScreenings + .Include(asc => asc.RentalApplication) + .ThenInclude(ra => ra!.ProspectiveTenant) + .Include(asc => asc.RentalApplication) + .ThenInclude(ra => ra!.Property) + .FirstOrDefaultAsync(asc => asc.Id == screeningId + && !asc.IsDeleted + && asc.OrganizationId == organizationId); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetScreeningWithRelations"); + throw; + } + } + + #endregion + + #region Business Logic Methods + + /// + /// Gets screening by rental application ID. + /// + public async Task GetScreeningByApplicationIdAsync(Guid rentalApplicationId) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.ApplicationScreenings + .Include(asc => asc.RentalApplication) + .FirstOrDefaultAsync(asc => asc.RentalApplicationId == rentalApplicationId + && !asc.IsDeleted + && asc.OrganizationId == organizationId); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetScreeningByApplicationId"); + throw; + } + } + + /// + /// Gets screenings by result status. + /// + public async Task> GetScreeningsByResultAsync(string result) + { + try + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.ApplicationScreenings + .Include(asc => asc.RentalApplication) + .ThenInclude(ra => ra!.ProspectiveTenant) + .Where(asc => asc.OverallResult == result + && !asc.IsDeleted + && asc.OrganizationId == organizationId) + .OrderByDescending(asc => asc.CreatedOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetScreeningsByResult"); + throw; + } + } + + /// + /// Updates screening result and automatically updates application status. + /// + public async Task UpdateScreeningResultAsync(Guid screeningId, string result, string? notes = null) + { + try + { + var screening = await GetByIdAsync(screeningId); + if (screening == null) + { + throw new InvalidOperationException($"Screening {screeningId} not found"); + } + + screening.OverallResult = result; + if (!string.IsNullOrWhiteSpace(notes)) + { + screening.ResultNotes = notes; + } + + await UpdateAsync(screening); + + // Update application status based on screening result + var application = await _context.RentalApplications.FindAsync(screening.RentalApplicationId); + if (application != null) + { + if (result == ApplicationConstants.ScreeningResults.Passed || result == ApplicationConstants.ScreeningResults.ConditionalPass) + { + application.Status = ApplicationConstants.ApplicationStatuses.Approved; + } + else if (result == ApplicationConstants.ScreeningResults.Failed) + { + application.Status = ApplicationConstants.ApplicationStatuses.Denied; + } + + application.LastModifiedOn = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + // Update prospective tenant status + var prospective = await _context.ProspectiveTenants.FindAsync(application.ProspectiveTenantId); + if (prospective != null) + { + if (result == ApplicationConstants.ScreeningResults.Passed || result == ApplicationConstants.ScreeningResults.ConditionalPass) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Approved; + } + else if (result == ApplicationConstants.ScreeningResults.Failed) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Denied; + } + + prospective.LastModifiedOn = DateTime.UtcNow; + await _context.SaveChangesAsync(); + } + } + + return screening; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "UpdateScreeningResult"); + throw; + } + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/SecurityDepositService.cs b/Aquiis.Professional/Application/Services/SecurityDepositService.cs new file mode 100644 index 0000000..1cd6b55 --- /dev/null +++ b/Aquiis.Professional/Application/Services/SecurityDepositService.cs @@ -0,0 +1,741 @@ +using Microsoft.EntityFrameworkCore; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Shared.Services; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing security deposits, investment pool, and dividend distribution. + /// Handles the complete lifecycle from collection to refund with investment tracking. + /// + public class SecurityDepositService + { + private readonly ApplicationDbContext _context; + private readonly UserContextService _userContext; + + public SecurityDepositService(ApplicationDbContext context, UserContextService userContext) + { + _context = context; + _userContext = userContext; + } + + #region Security Deposit Management + + /// + /// Collects a security deposit for a lease. + /// + public async Task CollectSecurityDepositAsync( + Guid leaseId, + decimal amount, + string paymentMethod, + string? transactionReference, + Guid? tenantId = null) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue) + throw new InvalidOperationException("Organization context is required"); + + var lease = await _context.Leases + .Include(l => l.Tenant) + .FirstOrDefaultAsync(l => l.Id == leaseId && !l.IsDeleted); + + if (lease == null) + throw new InvalidOperationException($"Lease {leaseId} not found"); + + // Check if deposit already exists for this lease + var existingDeposit = await _context.SecurityDeposits + .FirstOrDefaultAsync(sd => sd.LeaseId == leaseId && !sd.IsDeleted); + + if (existingDeposit != null) + throw new InvalidOperationException($"Security deposit already exists for lease {leaseId}"); + + // Use provided tenantId or fall back to lease.TenantId + Guid depositTenantId; + if (tenantId.HasValue) + { + depositTenantId = tenantId.Value; + } + else if (lease.TenantId != Guid.Empty) + { + depositTenantId = lease.TenantId; + } + else + { + throw new InvalidOperationException($"Tenant ID is required to collect security deposit for lease {leaseId}"); + } + + var deposit = new SecurityDeposit + { + OrganizationId = organizationId.Value, + LeaseId = leaseId, + TenantId = depositTenantId, + Amount = amount, + DateReceived = DateTime.UtcNow, + PaymentMethod = paymentMethod, + TransactionReference = transactionReference, + Status = ApplicationConstants.SecurityDepositStatuses.Held, + InInvestmentPool = false, // Will be added when lease becomes active + CreatedBy = userId ?? string.Empty, + CreatedOn = DateTime.UtcNow + }; + + _context.SecurityDeposits.Add(deposit); + await _context.SaveChangesAsync(); + + return deposit; + } + + /// + /// Adds a security deposit to the investment pool when lease becomes active. + /// + public async Task AddToInvestmentPoolAsync(Guid securityDepositId) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify deposit belongs to active organization + var deposit = await _context.SecurityDeposits + .Include(sd => sd.Lease) + .FirstOrDefaultAsync(sd => sd.Id == securityDepositId && + sd.OrganizationId == organizationId && + !sd.IsDeleted); + + if (deposit == null) + return false; + + if (deposit.InInvestmentPool) + return true; // Already in pool + + // Set tracking fields automatically + deposit.InInvestmentPool = true; + deposit.PoolEntryDate = DateTime.UtcNow; + deposit.LastModifiedBy = userId; + deposit.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return true; + } + + /// + /// Removes a security deposit from the investment pool when lease ends. + /// + public async Task RemoveFromInvestmentPoolAsync(Guid securityDepositId) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify deposit belongs to active organization + var deposit = await _context.SecurityDeposits + .FirstOrDefaultAsync(sd => sd.Id == securityDepositId && + sd.OrganizationId == organizationId && + !sd.IsDeleted); + + if (deposit == null) + return false; + + if (!deposit.InInvestmentPool) + return true; // Already removed + + // Set tracking fields automatically + deposit.InInvestmentPool = false; + deposit.PoolExitDate = DateTime.UtcNow; + deposit.LastModifiedBy = userId; + deposit.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return true; + } + + /// + /// Gets security deposit by lease ID. + /// + public async Task GetSecurityDepositByLeaseIdAsync(Guid leaseId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.SecurityDeposits + .Include(sd => sd.Lease) + .Include(sd => sd.Tenant) + .Include(sd => sd.Dividends) + .Where(sd => !sd.IsDeleted && + sd.OrganizationId == organizationId && + sd.LeaseId == leaseId) + .FirstOrDefaultAsync(); + } + + /// + /// Gets all security deposits for an organization. + /// + public async Task> GetSecurityDepositsAsync(string? status = null) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (organizationId == null) + return new List(); + + // Filter by OrganizationId (stored as string, consistent with Property/Tenant models) + var query = _context.SecurityDeposits + .Where(sd => sd.OrganizationId == organizationId && !sd.IsDeleted); + + if (!string.IsNullOrEmpty(status)) + query = query.Where(sd => sd.Status == status); + + // Load navigation properties + var deposits = await query + .Include(sd => sd.Lease) + .ThenInclude(l => l.Property) + .Include(sd => sd.Tenant) + .Include(sd => sd.Dividends) + .OrderByDescending(sd => sd.DateReceived) + .ToListAsync(); + + return deposits; + } + + /// + /// Gets all security deposits that were in the investment pool during a specific year. + /// + public async Task> GetSecurityDepositsInPoolAsync(int year) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + var yearStart = new DateTime(year, 1, 1); + var yearEnd = new DateTime(year, 12, 31); + + return await _context.SecurityDeposits + .Include(sd => sd.Lease) + .ThenInclude(l => l.Property) + .Include(sd => sd.Tenant) + .Include(sd => sd.Dividends) + .Where(sd => !sd.IsDeleted && + sd.OrganizationId == organizationId && + sd.InInvestmentPool && + sd.PoolEntryDate.HasValue && + sd.PoolEntryDate.Value <= yearEnd && + (!sd.PoolExitDate.HasValue || sd.PoolExitDate.Value >= yearStart)) + .OrderBy(sd => sd.PoolEntryDate) + .ToListAsync(); + } + + #endregion + + #region Investment Pool Management + + /// + /// Creates or gets the investment pool for a specific year. + /// + public async Task GetOrCreateInvestmentPoolAsync(int year) + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue) + throw new InvalidOperationException("Organization context is required"); + + var pool = await _context.SecurityDepositInvestmentPools + .FirstOrDefaultAsync(p => p.Year == year && + p.OrganizationId == organizationId && + !p.IsDeleted); + + if (pool != null) + return pool; + + // Get organization settings for default share percentage + var settings = await _context.OrganizationSettings + .FirstOrDefaultAsync(s => s.OrganizationId == organizationId && !s.IsDeleted); + + pool = new SecurityDepositInvestmentPool + { + OrganizationId = organizationId.Value, + Year = year, + StartingBalance = 0, + EndingBalance = 0, + TotalEarnings = 0, + ReturnRate = 0, + OrganizationSharePercentage = settings?.OrganizationSharePercentage ?? 0.20m, + OrganizationShare = 0, + TenantShareTotal = 0, + ActiveLeaseCount = 0, + DividendPerLease = 0, + Status = ApplicationConstants.InvestmentPoolStatuses.Open, + CreatedBy = userId ?? string.Empty, + CreatedOn = DateTime.UtcNow + }; + + _context.SecurityDepositInvestmentPools.Add(pool); + await _context.SaveChangesAsync(); + + return pool; + } + + /// + /// Records annual investment performance for the pool. + /// + public async Task RecordInvestmentPerformanceAsync( + int year, + decimal startingBalance, + decimal endingBalance, + decimal totalEarnings) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var pool = await GetOrCreateInvestmentPoolAsync(year); + + pool.StartingBalance = startingBalance; + pool.EndingBalance = endingBalance; + pool.TotalEarnings = totalEarnings; + pool.ReturnRate = startingBalance > 0 ? totalEarnings / startingBalance : 0; + + // Calculate organization and tenant shares + if (totalEarnings > 0) + { + pool.OrganizationShare = totalEarnings * pool.OrganizationSharePercentage; + pool.TenantShareTotal = totalEarnings - pool.OrganizationShare; + } + else + { + // Losses absorbed by organization - no negative dividends + pool.OrganizationShare = 0; + pool.TenantShareTotal = 0; + } + + // Set tracking fields automatically + pool.LastModifiedBy = userId; + pool.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return pool; + } + + /// + /// Calculates dividends for all active deposits in a year. + /// This is typically run as a background job, so it uses the system account. + /// + public async Task> CalculateDividendsAsync(int year) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (organizationId == null) + throw new InvalidOperationException("Organization context is required"); + + // Use system account for automated calculations + var userId = ApplicationConstants.SystemUser.Id; + + var pool = await GetOrCreateInvestmentPoolAsync(year); + + // Get all deposits that were in the pool during this year + var yearStart = new DateTime(year, 1, 1); + var yearEnd = new DateTime(year, 12, 31); + + var activeDeposits = await _context.SecurityDeposits + .Include(sd => sd.Lease) + .Include(sd => sd.Tenant) + .Where(sd => !sd.IsDeleted && + sd.OrganizationId == organizationId && + sd.InInvestmentPool && + sd.PoolEntryDate.HasValue && + sd.PoolEntryDate.Value <= yearEnd && + (!sd.PoolExitDate.HasValue || sd.PoolExitDate.Value >= yearStart)) + .ToListAsync(); + + if (!activeDeposits.Any() || pool.TenantShareTotal <= 0) + { + pool.ActiveLeaseCount = 0; + pool.DividendPerLease = 0; + pool.Status = ApplicationConstants.InvestmentPoolStatuses.Calculated; + pool.DividendsCalculatedOn = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return new List(); + } + + pool.ActiveLeaseCount = activeDeposits.Count; + pool.DividendPerLease = pool.TenantShareTotal / pool.ActiveLeaseCount; + + var dividends = new List(); + + // Get default payment method from settings + var settings = await _context.OrganizationSettings + .FirstOrDefaultAsync(s => s.OrganizationId == organizationId && !s.IsDeleted); + + var defaultPaymentMethod = settings?.AllowTenantDividendChoice == true + ? ApplicationConstants.DividendPaymentMethods.Pending + : (settings?.DefaultDividendPaymentMethod ?? ApplicationConstants.DividendPaymentMethods.LeaseCredit); + + foreach (var deposit in activeDeposits) + { + // Check if dividend already exists + var existingDividend = await _context.SecurityDepositDividends + .FirstOrDefaultAsync(d => d.SecurityDepositId == deposit.Id && + d.Year == year && + !d.IsDeleted); + + if (existingDividend != null) + { + dividends.Add(existingDividend); + continue; + } + + // Calculate pro-ration factor based on months in pool + if (!deposit.PoolEntryDate.HasValue) + continue; // Skip if no entry date + + var effectiveStart = deposit.PoolEntryDate.Value > yearStart + ? deposit.PoolEntryDate.Value + : yearStart; + + var effectiveEnd = deposit.PoolExitDate.HasValue && deposit.PoolExitDate.Value < yearEnd + ? deposit.PoolExitDate.Value + : yearEnd; + + var monthsInPool = ((effectiveEnd.Year - effectiveStart.Year) * 12) + + effectiveEnd.Month - effectiveStart.Month + 1; + + var prorationFactor = Math.Min(monthsInPool / 12.0m, 1.0m); + + var dividend = new SecurityDepositDividend + { + OrganizationId = organizationId.Value, + SecurityDepositId = deposit.Id, + InvestmentPoolId = pool.Id, + LeaseId = deposit.LeaseId, + TenantId = deposit.TenantId, + Year = year, + BaseDividendAmount = pool.DividendPerLease, + ProrationFactor = prorationFactor, + DividendAmount = pool.DividendPerLease * prorationFactor, + PaymentMethod = defaultPaymentMethod, + Status = ApplicationConstants.DividendStatuses.Pending, + MonthsInPool = monthsInPool, + CreatedBy = userId, // System account for automated calculations + CreatedOn = DateTime.UtcNow + }; + + _context.SecurityDepositDividends.Add(dividend); + dividends.Add(dividend); + } + + pool.Status = ApplicationConstants.InvestmentPoolStatuses.Calculated; + pool.DividendsCalculatedOn = DateTime.UtcNow; + pool.LastModifiedBy = userId; + pool.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return dividends; + } + + /// + /// Gets investment pool by year. + /// + public async Task GetInvestmentPoolByYearAsync(int year) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.SecurityDepositInvestmentPools + .Include(p => p.Dividends) + .FirstOrDefaultAsync(p => p.Year == year && + p.OrganizationId == organizationId && + !p.IsDeleted); + } + + /// + /// Gets an investment pool by ID. + /// + public async Task GetInvestmentPoolByIdAsync(Guid poolId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.SecurityDepositInvestmentPools + .Include(p => p.Dividends) + .FirstOrDefaultAsync(p => p.Id == poolId && + p.OrganizationId == organizationId && + !p.IsDeleted); + } + + /// + /// Gets all investment pools for an organization. + /// + public async Task> GetInvestmentPoolsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.SecurityDepositInvestmentPools + .Include(p => p.Dividends) + .Where(p => !p.IsDeleted && p.OrganizationId == organizationId) + .OrderByDescending(p => p.Year) + .ToListAsync(); + } + + #endregion + + #region Dividend Management + + /// + /// Records tenant's payment method choice for dividend. + /// + public async Task RecordDividendChoiceAsync( + Guid dividendId, + string paymentMethod, + string? mailingAddress) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify dividend belongs to active organization + var dividend = await _context.SecurityDepositDividends + .FirstOrDefaultAsync(d => d.Id == dividendId && + d.OrganizationId == organizationId && + !d.IsDeleted); + + if (dividend == null) + return false; + + // Set tracking fields automatically + dividend.PaymentMethod = paymentMethod; + dividend.MailingAddress = mailingAddress; + dividend.ChoiceMadeOn = DateTime.UtcNow; + dividend.Status = ApplicationConstants.DividendStatuses.ChoiceMade; + dividend.LastModifiedBy = userId; + dividend.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return true; + } + + /// + /// Processes dividend payment (applies as credit or marks as paid). + /// + public async Task ProcessDividendPaymentAsync( + Guid dividendId, + string? paymentReference) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify dividend belongs to active organization + var dividend = await _context.SecurityDepositDividends + .Include(d => d.Lease) + .FirstOrDefaultAsync(d => d.Id == dividendId && + d.OrganizationId == organizationId && + !d.IsDeleted); + + if (dividend == null) + return false; + + // Set tracking fields automatically + dividend.PaymentReference = paymentReference; + dividend.PaymentProcessedOn = DateTime.UtcNow; + dividend.Status = dividend.PaymentMethod == ApplicationConstants.DividendPaymentMethods.LeaseCredit + ? ApplicationConstants.DividendStatuses.Applied + : ApplicationConstants.DividendStatuses.Paid; + dividend.LastModifiedBy = userId; + dividend.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return true; + } + + /// + /// Gets dividends for a specific tenant. + /// + public async Task> GetTenantDividendsAsync(Guid tenantId) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.SecurityDepositDividends + .Include(d => d.InvestmentPool) + .Include(d => d.Lease) + .ThenInclude(l => l.Property) + .Where(d => !d.IsDeleted && + d.OrganizationId == organizationId && + d.TenantId == tenantId) + .OrderByDescending(d => d.Year) + .ToListAsync(); + } + + /// + /// Gets all dividends for a specific year. + /// + public async Task> GetDividendsByYearAsync(int year) + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.SecurityDepositDividends + .Include(d => d.InvestmentPool) + .Include(d => d.SecurityDeposit) + .Include(d => d.Lease) + .ThenInclude(l => l.Property) + .Include(d => d.Tenant) + .Where(d => !d.IsDeleted && + d.OrganizationId == organizationId && + d.Year == year) + .OrderBy(d => d.Tenant.LastName) + .ThenBy(d => d.Tenant.FirstName) + .ToListAsync(); + } + + #endregion + + #region Refund Processing + + /// + /// Calculates total refund amount (deposit + dividends - deductions). + /// + public async Task CalculateRefundAmountAsync( + Guid securityDepositId, + decimal deductionsAmount) + { + var deposit = await _context.SecurityDeposits + .Include(sd => sd.Dividends.Where(d => !d.IsDeleted)) + .FirstOrDefaultAsync(sd => sd.Id == securityDepositId && !sd.IsDeleted); + + if (deposit == null) + return 0; + + var totalDividends = deposit.Dividends + .Where(d => d.Status == ApplicationConstants.DividendStatuses.Applied || + d.Status == ApplicationConstants.DividendStatuses.Paid) + .Sum(d => d.DividendAmount); + + return deposit.Amount + totalDividends - deductionsAmount; + } + + /// + /// Processes security deposit refund. + /// + public async Task ProcessRefundAsync( + Guid securityDepositId, + decimal deductionsAmount, + string? deductionsReason, + string refundMethod, + string? refundReference) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify deposit belongs to active organization + var deposit = await _context.SecurityDeposits + .Include(sd => sd.Dividends) + .FirstOrDefaultAsync(sd => sd.Id == securityDepositId && + sd.OrganizationId == organizationId && + !sd.IsDeleted); + + if (deposit == null) + throw new InvalidOperationException($"Security deposit {securityDepositId} not found"); + + if (deposit.IsRefunded) + throw new InvalidOperationException($"Security deposit {securityDepositId} has already been refunded"); + + // Remove from pool if still in it + if (deposit.InInvestmentPool) + { + await RemoveFromInvestmentPoolAsync(securityDepositId); + } + + var refundAmount = await CalculateRefundAmountAsync(securityDepositId, deductionsAmount); + + deposit.DeductionsAmount = deductionsAmount; + deposit.DeductionsReason = deductionsReason; + deposit.RefundAmount = refundAmount; + deposit.RefundMethod = refundMethod; + deposit.RefundReference = refundReference; + deposit.RefundProcessedDate = DateTime.UtcNow; + deposit.Status = refundAmount < deposit.Amount + ? ApplicationConstants.SecurityDepositStatuses.PartiallyRefunded + : ApplicationConstants.SecurityDepositStatuses.Refunded; + deposit.LastModifiedBy = userId; + deposit.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return deposit; + } + + /// + /// Gets security deposits pending refund (lease ended, not yet refunded). + /// + public async Task> GetPendingRefundsAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.SecurityDeposits + .Include(sd => sd.Lease) + .ThenInclude(l => l.Property) + .Include(sd => sd.Tenant) + .Include(sd => sd.Dividends) + .Where(sd => !sd.IsDeleted && + sd.OrganizationId == organizationId && + sd.Status == ApplicationConstants.SecurityDepositStatuses.Held && + sd.Lease.EndDate < DateTime.UtcNow) + .OrderBy(sd => sd.Lease.EndDate) + .ToListAsync(); + } + + /// + /// Closes an investment pool, marking it as complete. + /// + public async Task CloseInvestmentPoolAsync(Guid poolId) + { + var userId = await _userContext.GetUserIdAsync(); + + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Security: Verify pool belongs to active organization + var pool = await _context.SecurityDepositInvestmentPools + .FirstOrDefaultAsync(p => p.Id == poolId && + p.OrganizationId == organizationId && + !p.IsDeleted); + + if (pool == null) + throw new InvalidOperationException($"Investment pool {poolId} not found"); + + // Set tracking fields automatically + pool.Status = ApplicationConstants.InvestmentPoolStatuses.Closed; + pool.LastModifiedBy = userId; + pool.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return pool; + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/TenantConversionService.cs b/Aquiis.Professional/Application/Services/TenantConversionService.cs new file mode 100644 index 0000000..4ceecf1 --- /dev/null +++ b/Aquiis.Professional/Application/Services/TenantConversionService.cs @@ -0,0 +1,129 @@ +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; + + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Handles conversion of ProspectiveTenant to Tenant during lease signing workflow + /// + public class TenantConversionService + { + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + + private readonly UserContextService _userContext; + + public TenantConversionService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext) + { + _context = context; + _logger = logger; + _userContext = userContext; + } + + /// + /// Converts a ProspectiveTenant to a Tenant, maintaining audit trail + /// + /// ID of the prospective tenant to convert + /// The newly created Tenant, or existing Tenant if already converted + public async Task ConvertProspectToTenantAsync(Guid prospectiveTenantId) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + + // Check if this prospect has already been converted + var existingTenant = await _context.Tenants + .FirstOrDefaultAsync(t => t.ProspectiveTenantId == prospectiveTenantId && !t.IsDeleted); + + if (existingTenant != null) + { + _logger.LogInformation("ProspectiveTenant {ProspectId} already converted to Tenant {TenantId}", + prospectiveTenantId, existingTenant.Id); + return existingTenant; + } + + // Load the prospective tenant + var prospect = await _context.ProspectiveTenants + .FirstOrDefaultAsync(p => p.Id == prospectiveTenantId && !p.IsDeleted); + + if (prospect == null) + { + _logger.LogWarning("ProspectiveTenant {ProspectId} not found", prospectiveTenantId); + return null; + } + + // Create new tenant from prospect data + var tenant = new Tenant + { + OrganizationId = prospect.OrganizationId, + FirstName = prospect.FirstName, + LastName = prospect.LastName, + Email = prospect.Email, + PhoneNumber = prospect.Phone, + DateOfBirth = prospect.DateOfBirth, + IdentificationNumber = prospect.IdentificationNumber ?? string.Empty, + IsActive = true, + Notes = prospect.Notes ?? string.Empty, + ProspectiveTenantId = prospectiveTenantId, // Maintain audit trail + CreatedBy = userId ?? string.Empty, + CreatedOn = DateTime.UtcNow + }; + + _context.Tenants.Add(tenant); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Successfully converted ProspectiveTenant {ProspectId} to Tenant {TenantId}", + prospectiveTenantId, tenant.Id); + + return tenant; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error converting ProspectiveTenant {ProspectId} to Tenant", prospectiveTenantId); + throw; + } + } + + /// + /// Gets tenant by ProspectiveTenantId, or null if not yet converted + /// + public async Task GetTenantByProspectIdAsync(Guid prospectiveTenantId) + { + return await _context.Tenants + .FirstOrDefaultAsync(t => t.ProspectiveTenantId == prospectiveTenantId && !t.IsDeleted); + } + + /// + /// Checks if a prospect has already been converted to a tenant + /// + public async Task IsProspectAlreadyConvertedAsync(Guid prospectiveTenantId) + { + return await _context.Tenants + .AnyAsync(t => t.ProspectiveTenantId == prospectiveTenantId && !t.IsDeleted); + } + + /// + /// Gets the ProspectiveTenant history for a given Tenant + /// + public async Task GetProspectHistoryForTenantAsync(Guid tenantId) + { + var tenant = await _context.Tenants + .FirstOrDefaultAsync(t => t.Id == tenantId && !t.IsDeleted); + + if (tenant?.ProspectiveTenantId == null) + return null; + + return await _context.ProspectiveTenants + .Include(p => p.InterestedProperty) + .Include(p => p.Applications) + .Include(p => p.Tours) + .FirstOrDefaultAsync(p => p.Id == tenant.ProspectiveTenantId.Value); + } + } +} diff --git a/Aquiis.Professional/Application/Services/TenantService.cs b/Aquiis.Professional/Application/Services/TenantService.cs new file mode 100644 index 0000000..f9fd612 --- /dev/null +++ b/Aquiis.Professional/Application/Services/TenantService.cs @@ -0,0 +1,418 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing Tenant entities. + /// Inherits common CRUD operations from BaseService and adds tenant-specific business logic. + /// + public class TenantService : BaseService + { + public TenantService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings) + : base(context, logger, userContext, settings) + { + } + + #region Overrides with Tenant-Specific Logic + + /// + /// Retrieves a tenant by ID with related entities (Leases). + /// + public async Task GetTenantWithRelationsAsync(Guid tenantId) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Tenants + .Include(t => t.Leases) + .FirstOrDefaultAsync(t => t.Id == tenantId && + t.OrganizationId == organizationId && + !t.IsDeleted); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetTenantWithRelations"); + throw; + } + } + + /// + /// Retrieves all tenants with related entities. + /// + public async Task> GetTenantsWithRelationsAsync() + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Tenants + .Include(t => t.Leases) + .Where(t => !t.IsDeleted && t.OrganizationId == organizationId) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetTenantsWithRelations"); + throw; + } + } + + /// + /// Validates tenant data before create/update operations. + /// + protected override async Task ValidateEntityAsync(Tenant tenant) + { + // Validate required email + if (string.IsNullOrWhiteSpace(tenant.Email)) + { + throw new ValidationException("Tenant email is required."); + } + + // Validate required identification number + if (string.IsNullOrWhiteSpace(tenant.IdentificationNumber)) + { + throw new ValidationException("Tenant identification number is required."); + } + + // Check for duplicate email in same organization + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var emailExists = await _context.Tenants + .AnyAsync(t => t.Email == tenant.Email && + t.Id != tenant.Id && + t.OrganizationId == organizationId && + !t.IsDeleted); + + if (emailExists) + { + throw new ValidationException($"A tenant with email '{tenant.Email}' already exists."); + } + + // Check for duplicate identification number in same organization + var idNumberExists = await _context.Tenants + .AnyAsync(t => t.IdentificationNumber == tenant.IdentificationNumber && + t.Id != tenant.Id && + t.OrganizationId == organizationId && + !t.IsDeleted); + + if (idNumberExists) + { + throw new ValidationException($"A tenant with identification number '{tenant.IdentificationNumber}' already exists."); + } + + await base.ValidateEntityAsync(tenant); + } + + #endregion + + #region Business Logic Methods + + /// + /// Retrieves a tenant by identification number. + /// + public async Task GetTenantByIdentificationNumberAsync(string identificationNumber) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Tenants + .Include(t => t.Leases) + .FirstOrDefaultAsync(t => t.IdentificationNumber == identificationNumber && + t.OrganizationId == organizationId && + !t.IsDeleted); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetTenantByIdentificationNumber"); + throw; + } + } + + /// + /// Retrieves a tenant by email address. + /// + public async Task GetTenantByEmailAsync(string email) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Tenants + .Include(t => t.Leases) + .FirstOrDefaultAsync(t => t.Email == email && + t.OrganizationId == organizationId && + !t.IsDeleted); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetTenantByEmail"); + throw; + } + } + + /// + /// Retrieves all active tenants (IsActive = true). + /// + public async Task> GetActiveTenantsAsync() + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Tenants + .Where(t => !t.IsDeleted && + t.IsActive && + t.OrganizationId == organizationId) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetActiveTenants"); + throw; + } + } + + /// + /// Retrieves all tenants with active leases. + /// + public async Task> GetTenantsWithActiveLeasesAsync() + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.Tenants + .Where(t => !t.IsDeleted && + t.OrganizationId == organizationId) + .Where(t => _context.Leases.Any(l => + l.TenantId == t.Id && + l.Status == ApplicationConstants.LeaseStatuses.Active && + !l.IsDeleted)) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetTenantsWithActiveLeases"); + throw; + } + } + + /// + /// Retrieves tenants by property ID (via their leases). + /// + public async Task> GetTenantsByPropertyIdAsync(Guid propertyId) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var leases = await _context.Leases + .Include(l => l.Tenant) + .Where(l => l.PropertyId == propertyId && + l.Tenant!.OrganizationId == organizationId && + !l.IsDeleted && + !l.Tenant.IsDeleted) + .ToListAsync(); + + var tenantIds = leases.Select(l => l.TenantId).Distinct().ToList(); + + return await _context.Tenants + .Where(t => tenantIds.Contains(t.Id) && + t.OrganizationId == organizationId && + !t.IsDeleted) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetTenantsByPropertyId"); + throw; + } + } + + /// + /// Retrieves tenants by lease ID. + /// + public async Task> GetTenantsByLeaseIdAsync(Guid leaseId) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var leases = await _context.Leases + .Include(l => l.Tenant) + .Where(l => l.Id == leaseId && + l.Tenant!.OrganizationId == organizationId && + !l.IsDeleted && + !l.Tenant.IsDeleted) + .ToListAsync(); + + var tenantIds = leases.Select(l => l.TenantId).Distinct().ToList(); + + return await _context.Tenants + .Where(t => tenantIds.Contains(t.Id) && + t.OrganizationId == organizationId && + !t.IsDeleted) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetTenantsByLeaseId"); + throw; + } + } + + /// + /// Searches tenants by name, email, or identification number. + /// + public async Task> SearchTenantsAsync(string searchTerm) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrWhiteSpace(searchTerm)) + { + return await _context.Tenants + .Where(t => !t.IsDeleted && t.OrganizationId == organizationId) + .OrderBy(t => t.LastName) + .ThenBy(t => t.FirstName) + .Take(20) + .ToListAsync(); + } + + return await _context.Tenants + .Where(t => !t.IsDeleted && + t.OrganizationId == organizationId && + (t.FirstName.Contains(searchTerm) || + t.LastName.Contains(searchTerm) || + t.Email.Contains(searchTerm) || + t.IdentificationNumber.Contains(searchTerm))) + .OrderBy(t => t.LastName) + .ThenBy(t => t.FirstName) + .Take(20) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "SearchTenants"); + throw; + } + } + + /// + /// Calculates the total outstanding balance for a tenant across all their leases. + /// + public async Task CalculateTenantBalanceAsync(Guid tenantId) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Verify tenant exists and belongs to organization + var tenant = await GetByIdAsync(tenantId); + if (tenant == null) + { + throw new InvalidOperationException($"Tenant not found: {tenantId}"); + } + + // Calculate total invoiced amount + var totalInvoiced = await _context.Invoices + .Where(i => i.Lease.TenantId == tenantId && + i.Lease.Property.OrganizationId == organizationId && + !i.IsDeleted && + !i.Lease.IsDeleted) + .SumAsync(i => i.Amount); + + // Calculate total paid amount + var totalPaid = await _context.Payments + .Where(p => p.Invoice.Lease.TenantId == tenantId && + p.Invoice.Lease.Property.OrganizationId == organizationId && + !p.IsDeleted && + !p.Invoice.IsDeleted) + .SumAsync(p => p.Amount); + + return totalInvoiced - totalPaid; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "CalculateTenantBalance"); + throw; + } + } + + #endregion + } +} diff --git a/Aquiis.Professional/Application/Services/TourService.cs b/Aquiis.Professional/Application/Services/TourService.cs new file mode 100644 index 0000000..89ef4a7 --- /dev/null +++ b/Aquiis.Professional/Application/Services/TourService.cs @@ -0,0 +1,490 @@ +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Interfaces; +using Aquiis.Professional.Core.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Application.Services +{ + /// + /// Service for managing property tours with business logic for scheduling, + /// prospect tracking, and checklist integration. + /// + public class TourService : BaseService + { + private readonly ICalendarEventService _calendarEventService; + private readonly ChecklistService _checklistService; + + public TourService( + ApplicationDbContext context, + ILogger logger, + UserContextService userContext, + IOptions settings, + ICalendarEventService calendarEventService, + ChecklistService checklistService) + : base(context, logger, userContext, settings) + { + _calendarEventService = calendarEventService; + _checklistService = checklistService; + } + + #region Helper Methods + + protected async Task GetUserIdAsync() + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + return userId; + } + + protected async Task GetActiveOrganizationIdAsync() + { + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue) + { + throw new UnauthorizedAccessException("No active organization."); + } + return organizationId.Value; + } + + #endregion + + /// + /// Validates tour business rules. + /// + protected override async Task ValidateEntityAsync(Tour entity) + { + var errors = new List(); + + // Required fields + if (entity.ProspectiveTenantId == Guid.Empty) + { + errors.Add("Prospective tenant is required"); + } + + if (entity.PropertyId == Guid.Empty) + { + errors.Add("Property is required"); + } + + if (entity.ScheduledOn == default) + { + errors.Add("Scheduled date/time is required"); + } + + if (entity.DurationMinutes <= 0) + { + errors.Add("Duration must be greater than 0"); + } + + if (errors.Any()) + { + throw new InvalidOperationException(string.Join("; ", errors)); + } + + await Task.CompletedTask; + } + + /// + /// Gets all tours for the active organization. + /// + public override async Task> GetAllAsync() + { + var organizationId = await GetActiveOrganizationIdAsync(); + + return await _context.Tours + .Include(t => t.ProspectiveTenant) + .Include(t => t.Property) + .Include(t => t.Checklist) + .Where(t => !t.IsDeleted && t.OrganizationId == organizationId) + .OrderBy(t => t.ScheduledOn) + .ToListAsync(); + } + + /// + /// Gets tours by prospective tenant ID. + /// + public async Task> GetByProspectiveIdAsync(Guid prospectiveTenantId) + { + var organizationId = await GetActiveOrganizationIdAsync(); + + return await _context.Tours + .Include(t => t.ProspectiveTenant) + .Include(t => t.Property) + .Include(t => t.Checklist) + .Where(t => t.ProspectiveTenantId == prospectiveTenantId && + !t.IsDeleted && + t.OrganizationId == organizationId) + .OrderBy(t => t.ScheduledOn) + .ToListAsync(); + } + + /// + /// Gets a single tour by ID with related data. + /// + public override async Task GetByIdAsync(Guid id) + { + var organizationId = await GetActiveOrganizationIdAsync(); + + return await _context.Tours + .Include(t => t.ProspectiveTenant) + .Include(t => t.Property) + .Include(t => t.Checklist) + .FirstOrDefaultAsync(t => t.Id == id && !t.IsDeleted && t.OrganizationId == organizationId); + } + + /// + /// Creates a new tour with optional checklist from template. + /// + public async Task CreateAsync(Tour tour, Guid? checklistTemplateId = null) + { + await ValidateEntityAsync(tour); + + var userId = await GetUserIdAsync(); + var organizationId = await GetActiveOrganizationIdAsync(); + + tour.Id = Guid.NewGuid(); + tour.OrganizationId = organizationId; + tour.CreatedBy = userId; + tour.CreatedOn = DateTime.UtcNow; + tour.Status = ApplicationConstants.TourStatuses.Scheduled; + + // Get prospect information for checklist + var prospective = await _context.ProspectiveTenants + .Include(p => p.InterestedProperty) + .FirstOrDefaultAsync(p => p.Id == tour.ProspectiveTenantId); + + // Create checklist if template specified + if (checklistTemplateId.HasValue || prospective != null) + { + await CreateTourChecklistAsync(tour, prospective, checklistTemplateId); + } + + await _context.Tours.AddAsync(tour); + await _context.SaveChangesAsync(); + + // Create calendar event for the tour + await _calendarEventService.CreateOrUpdateEventAsync(tour); + + // Update prospective tenant status if needed + if (prospective != null && prospective.Status == ApplicationConstants.ProspectiveStatuses.Lead) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.TourScheduled; + prospective.LastModifiedOn = DateTime.UtcNow; + prospective.LastModifiedBy = userId; + await _context.SaveChangesAsync(); + } + + _logger.LogInformation("Created tour {TourId} for prospect {ProspectId}", + tour.Id, tour.ProspectiveTenantId); + + return tour; + } + + /// + /// Creates a tour using the base CreateAsync (without template parameter). + /// + public override async Task CreateAsync(Tour tour) + { + return await CreateAsync(tour, checklistTemplateId: null); + } + + /// + /// Updates an existing tour. + /// + public override async Task UpdateAsync(Tour tour) + { + await ValidateEntityAsync(tour); + + var userId = await GetUserIdAsync(); + var organizationId = await GetActiveOrganizationIdAsync(); + + // Security: Verify tour belongs to active organization + var existing = await _context.Tours + .FirstOrDefaultAsync(t => t.Id == tour.Id && t.OrganizationId == organizationId); + + if (existing == null) + { + throw new UnauthorizedAccessException($"Tour {tour.Id} not found in active organization."); + } + + // Set tracking fields + tour.LastModifiedBy = userId; + tour.LastModifiedOn = DateTime.UtcNow; + tour.OrganizationId = organizationId; // Prevent org hijacking + + _context.Entry(existing).CurrentValues.SetValues(tour); + await _context.SaveChangesAsync(); + + // Update calendar event + await _calendarEventService.CreateOrUpdateEventAsync(tour); + + _logger.LogInformation("Updated tour {TourId}", tour.Id); + + return tour; + } + + /// + /// Deletes a tour (soft delete). + /// + public override async Task DeleteAsync(Guid id) + { + var userId = await GetUserIdAsync(); + var organizationId = await GetActiveOrganizationIdAsync(); + + var tour = await _context.Tours + .FirstOrDefaultAsync(t => t.Id == id && t.OrganizationId == organizationId); + + if (tour == null) + { + throw new KeyNotFoundException($"Tour {id} not found."); + } + + tour.IsDeleted = true; + tour.LastModifiedBy = userId; + tour.LastModifiedOn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + // TODO: Delete associated calendar event when interface method is available + // await _calendarEventService.DeleteEventBySourceAsync(id, nameof(Tour)); + + _logger.LogInformation("Deleted tour {TourId}", id); + + return true; + } + + /// + /// Completes a tour with feedback and interest level. + /// + public async Task CompleteTourAsync(Guid tourId, string? feedback = null, string? interestLevel = null) + { + var userId = await GetUserIdAsync(); + var organizationId = await GetActiveOrganizationIdAsync(); + + var tour = await GetByIdAsync(tourId); + if (tour == null) return false; + + // Update tour status and feedback + tour.Status = ApplicationConstants.TourStatuses.Completed; + tour.Feedback = feedback; + tour.InterestLevel = interestLevel; + tour.ConductedBy = userId; + tour.LastModifiedOn = DateTime.UtcNow; + tour.LastModifiedBy = userId; + + await _context.SaveChangesAsync(); + + // Update calendar event + await _calendarEventService.CreateOrUpdateEventAsync(tour); + + // Update prospective tenant status if highly interested + if (interestLevel == ApplicationConstants.TourInterestLevels.VeryInterested) + { + var prospect = await _context.ProspectiveTenants + .FirstOrDefaultAsync(p => p.Id == tour.ProspectiveTenantId); + + if (prospect != null && prospect.Status == ApplicationConstants.ProspectiveStatuses.TourScheduled) + { + prospect.Status = ApplicationConstants.ProspectiveStatuses.Applied; + prospect.LastModifiedOn = DateTime.UtcNow; + prospect.LastModifiedBy = userId; + await _context.SaveChangesAsync(); + } + } + + _logger.LogInformation("Completed tour {TourId} with interest level {InterestLevel}", + tourId, interestLevel); + + return true; + } + + /// + /// Creates a checklist for a tour from a template. + /// + private async Task CreateTourChecklistAsync(Tour tour, ProspectiveTenant? prospective, Guid? templateId) + { + var organizationId = await GetActiveOrganizationIdAsync(); + + // Find the specified template, or fall back to default "Property Tour" template + ChecklistTemplate? tourTemplate = null; + + if (templateId.HasValue) + { + tourTemplate = await _context.ChecklistTemplates + .FirstOrDefaultAsync(t => t.Id == templateId.Value && + (t.OrganizationId == organizationId || t.IsSystemTemplate) && + !t.IsDeleted); + } + + // Fall back to default "Property Tour" template if not specified or not found + if (tourTemplate == null) + { + tourTemplate = await _context.ChecklistTemplates + .FirstOrDefaultAsync(t => t.Name == "Property Tour" && + (t.OrganizationId == organizationId || t.IsSystemTemplate) && + !t.IsDeleted); + } + + if (tourTemplate != null && prospective != null) + { + // Create checklist from template + var checklist = await _checklistService.CreateChecklistFromTemplateAsync(tourTemplate.Id); + + // Customize checklist with prospect information + checklist.Name = $"Property Tour - {prospective.FullName}"; + checklist.PropertyId = tour.PropertyId; + checklist.GeneralNotes = $"Prospect: {prospective.FullName}\n" + + $"Email: {prospective.Email}\n" + + $"Phone: {prospective.Phone}\n" + + $"Scheduled: {tour.ScheduledOn:MMM dd, yyyy h:mm tt}"; + + // Link tour to checklist + tour.ChecklistId = checklist.Id; + } + } + + /// + /// Marks a tour as no-show and updates the associated calendar event. + /// + public async Task MarkTourAsNoShowAsync(Guid tourId) + { + try + { + var userId = await GetUserIdAsync(); + var organizationId = await GetActiveOrganizationIdAsync(); + + var tour = await GetByIdAsync(tourId); + if (tour == null) return false; + + if (tour.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("User is not authorized to update this tour."); + } + + // Update tour status to NoShow + tour.Status = "NoShow"; + tour.LastModifiedOn = DateTime.UtcNow; + tour.LastModifiedBy = userId; + + // Update calendar event status + if (tour.CalendarEventId.HasValue) + { + var calendarEvent = await _context.CalendarEvents + .FirstOrDefaultAsync(e => e.Id == tour.CalendarEventId.Value); + if (calendarEvent != null) + { + calendarEvent.Status = "NoShow"; + calendarEvent.LastModifiedBy = userId; + calendarEvent.LastModifiedOn = DateTime.UtcNow; + } + } + + await _context.SaveChangesAsync(); + _logger.LogInformation("Tour {TourId} marked as no-show by user {UserId}", tourId, userId); + return true; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "MarkTourAsNoShow"); + throw; + } + } + + /// + /// Cancels a tour and updates related prospect status. + /// + public async Task CancelTourAsync(Guid tourId) + { + try + { + var userId = await GetUserIdAsync(); + var organizationId = await GetActiveOrganizationIdAsync(); + var tour = await GetByIdAsync(tourId); + + if (tour == null) + { + throw new InvalidOperationException("Tour not found."); + } + + if (tour.OrganizationId != organizationId) + { + throw new UnauthorizedAccessException("Unauthorized access to tour."); + } + + // Update tour status to cancelled + tour.Status = ApplicationConstants.TourStatuses.Cancelled; + tour.LastModifiedOn = DateTime.UtcNow; + tour.LastModifiedBy = userId; + await _context.SaveChangesAsync(); + + // Update calendar event status + await _calendarEventService.CreateOrUpdateEventAsync(tour); + + // Check if prospect has any other scheduled tours + var prospective = await _context.ProspectiveTenants.FindAsync(tour.ProspectiveTenantId); + if (prospective != null && prospective.Status == ApplicationConstants.ProspectiveStatuses.TourScheduled) + { + var hasOtherScheduledTours = await _context.Tours + .AnyAsync(s => s.ProspectiveTenantId == tour.ProspectiveTenantId + && s.Id != tourId + && !s.IsDeleted + && s.Status == ApplicationConstants.TourStatuses.Scheduled); + + // If no other scheduled tours, revert prospect status to Lead + if (!hasOtherScheduledTours) + { + prospective.Status = ApplicationConstants.ProspectiveStatuses.Lead; + prospective.LastModifiedOn = DateTime.UtcNow; + prospective.LastModifiedBy = userId; + await _context.SaveChangesAsync(); + } + } + + _logger.LogInformation("Tour {TourId} cancelled by user {UserId}", tourId, userId); + return true; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "CancelTour"); + throw; + } + } + + /// + /// Gets upcoming tours within specified number of days. + /// + public async Task> GetUpcomingToursAsync(int days = 7) + { + try + { + var organizationId = await GetActiveOrganizationIdAsync(); + var startDate = DateTime.UtcNow; + var endDate = startDate.AddDays(days); + + return await _context.Tours + .Where(s => s.OrganizationId == organizationId + && !s.IsDeleted + && s.Status == ApplicationConstants.TourStatuses.Scheduled + && s.ScheduledOn >= startDate + && s.ScheduledOn <= endDate) + .Include(s => s.ProspectiveTenant) + .Include(s => s.Property) + .Include(s => s.Checklist) + .OrderBy(s => s.ScheduledOn) + .ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, "GetUpcomingTours"); + throw; + } + } + } +} diff --git a/Aquiis.Professional/Application/Services/Workflows/AccountWorkflowService.cs b/Aquiis.Professional/Application/Services/Workflows/AccountWorkflowService.cs new file mode 100644 index 0000000..e138cc3 --- /dev/null +++ b/Aquiis.Professional/Application/Services/Workflows/AccountWorkflowService.cs @@ -0,0 +1,40 @@ + +using Aquiis.Professional.Shared.Components.Account; +using Aquiis.Professional.Application.Services; +using Aquiis.Professional.Shared.Services; +using Aquiis.Professional.Infrastructure.Data; + +namespace Aquiis.Professional.Application.Services.Workflows +{ + public enum AccountStatus + { + Created, + Active, + Locked, + Closed + } + public class AccountWorkflowService : BaseWorkflowService, IWorkflowState + { + public AccountWorkflowService(ApplicationDbContext context, + UserContextService userContext, + NotificationService notificationService) + : base(context, userContext) + { + } + // Implementation of the account workflow service + public string GetInvalidTransitionReason(AccountStatus fromStatus, AccountStatus toStatus) + { + throw new NotImplementedException(); + } + + public List GetValidNextStates(AccountStatus currentStatus) + { + throw new NotImplementedException(); + } + + public bool IsValidTransition(AccountStatus fromStatus, AccountStatus toStatus) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Application/Services/Workflows/ApplicationWorkflowService.cs b/Aquiis.Professional/Application/Services/Workflows/ApplicationWorkflowService.cs new file mode 100644 index 0000000..aecc275 --- /dev/null +++ b/Aquiis.Professional/Application/Services/Workflows/ApplicationWorkflowService.cs @@ -0,0 +1,1277 @@ +using Aquiis.Professional.Application.Services.Workflows; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Professional.Application.Services.Workflows +{ + /// + /// Application status enumeration for state machine validation. + /// + public enum ApplicationStatus + { + Submitted, + UnderReview, + Screening, + Approved, + Denied, + LeaseOffered, + LeaseAccepted, + LeaseDeclined, + Expired, + Withdrawn + } + + /// + /// Workflow service for rental application lifecycle management. + /// Centralizes all state transitions from prospect inquiry through lease offer generation. + /// + public class ApplicationWorkflowService : BaseWorkflowService, IWorkflowState + { + private readonly NoteService _noteService; + + public ApplicationWorkflowService( + ApplicationDbContext context, + UserContextService userContext, + NoteService noteService) + : base(context, userContext) + { + _noteService = noteService; + } + + #region State Machine Implementation + + public bool IsValidTransition(ApplicationStatus fromStatus, ApplicationStatus toStatus) + { + var validTransitions = GetValidNextStates(fromStatus); + return validTransitions.Contains(toStatus); + } + + public List GetValidNextStates(ApplicationStatus currentStatus) + { + return currentStatus switch + { + ApplicationStatus.Submitted => new() + { + ApplicationStatus.UnderReview, + ApplicationStatus.Denied, + ApplicationStatus.Withdrawn, + ApplicationStatus.Expired + }, + ApplicationStatus.UnderReview => new() + { + ApplicationStatus.Screening, + ApplicationStatus.Denied, + ApplicationStatus.Withdrawn, + ApplicationStatus.Expired + }, + ApplicationStatus.Screening => new() + { + ApplicationStatus.Approved, + ApplicationStatus.Denied, + ApplicationStatus.Withdrawn + }, + ApplicationStatus.Approved => new() + { + ApplicationStatus.LeaseOffered, + ApplicationStatus.Denied // Can deny after approval if issues found + }, + ApplicationStatus.LeaseOffered => new() + { + ApplicationStatus.LeaseAccepted, + ApplicationStatus.LeaseDeclined, + ApplicationStatus.Expired + }, + _ => new List() // Terminal states have no valid transitions + }; + } + + public string GetInvalidTransitionReason(ApplicationStatus fromStatus, ApplicationStatus toStatus) + { + var validStates = GetValidNextStates(fromStatus); + return $"Cannot transition from {fromStatus} to {toStatus}. Valid next states: {string.Join(", ", validStates)}"; + } + + #endregion + + #region Core Workflow Methods + + /// + /// Submits a new rental application for a prospect and property. + /// Creates application, updates property status if first app, and updates prospect status. + /// + public async Task> SubmitApplicationAsync( + Guid prospectId, + Guid propertyId, + ApplicationSubmissionModel model) + { + return await ExecuteWorkflowAsync(async () => + { + // Validation + var validation = await ValidateApplicationSubmissionAsync(prospectId, propertyId); + if (!validation.Success) + return WorkflowResult.Fail(validation.Errors); + + var userId = await GetCurrentUserIdAsync(); + var orgId = await GetActiveOrganizationIdAsync(); + + // Get organization settings for expiration days + var settings = await _context.OrganizationSettings + .FirstOrDefaultAsync(s => s.OrganizationId == orgId); + + var expirationDays = settings?.ApplicationExpirationDays ?? 30; + + // Create application + var application = new RentalApplication + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + ProspectiveTenantId = prospectId, + PropertyId = propertyId, + Status = ApplicationConstants.ApplicationStatuses.Submitted, + AppliedOn = DateTime.UtcNow, + ExpiresOn = DateTime.UtcNow.AddDays(expirationDays), + ApplicationFee = model.ApplicationFee, + ApplicationFeePaid = model.ApplicationFeePaid, + ApplicationFeePaidOn = model.ApplicationFeePaid ? DateTime.UtcNow : null, + ApplicationFeePaymentMethod = model.ApplicationFeePaymentMethod, + CurrentAddress = model.CurrentAddress, + CurrentCity = model.CurrentCity, + CurrentState = model.CurrentState, + CurrentZipCode = model.CurrentZipCode, + CurrentRent = model.CurrentRent, + LandlordName = model.LandlordName, + LandlordPhone = model.LandlordPhone, + EmployerName = model.EmployerName, + JobTitle = model.JobTitle, + MonthlyIncome = model.MonthlyIncome, + EmploymentLengthMonths = model.EmploymentLengthMonths, + Reference1Name = model.Reference1Name, + Reference1Phone = model.Reference1Phone, + Reference1Relationship = model.Reference1Relationship, + Reference2Name = model.Reference2Name, + Reference2Phone = model.Reference2Phone, + Reference2Relationship = model.Reference2Relationship, + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.RentalApplications.Add(application); + // Note: EF Core will assign ID when transaction commits + + // Update property status if this is first application + var property = await _context.Properties + .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == orgId); + + if (property != null && property.Status == ApplicationConstants.PropertyStatuses.Available) + { + property.Status = ApplicationConstants.PropertyStatuses.ApplicationPending; + property.LastModifiedBy = userId; + property.LastModifiedOn = DateTime.UtcNow; + } + + // Update prospect status + var prospect = await _context.ProspectiveTenants + .FirstOrDefaultAsync(p => p.Id == prospectId && p.OrganizationId == orgId); + + if (prospect != null) + { + var oldStatus = prospect.Status; + prospect.Status = ApplicationConstants.ProspectiveStatuses.Applied; + prospect.LastModifiedBy = userId; + prospect.LastModifiedOn = DateTime.UtcNow; + + // Log prospect transition + await LogTransitionAsync( + "ProspectiveTenant", + prospectId, + oldStatus, + prospect.Status, + "SubmitApplication"); + } + + // Log application creation + await LogTransitionAsync( + "RentalApplication", + application.Id, + null, + ApplicationConstants.ApplicationStatuses.Submitted, + "SubmitApplication"); + + return WorkflowResult.Ok( + application, + "Application submitted successfully"); + + }); + } + + /// + /// Marks an application as under manual review. + /// + public async Task MarkApplicationUnderReviewAsync(Guid applicationId) + { + return await ExecuteWorkflowAsync(async () => + { + var application = await GetApplicationAsync(applicationId); + if (application == null) + return WorkflowResult.Fail("Application not found"); + + // Validate state transition + if (!IsValidTransition( + Enum.Parse(application.Status), + ApplicationStatus.UnderReview)) + { + return WorkflowResult.Fail(GetInvalidTransitionReason( + Enum.Parse(application.Status), + ApplicationStatus.UnderReview)); + } + + var userId = await GetCurrentUserIdAsync(); + var oldStatus = application.Status; + + application.Status = ApplicationConstants.ApplicationStatuses.UnderReview; + application.DecisionBy = userId; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + await LogTransitionAsync( + "RentalApplication", + applicationId, + oldStatus, + application.Status, + "MarkUnderReview"); + + return WorkflowResult.Ok("Application marked as under review"); + + }); + } + + /// + /// Initiates background and/or credit screening for an application. + /// Requires application fee to be paid. + /// + public async Task> InitiateScreeningAsync( + Guid applicationId, + bool requestBackgroundCheck, + bool requestCreditCheck) + { + return await ExecuteWorkflowAsync(async () => + { + var application = await GetApplicationAsync(applicationId); + if (application == null) + return WorkflowResult.Fail("Application not found"); + + var userId = await GetCurrentUserIdAsync(); + var orgId = await GetActiveOrganizationIdAsync(); + + // Auto-transition from Submitted to UnderReview if needed + if (application.Status == ApplicationConstants.ApplicationStatuses.Submitted) + { + application.Status = ApplicationConstants.ApplicationStatuses.UnderReview; + application.DecisionBy = userId; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + await LogTransitionAsync( + "RentalApplication", + applicationId, + ApplicationConstants.ApplicationStatuses.Submitted, + ApplicationConstants.ApplicationStatuses.UnderReview, + "AutoTransition-InitiateScreening"); + } + + // Validate state + if (application.Status != ApplicationConstants.ApplicationStatuses.UnderReview) + return WorkflowResult.Fail( + $"Application must be Submitted or Under Review to initiate screening. Current status: {application.Status}"); + + // Validate application fee paid + if (!application.ApplicationFeePaid) + return WorkflowResult.Fail( + "Application fee must be paid before initiating screening"); + + // Check for existing screening + var existingScreening = await _context.ApplicationScreenings + .FirstOrDefaultAsync(s => s.RentalApplicationId == applicationId); + + if (existingScreening != null) + return WorkflowResult.Fail( + "Screening already exists for this application"); + + // Create screening record + var screening = new ApplicationScreening + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + RentalApplicationId = applicationId, + BackgroundCheckRequested = requestBackgroundCheck, + BackgroundCheckRequestedOn = requestBackgroundCheck ? DateTime.UtcNow : null, + CreditCheckRequested = requestCreditCheck, + CreditCheckRequestedOn = requestCreditCheck ? DateTime.UtcNow : null, + OverallResult = "Pending", + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.ApplicationScreenings.Add(screening); + + // Update application status + var oldStatus = application.Status; + application.Status = ApplicationConstants.ApplicationStatuses.Screening; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect status + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.Screening; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + + await LogTransitionAsync( + "RentalApplication", + applicationId, + oldStatus, + application.Status, + "InitiateScreening"); + + return WorkflowResult.Ok( + screening, + "Screening initiated successfully"); + + }); + } + + /// + /// Approves an application after screening review. + /// Requires screening to be completed with passing result. + /// + public async Task ApproveApplicationAsync(Guid applicationId) + { + return await ExecuteWorkflowAsync(async () => + { + var application = await GetApplicationAsync(applicationId); + if (application == null) + return WorkflowResult.Fail("Application not found"); + + // Validate state + if (application.Status != ApplicationConstants.ApplicationStatuses.Screening) + return WorkflowResult.Fail( + $"Application must be in Screening status to approve. Current status: {application.Status}"); + + // Validate screening completed + if (application.Screening == null) + return WorkflowResult.Fail("Screening record not found"); + + if (application.Screening.OverallResult != "Passed" && + application.Screening.OverallResult != "ConditionalPass") + return WorkflowResult.Fail( + $"Cannot approve application with screening result: {application.Screening.OverallResult}"); + + var userId = await GetCurrentUserIdAsync(); + var oldStatus = application.Status; + + // Update application + application.Status = ApplicationConstants.ApplicationStatuses.Approved; + application.DecidedOn = DateTime.UtcNow; + application.DecisionBy = userId; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.Approved; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + + await LogTransitionAsync( + "RentalApplication", + applicationId, + oldStatus, + application.Status, + "ApproveApplication"); + + return WorkflowResult.Ok("Application approved successfully"); + + }); + } + + /// + /// Denies an application with a required reason. + /// Rolls back property status if no other pending applications exist. + /// + public async Task DenyApplicationAsync(Guid applicationId, string denialReason) + { + return await ExecuteWorkflowAsync(async () => + { + if (string.IsNullOrWhiteSpace(denialReason)) + return WorkflowResult.Fail("Denial reason is required"); + + var application = await GetApplicationAsync(applicationId); + if (application == null) + return WorkflowResult.Fail("Application not found"); + + // Validate not already in terminal state + var terminalStates = new[] { + ApplicationConstants.ApplicationStatuses.Denied, + ApplicationConstants.ApplicationStatuses.LeaseAccepted, + ApplicationConstants.ApplicationStatuses.Withdrawn + }; + + if (terminalStates.Contains(application.Status)) + return WorkflowResult.Fail( + $"Cannot deny application in {application.Status} status"); + + var userId = await GetCurrentUserIdAsync(); + var oldStatus = application.Status; + + // Update application + application.Status = ApplicationConstants.ApplicationStatuses.Denied; + application.DenialReason = denialReason; + application.DecidedOn = DateTime.UtcNow; + application.DecisionBy = userId; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.Denied; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + + // Check if property status should roll back (exclude this application which is being denied) + await RollbackPropertyStatusIfNeededAsync(application.PropertyId, excludeApplicationId: applicationId); + + await LogTransitionAsync( + "RentalApplication", + applicationId, + oldStatus, + application.Status, + "DenyApplication", + denialReason); + + return WorkflowResult.Ok("Application denied"); + + }); + } + + /// + /// Withdraws an application (initiated by prospect). + /// Rolls back property status if no other pending applications exist. + /// + public async Task WithdrawApplicationAsync(Guid applicationId, string withdrawalReason) + { + return await ExecuteWorkflowAsync(async () => + { + if (string.IsNullOrWhiteSpace(withdrawalReason)) + return WorkflowResult.Fail("Withdrawal reason is required"); + + var application = await GetApplicationAsync(applicationId); + if (application == null) + return WorkflowResult.Fail("Application not found"); + + // Validate in active state + var activeStates = new[] { + ApplicationConstants.ApplicationStatuses.Submitted, + ApplicationConstants.ApplicationStatuses.UnderReview, + ApplicationConstants.ApplicationStatuses.Screening, + ApplicationConstants.ApplicationStatuses.Approved, + ApplicationConstants.ApplicationStatuses.LeaseOffered + }; + + if (!activeStates.Contains(application.Status)) + return WorkflowResult.Fail( + $"Cannot withdraw application in {application.Status} status"); + + var userId = await GetCurrentUserIdAsync(); + var oldStatus = application.Status; + + // Update application + application.Status = ApplicationConstants.ApplicationStatuses.Withdrawn; + application.DenialReason = withdrawalReason; // Reuse field + application.DecidedOn = DateTime.UtcNow; + application.DecisionBy = userId; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.Withdrawn; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + + // Check if property status should roll back (exclude this application which is being withdrawn) + await RollbackPropertyStatusIfNeededAsync(application.PropertyId, excludeApplicationId: applicationId); + + await LogTransitionAsync( + "RentalApplication", + applicationId, + oldStatus, + application.Status, + "WithdrawApplication", + withdrawalReason); + + return WorkflowResult.Ok("Application withdrawn"); + + }); + } + + /// + /// Updates screening results after background/credit checks are completed. + /// Does not automatically approve - requires manual ApproveApplicationAsync call. + /// + public async Task CompleteScreeningAsync( + Guid applicationId, + ScreeningResultModel results) + { + return await ExecuteWorkflowAsync(async () => + { + var application = await GetApplicationAsync(applicationId); + if (application == null) + return WorkflowResult.Fail("Application not found"); + + if (application.Status != ApplicationConstants.ApplicationStatuses.Screening) + return WorkflowResult.Fail( + $"Application must be in Screening status. Current status: {application.Status}"); + + if (application.Screening == null) + return WorkflowResult.Fail("Screening record not found"); + + var userId = await GetCurrentUserIdAsync(); + + // Update screening results + var screening = application.Screening; + + if (results.BackgroundCheckPassed.HasValue) + { + screening.BackgroundCheckPassed = results.BackgroundCheckPassed; + screening.BackgroundCheckCompletedOn = DateTime.UtcNow; + screening.BackgroundCheckNotes = results.BackgroundCheckNotes; + } + + if (results.CreditCheckPassed.HasValue) + { + screening.CreditCheckPassed = results.CreditCheckPassed; + screening.CreditScore = results.CreditScore; + screening.CreditCheckCompletedOn = DateTime.UtcNow; + screening.CreditCheckNotes = results.CreditCheckNotes; + } + + screening.OverallResult = results.OverallResult; + screening.ResultNotes = results.ResultNotes; + screening.LastModifiedBy = userId; + screening.LastModifiedOn = DateTime.UtcNow; + + await LogTransitionAsync( + "ApplicationScreening", + screening.Id, + "Pending", + screening.OverallResult, + "CompleteScreening", + results.ResultNotes); + + return WorkflowResult.Ok("Screening results updated successfully"); + + }); + } + + /// + /// Generates a lease offer for an approved application. + /// Creates LeaseOffer entity, updates property to LeasePending, and denies competing applications. + /// + public async Task> GenerateLeaseOfferAsync( + Guid applicationId, + LeaseOfferModel model) + { + return await ExecuteWorkflowAsync(async () => + { + var application = await GetApplicationAsync(applicationId); + if (application == null) + return WorkflowResult.Fail("Application not found"); + + // Validate application approved + if (application.Status != ApplicationConstants.ApplicationStatuses.Approved) + return WorkflowResult.Fail( + $"Application must be Approved to generate lease offer. Current status: {application.Status}"); + + // Validate property not already leased + var property = application.Property; + if (property == null) + return WorkflowResult.Fail("Property not found"); + + if (property.Status == ApplicationConstants.PropertyStatuses.Occupied) + return WorkflowResult.Fail("Property is already occupied"); + + // Validate lease dates + if (model.StartDate >= model.EndDate) + return WorkflowResult.Fail("End date must be after start date"); + + if (model.StartDate < DateTime.Today) + return WorkflowResult.Fail("Start date cannot be in the past"); + + if (model.MonthlyRent <= 0 || model.SecurityDeposit < 0) + return WorkflowResult.Fail("Invalid rent or deposit amount"); + + var userId = await GetCurrentUserIdAsync(); + var orgId = await GetActiveOrganizationIdAsync(); + + // Create lease offer + var leaseOffer = new LeaseOffer + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + RentalApplicationId = applicationId, + PropertyId = property.Id, + ProspectiveTenantId = application.ProspectiveTenantId, + StartDate = model.StartDate, + EndDate = model.EndDate, + MonthlyRent = model.MonthlyRent, + SecurityDeposit = model.SecurityDeposit, + Terms = model.Terms, + Notes = model.Notes ?? string.Empty, + OfferedOn = DateTime.UtcNow, + ExpiresOn = DateTime.UtcNow.AddDays(30), + Status = "Pending", + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.LeaseOffers.Add(leaseOffer); + // Note: EF Core will assign ID when transaction commits + + // Update application + var oldAppStatus = application.Status; + application.Status = ApplicationConstants.ApplicationStatuses.LeaseOffered; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.LeaseOffered; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + + // Update property to LeasePending + property.Status = ApplicationConstants.PropertyStatuses.LeasePending; + property.LastModifiedBy = userId; + property.LastModifiedOn = DateTime.UtcNow; + + // Deny all competing applications + var competingApps = await _context.RentalApplications + .Where(a => a.PropertyId == property.Id && + a.Id != applicationId && + a.OrganizationId == orgId && + (a.Status == ApplicationConstants.ApplicationStatuses.Submitted || + a.Status == ApplicationConstants.ApplicationStatuses.UnderReview || + a.Status == ApplicationConstants.ApplicationStatuses.Screening || + a.Status == ApplicationConstants.ApplicationStatuses.Approved) && + !a.IsDeleted) + .Include(a => a.ProspectiveTenant) + .ToListAsync(); + + foreach (var competingApp in competingApps) + { + competingApp.Status = ApplicationConstants.ApplicationStatuses.Denied; + competingApp.DenialReason = "Property leased to another applicant"; + competingApp.DecidedOn = DateTime.UtcNow; + competingApp.DecisionBy = userId; + competingApp.LastModifiedBy = userId; + competingApp.LastModifiedOn = DateTime.UtcNow; + + if (competingApp.ProspectiveTenant != null) + { + competingApp.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.Denied; + competingApp.ProspectiveTenant.LastModifiedBy = userId; + competingApp.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + + await LogTransitionAsync( + "RentalApplication", + competingApp.Id, + competingApp.Status, + ApplicationConstants.ApplicationStatuses.Denied, + "DenyCompetingApplication", + "Property leased to another applicant"); + } + + await LogTransitionAsync( + "RentalApplication", + applicationId, + oldAppStatus, + application.Status, + "GenerateLeaseOffer"); + + await LogTransitionAsync( + "LeaseOffer", + leaseOffer.Id, + null, + "Pending", + "GenerateLeaseOffer"); + + return WorkflowResult.Ok( + leaseOffer, + $"Lease offer generated successfully. {competingApps.Count} competing application(s) denied."); + + }); + } + + /// + /// Accepts a lease offer and converts prospect to tenant. + /// Creates Tenant and Lease entities, updates property to Occupied. + /// Records security deposit payment. + /// + public async Task> AcceptLeaseOfferAsync( + Guid leaseOfferId, + string depositPaymentMethod, + DateTime depositPaymentDate, + string? depositReferenceNumber = null, + string? depositNotes = null) + { + return await ExecuteWorkflowAsync(async () => + { + var orgId = await GetActiveOrganizationIdAsync(); + var userId = await GetCurrentUserIdAsync(); + + var leaseOffer = await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .ThenInclude(a => a.ProspectiveTenant) + .Include(lo => lo.Property) + .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && + lo.OrganizationId == orgId && + !lo.IsDeleted); + + if (leaseOffer == null) + return WorkflowResult.Fail("Lease offer not found"); + + if (leaseOffer.Status != "Pending") + return WorkflowResult.Fail($"Lease offer status is {leaseOffer.Status}, not Pending"); + + if (leaseOffer.ExpiresOn < DateTime.UtcNow) + return WorkflowResult.Fail("Lease offer has expired"); + + var prospect = leaseOffer.RentalApplication?.ProspectiveTenant; + if (prospect == null) + return WorkflowResult.Fail("Prospective tenant not found"); + + // Convert prospect to tenant + var tenant = new Tenant + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + FirstName = prospect.FirstName, + LastName = prospect.LastName, + Email = prospect.Email, + PhoneNumber = prospect.Phone, + DateOfBirth = prospect.DateOfBirth, + IdentificationNumber = prospect.IdentificationNumber ?? $"ID-{Guid.NewGuid().ToString("N")[..8]}", + ProspectiveTenantId = prospect.Id, + IsActive = true, + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.Tenants.Add(tenant); + // Note: EF Core will assign ID when transaction commits + + // Create lease + var lease = new Lease + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + PropertyId = leaseOffer.PropertyId, + Tenant = tenant, // Use navigation property instead of TenantId + LeaseOfferId = leaseOffer.Id, + StartDate = leaseOffer.StartDate, + EndDate = leaseOffer.EndDate, + MonthlyRent = leaseOffer.MonthlyRent, + SecurityDeposit = leaseOffer.SecurityDeposit, + Terms = leaseOffer.Terms, + Status = ApplicationConstants.LeaseStatuses.Active, + SignedOn = DateTime.UtcNow, + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.Leases.Add(lease); + // Note: EF Core will assign ID when transaction commits + + // Create security deposit record + var securityDeposit = new SecurityDeposit + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + Lease = lease, // Use navigation property + Tenant = tenant, // Use navigation property + Amount = leaseOffer.SecurityDeposit, + DateReceived = depositPaymentDate, + PaymentMethod = depositPaymentMethod, + TransactionReference = depositReferenceNumber, + Status = "Held", + InInvestmentPool = true, + PoolEntryDate = leaseOffer.StartDate, + Notes = depositNotes, + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.SecurityDeposits.Add(securityDeposit); + + // Update lease offer + leaseOffer.Status = "Accepted"; + leaseOffer.RespondedOn = DateTime.UtcNow; + leaseOffer.ConvertedLeaseId = lease.Id; + leaseOffer.LastModifiedBy = userId; + leaseOffer.LastModifiedOn = DateTime.UtcNow; + + // Update application + var application = leaseOffer.RentalApplication; + if (application != null) + { + application.Status = ApplicationConstants.ApplicationStatuses.LeaseAccepted; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + } + + // Update prospect + prospect.Status = ApplicationConstants.ProspectiveStatuses.ConvertedToTenant; + prospect.LastModifiedBy = userId; + prospect.LastModifiedOn = DateTime.UtcNow; + + // Update property + var property = leaseOffer.Property; + if (property != null) + { + property.Status = ApplicationConstants.PropertyStatuses.Occupied; + property.LastModifiedBy = userId; + property.LastModifiedOn = DateTime.UtcNow; + } + + await LogTransitionAsync( + "LeaseOffer", + leaseOfferId, + "Pending", + "Accepted", + "AcceptLeaseOffer"); + + await LogTransitionAsync( + "ProspectiveTenant", + prospect.Id, + ApplicationConstants.ProspectiveStatuses.LeaseOffered, + ApplicationConstants.ProspectiveStatuses.ConvertedToTenant, + "AcceptLeaseOffer"); + + // Add note if lease start date is in the future + if (leaseOffer.StartDate > DateTime.Today) + { + var noteContent = $"Lease accepted on {DateTime.Today:MMM dd, yyyy}. Lease start date: {leaseOffer.StartDate:MMM dd, yyyy}."; + await _noteService.AddNoteAsync(ApplicationConstants.EntityTypes.Lease, lease.Id, noteContent); + } + + return WorkflowResult.Ok(lease, "Lease offer accepted and tenant created successfully"); + + }); + } + + /// + /// Declines a lease offer. + /// Rolls back property status and marks prospect as lease declined. + /// + public async Task DeclineLeaseOfferAsync(Guid leaseOfferId, string declineReason) + { + return await ExecuteWorkflowAsync(async () => + { + if (string.IsNullOrWhiteSpace(declineReason)) + return WorkflowResult.Fail("Decline reason is required"); + + var orgId = await GetActiveOrganizationIdAsync(); + var userId = await GetCurrentUserIdAsync(); + + var leaseOffer = await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .ThenInclude(a => a.ProspectiveTenant) + .Include(lo => lo.Property) + .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && + lo.OrganizationId == orgId && + !lo.IsDeleted); + + if (leaseOffer == null) + return WorkflowResult.Fail("Lease offer not found"); + + if (leaseOffer.Status != "Pending") + return WorkflowResult.Fail($"Lease offer status is {leaseOffer.Status}, not Pending"); + + // Update lease offer + leaseOffer.Status = "Declined"; + leaseOffer.RespondedOn = DateTime.UtcNow; + leaseOffer.ResponseNotes = declineReason; + leaseOffer.LastModifiedBy = userId; + leaseOffer.LastModifiedOn = DateTime.UtcNow; + + // Update application + var application = leaseOffer.RentalApplication; + if (application != null) + { + application.Status = ApplicationConstants.ApplicationStatuses.LeaseDeclined; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.LeaseDeclined; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + } + + // Rollback property status (exclude this lease offer which is being declined and the application being updated) + await RollbackPropertyStatusIfNeededAsync( + leaseOffer.PropertyId, + excludeApplicationId: application?.Id, + excludeLeaseOfferId: leaseOfferId); + + await LogTransitionAsync( + "LeaseOffer", + leaseOfferId, + "Pending", + "Declined", + "DeclineLeaseOffer", + declineReason); + + return WorkflowResult.Ok("Lease offer declined"); + + }); + } + + /// + /// Expires a lease offer (called by scheduled task). + /// Similar to decline but automated. + /// + public async Task ExpireLeaseOfferAsync(Guid leaseOfferId) + { + return await ExecuteWorkflowAsync(async () => + { + var orgId = await GetActiveOrganizationIdAsync(); + var userId = await GetCurrentUserIdAsync(); + + var leaseOffer = await _context.LeaseOffers + .Include(lo => lo.RentalApplication) + .ThenInclude(a => a.ProspectiveTenant) + .Include(lo => lo.Property) + .FirstOrDefaultAsync(lo => lo.Id == leaseOfferId && + lo.OrganizationId == orgId && + !lo.IsDeleted); + + if (leaseOffer == null) + return WorkflowResult.Fail("Lease offer not found"); + + if (leaseOffer.Status != "Pending") + return WorkflowResult.Fail($"Lease offer status is {leaseOffer.Status}, not Pending"); + + if (leaseOffer.ExpiresOn >= DateTime.UtcNow) + return WorkflowResult.Fail("Lease offer has not expired yet"); + + // Update lease offer + leaseOffer.Status = "Expired"; + leaseOffer.RespondedOn = DateTime.UtcNow; + leaseOffer.LastModifiedBy = userId; + leaseOffer.LastModifiedOn = DateTime.UtcNow; + + // Update application + var application = leaseOffer.RentalApplication; + if (application != null) + { + application.Status = ApplicationConstants.ApplicationStatuses.Expired; + application.LastModifiedBy = userId; + application.LastModifiedOn = DateTime.UtcNow; + + // Update prospect + if (application.ProspectiveTenant != null) + { + application.ProspectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.LeaseDeclined; + application.ProspectiveTenant.LastModifiedBy = userId; + application.ProspectiveTenant.LastModifiedOn = DateTime.UtcNow; + } + } + + // Rollback property status (exclude this lease offer which is expiring and the application being updated) + await RollbackPropertyStatusIfNeededAsync( + leaseOffer.PropertyId, + excludeApplicationId: application?.Id, + excludeLeaseOfferId: leaseOfferId); + + await LogTransitionAsync( + "LeaseOffer", + leaseOfferId, + "Pending", + "Expired", + "ExpireLeaseOffer", + "Offer expired after 30 days"); + + return WorkflowResult.Ok("Lease offer expired"); + + }); + } + + #endregion + + #region Helper Methods + + private async Task GetApplicationAsync(Guid applicationId) + { + var orgId = await GetActiveOrganizationIdAsync(); + return await _context.RentalApplications + .Include(a => a.ProspectiveTenant) + .Include(a => a.Property) + .Include(a => a.Screening) + .FirstOrDefaultAsync(a => + a.Id == applicationId && + a.OrganizationId == orgId && + !a.IsDeleted); + } + + private async Task ValidateApplicationSubmissionAsync( + Guid prospectId, + Guid propertyId) + { + var errors = new List(); + var orgId = await GetActiveOrganizationIdAsync(); + + // Validate prospect exists + var prospect = await _context.ProspectiveTenants + .FirstOrDefaultAsync(p => p.Id == prospectId && p.OrganizationId == orgId && !p.IsDeleted); + + if (prospect == null) + errors.Add("Prospect not found"); + else if (prospect.Status == ApplicationConstants.ProspectiveStatuses.ConvertedToTenant) + errors.Add("Prospect has already been converted to a tenant"); + + // Validate property exists and is available + var property = await _context.Properties + .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == orgId && !p.IsDeleted); + + if (property == null) + errors.Add("Property not found"); + else if (property.Status == ApplicationConstants.PropertyStatuses.Occupied) + errors.Add("Property is currently occupied"); + + // Check for existing active application by identification number and state + // A prospect can have multiple applications over time, but only one "active" (non-disposed) application + if (prospect != null && !string.IsNullOrEmpty(prospect.IdentificationNumber) && !string.IsNullOrEmpty(prospect.IdentificationState)) + { + // Terminal/disposed statuses - application is no longer active + var disposedStatuses = new[] { + ApplicationConstants.ApplicationStatuses.Approved, + ApplicationConstants.ApplicationStatuses.Denied, + ApplicationConstants.ApplicationStatuses.Withdrawn, + ApplicationConstants.ApplicationStatuses.Expired, + ApplicationConstants.ApplicationStatuses.LeaseDeclined, + ApplicationConstants.ApplicationStatuses.LeaseAccepted + }; + + var existingActiveApp = await _context.RentalApplications + .Include(a => a.ProspectiveTenant) + .AnyAsync(a => + a.ProspectiveTenant != null && + a.ProspectiveTenant.IdentificationNumber == prospect.IdentificationNumber && + a.ProspectiveTenant.IdentificationState == prospect.IdentificationState && + a.OrganizationId == orgId && + !disposedStatuses.Contains(a.Status) && + !a.IsDeleted); + + if (existingActiveApp) + errors.Add("An active application already exists for this identification"); + } + + return errors.Any() + ? WorkflowResult.Fail(errors) + : WorkflowResult.Ok(); + } + + /// + /// Checks if property status should roll back when an application is denied/withdrawn. + /// Rolls back to Available if no active applications or pending lease offers remain. + /// + /// The property to check + /// Optional application ID to exclude from the active apps check (for the app being denied/withdrawn) + /// Optional lease offer ID to exclude from the pending offers check (for the offer being declined) + private async Task RollbackPropertyStatusIfNeededAsync( + Guid propertyId, + Guid? excludeApplicationId = null, + Guid? excludeLeaseOfferId = null) + { + var orgId = await GetActiveOrganizationIdAsync(); + var userId = await GetCurrentUserIdAsync(); + + // Get all active applications for this property + var activeStates = new[] { + ApplicationConstants.ApplicationStatuses.Submitted, + ApplicationConstants.ApplicationStatuses.UnderReview, + ApplicationConstants.ApplicationStatuses.Screening, + ApplicationConstants.ApplicationStatuses.Approved, + ApplicationConstants.ApplicationStatuses.LeaseOffered + }; + + var hasActiveApplications = await _context.RentalApplications + .AnyAsync(a => + a.PropertyId == propertyId && + a.OrganizationId == orgId && + activeStates.Contains(a.Status) && + (excludeApplicationId == null || a.Id != excludeApplicationId) && + !a.IsDeleted); + + // Also check for pending lease offers + var hasPendingLeaseOffers = await _context.LeaseOffers + .AnyAsync(lo => + lo.PropertyId == propertyId && + lo.OrganizationId == orgId && + lo.Status == "Pending" && + (excludeLeaseOfferId == null || lo.Id != excludeLeaseOfferId) && + !lo.IsDeleted); + + // If no active applications or pending lease offers remain, roll back property to Available + if (!hasActiveApplications && !hasPendingLeaseOffers) + { + var property = await _context.Properties + .FirstOrDefaultAsync(p => p.Id == propertyId && p.OrganizationId == orgId); + + if (property != null && + (property.Status == ApplicationConstants.PropertyStatuses.ApplicationPending || + property.Status == ApplicationConstants.PropertyStatuses.LeasePending)) + { + property.Status = ApplicationConstants.PropertyStatuses.Available; + property.LastModifiedBy = userId; + property.LastModifiedOn = DateTime.UtcNow; + } + } + } + + #endregion + + /// + /// Returns a comprehensive view of the application's workflow state, + /// including related prospect, property, screening, lease offers, and audit history. + /// + public async Task GetApplicationWorkflowStateAsync(Guid applicationId) + { + var orgId = await GetActiveOrganizationIdAsync(); + + var application = await _context.RentalApplications + .Include(a => a.ProspectiveTenant) + .Include(a => a.Property) + .Include(a => a.Screening) + .FirstOrDefaultAsync(a => a.Id == applicationId && a.OrganizationId == orgId && !a.IsDeleted); + + if (application == null) + return new ApplicationWorkflowState + { + Application = null, + AuditHistory = new List(), + LeaseOffers = new List() + }; + + var leaseOffers = await _context.LeaseOffers + .Where(lo => lo.RentalApplicationId == applicationId && lo.OrganizationId == orgId && !lo.IsDeleted) + .OrderByDescending(lo => lo.OfferedOn) + .ToListAsync(); + + var auditHistory = await _context.WorkflowAuditLogs + .Where(w => w.EntityType == "RentalApplication" && w.EntityId == applicationId && w.OrganizationId == orgId) + .OrderByDescending(w => w.PerformedOn) + .ToListAsync(); + + return new ApplicationWorkflowState + { + Application = application, + Prospect = application.ProspectiveTenant, + Property = application.Property, + Screening = application.Screening, + LeaseOffers = leaseOffers, + AuditHistory = auditHistory + }; + } + } + + /// + /// Model for application submission data. + /// + public class ApplicationSubmissionModel + { + public decimal ApplicationFee { get; set; } + public bool ApplicationFeePaid { get; set; } + public string? ApplicationFeePaymentMethod { get; set; } + + public string CurrentAddress { get; set; } = string.Empty; + public string CurrentCity { get; set; } = string.Empty; + public string CurrentState { get; set; } = string.Empty; + public string CurrentZipCode { get; set; } = string.Empty; + public decimal CurrentRent { get; set; } + public string LandlordName { get; set; } = string.Empty; + public string LandlordPhone { get; set; } = string.Empty; + + public string EmployerName { get; set; } = string.Empty; + public string JobTitle { get; set; } = string.Empty; + public decimal MonthlyIncome { get; set; } + public int EmploymentLengthMonths { get; set; } + + public string Reference1Name { get; set; } = string.Empty; + public string Reference1Phone { get; set; } = string.Empty; + public string Reference1Relationship { get; set; } = string.Empty; + public string? Reference2Name { get; set; } + public string? Reference2Phone { get; set; } + public string? Reference2Relationship { get; set; } + } + + /// + /// Model for screening results update. + /// + public class ScreeningResultModel + { + public bool? BackgroundCheckPassed { get; set; } + public string? BackgroundCheckNotes { get; set; } + + public bool? CreditCheckPassed { get; set; } + public int? CreditScore { get; set; } + public string? CreditCheckNotes { get; set; } + + public string OverallResult { get; set; } = "Pending"; // Pending, Passed, Failed, ConditionalPass + public string? ResultNotes { get; set; } + } + + /// + /// Model for lease offer generation. + /// + public class LeaseOfferModel + { + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public decimal MonthlyRent { get; set; } + public decimal SecurityDeposit { get; set; } + public string Terms { get; set; } = string.Empty; + public string? Notes { get; set; } + } + + /// + /// Aggregated workflow state returned by GetApplicationWorkflowStateAsync. + /// + public class ApplicationWorkflowState + { + public RentalApplication? Application { get; set; } + public ProspectiveTenant? Prospect { get; set; } + public Property? Property { get; set; } + public ApplicationScreening? Screening { get; set; } + public List LeaseOffers { get; set; } = new(); + public List AuditHistory { get; set; } = new(); + } +} diff --git a/Aquiis.Professional/Application/Services/Workflows/BaseWorkflowService.cs b/Aquiis.Professional/Application/Services/Workflows/BaseWorkflowService.cs new file mode 100644 index 0000000..bdd47df --- /dev/null +++ b/Aquiis.Professional/Application/Services/Workflows/BaseWorkflowService.cs @@ -0,0 +1,208 @@ +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace Aquiis.Professional.Application.Services.Workflows +{ + /// + /// Abstract base class for all workflow services. + /// Provides transaction support, audit logging, and validation infrastructure. + /// + public abstract class BaseWorkflowService + { + protected readonly ApplicationDbContext _context; + protected readonly UserContextService _userContext; + + protected BaseWorkflowService( + ApplicationDbContext context, + UserContextService userContext) + { + _context = context; + _userContext = userContext; + } + + /// + /// Executes a workflow operation within a database transaction. + /// Automatically commits on success or rolls back on failure. + /// + protected async Task> ExecuteWorkflowAsync( + Func>> workflowOperation) + { + using var transaction = await _context.Database.BeginTransactionAsync(); + + try + { + var result = await workflowOperation(); + + if (result.Success) + { + await _context.SaveChangesAsync(); + await transaction.CommitAsync(); + } + else + { + await transaction.RollbackAsync(); + // Clear the ChangeTracker to discard all tracked changes + _context.ChangeTracker.Clear(); + } + + return result; + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + // Clear the ChangeTracker to discard all tracked changes + _context.ChangeTracker.Clear(); + + var errorMessage = ex.Message; + if (ex.InnerException != null) + { + errorMessage += $" | Inner: {ex.InnerException.Message}"; + if (ex.InnerException.InnerException != null) + { + errorMessage += $" | Inner(2): {ex.InnerException.InnerException.Message}"; + } + } + Console.WriteLine($"Workflow Error: {errorMessage}"); + Console.WriteLine($"Stack Trace: {ex.StackTrace}"); + return WorkflowResult.Fail($"Workflow operation failed: {errorMessage}"); + } + } + + /// + /// Executes a workflow operation within a database transaction (non-generic version). + /// + protected async Task ExecuteWorkflowAsync( + Func> workflowOperation) + { + using var transaction = await _context.Database.BeginTransactionAsync(); + + try + { + var result = await workflowOperation(); + + if (result.Success) + { + await _context.SaveChangesAsync(); + await transaction.CommitAsync(); + } + else + { + await transaction.RollbackAsync(); + // Clear the ChangeTracker to discard all tracked changes + _context.ChangeTracker.Clear(); + } + + return result; + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + // Clear the ChangeTracker to discard all tracked changes + _context.ChangeTracker.Clear(); + + var errorMessage = ex.Message; + if (ex.InnerException != null) + { + errorMessage += $" | Inner: {ex.InnerException.Message}"; + if (ex.InnerException.InnerException != null) + { + errorMessage += $" | Inner(2): {ex.InnerException.InnerException.Message}"; + } + } + Console.WriteLine($"Workflow Error: {errorMessage}"); + Console.WriteLine($"Stack Trace: {ex.StackTrace}"); + return WorkflowResult.Fail($"Workflow operation failed: {errorMessage}"); + } + } + + /// + /// Logs a workflow state transition to the audit log. + /// + protected async Task LogTransitionAsync( + string entityType, + Guid entityId, + string? fromStatus, + string toStatus, + string action, + string? reason = null, + Dictionary? metadata = null) + { + var userId = await _userContext.GetUserIdAsync() ?? string.Empty; + var activeOrgId = await _userContext.GetActiveOrganizationIdAsync(); + + var auditLog = new WorkflowAuditLog + { + Id = Guid.NewGuid(), + EntityType = entityType, + EntityId = entityId, + FromStatus = fromStatus, + ToStatus = toStatus, + Action = action, + Reason = reason, + PerformedBy = userId, + PerformedOn = DateTime.UtcNow, + OrganizationId = activeOrgId.HasValue ? activeOrgId.Value : Guid.Empty, + Metadata = metadata != null ? JsonSerializer.Serialize(metadata) : null, + CreatedOn = DateTime.UtcNow, + CreatedBy = userId + }; + + _context.WorkflowAuditLogs.Add(auditLog); + // Note: SaveChangesAsync is called by ExecuteWorkflowAsync + } + + /// + /// Gets the complete audit history for an entity. + /// + public async Task> GetAuditHistoryAsync( + string entityType, + Guid entityId) + { + var activeOrgId = await _userContext.GetActiveOrganizationIdAsync(); + + return await _context.WorkflowAuditLogs + .Where(w => w.EntityType == entityType && w.EntityId == entityId) + .Where(w => w.OrganizationId == activeOrgId) + .OrderBy(w => w.PerformedOn) + .ToListAsync(); + } + + /// + /// Validates that an entity belongs to the active organization. + /// + protected async Task ValidateOrganizationOwnershipAsync( + IQueryable query, + Guid entityId) where TEntity : class + { + var activeOrgId = await _userContext.GetActiveOrganizationIdAsync(); + + // This assumes entities have OrganizationId property + // Override in derived classes if different validation needed + var entity = await query + .Where(e => EF.Property(e, "Id") == entityId) + .Where(e => EF.Property(e, "OrganizationId") == activeOrgId) + .Where(e => EF.Property(e, "IsDeleted") == false) + .FirstOrDefaultAsync(); + + return entity != null; + } + + /// + /// Gets the current user ID from the user context. + /// + protected async Task GetCurrentUserIdAsync() + { + return await _userContext.GetUserIdAsync() ?? string.Empty; + } + + /// + /// Gets the active organization ID from the user context. + /// + protected async Task GetActiveOrganizationIdAsync() + { + return await _userContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + } + } +} diff --git a/Aquiis.Professional/Application/Services/Workflows/IWorkflowState.cs b/Aquiis.Professional/Application/Services/Workflows/IWorkflowState.cs new file mode 100644 index 0000000..e8370ae --- /dev/null +++ b/Aquiis.Professional/Application/Services/Workflows/IWorkflowState.cs @@ -0,0 +1,32 @@ +namespace Aquiis.Professional.Application.Services.Workflows +{ + /// + /// Interface for implementing state machines that validate workflow transitions. + /// + /// Enum type representing workflow statuses + public interface IWorkflowState where TStatus : Enum + { + /// + /// Validates if a transition from one status to another is allowed. + /// + /// Current status (can be null for initial creation) + /// Target status + /// True if transition is valid + bool IsValidTransition(TStatus fromStatus, TStatus toStatus); + + /// + /// Gets all valid next statuses from the current status. + /// + /// Current status + /// List of valid next statuses + List GetValidNextStates(TStatus currentStatus); + + /// + /// Gets a human-readable reason why a transition is invalid. + /// + /// Current status + /// Target status + /// Error message explaining why transition is invalid + string GetInvalidTransitionReason(TStatus fromStatus, TStatus toStatus); + } +} diff --git a/Aquiis.Professional/Application/Services/Workflows/LeaseWorkflowService.cs b/Aquiis.Professional/Application/Services/Workflows/LeaseWorkflowService.cs new file mode 100644 index 0000000..3871d49 --- /dev/null +++ b/Aquiis.Professional/Application/Services/Workflows/LeaseWorkflowService.cs @@ -0,0 +1,848 @@ +using Aquiis.Professional.Application.Services.Workflows; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Professional.Application.Services.Workflows +{ + /// + /// Lease status enumeration for state machine validation. + /// + public enum LeaseStatus + { + Pending, + Active, + Renewed, + MonthToMonth, + NoticeGiven, + Expired, + Terminated + } + + /// + /// Workflow service for lease lifecycle management. + /// Handles lease activation, renewals, termination notices, and move-out workflows. + /// + public class LeaseWorkflowService : BaseWorkflowService, IWorkflowState + { + private readonly NoteService _noteService; + + public LeaseWorkflowService( + ApplicationDbContext context, + UserContextService userContext, + NoteService noteService) + : base(context, userContext) + { + _noteService = noteService; + } + + #region State Machine Implementation + + public bool IsValidTransition(LeaseStatus fromStatus, LeaseStatus toStatus) + { + var validTransitions = GetValidNextStates(fromStatus); + return validTransitions.Contains(toStatus); + } + + public List GetValidNextStates(LeaseStatus currentStatus) + { + return currentStatus switch + { + LeaseStatus.Pending => new() + { + LeaseStatus.Active, + LeaseStatus.Terminated // Can cancel before activation + }, + LeaseStatus.Active => new() + { + LeaseStatus.Renewed, + LeaseStatus.MonthToMonth, + LeaseStatus.NoticeGiven, + LeaseStatus.Expired, + LeaseStatus.Terminated + }, + LeaseStatus.Renewed => new() + { + LeaseStatus.Active, // New term starts + LeaseStatus.NoticeGiven, + LeaseStatus.Terminated + }, + LeaseStatus.MonthToMonth => new() + { + LeaseStatus.NoticeGiven, + LeaseStatus.Renewed, // Sign new fixed-term lease + LeaseStatus.Terminated + }, + LeaseStatus.NoticeGiven => new() + { + LeaseStatus.Expired, // Notice period ends naturally + LeaseStatus.Terminated // Early termination + }, + _ => new List() // Terminal states have no valid transitions + }; + } + + public string GetInvalidTransitionReason(LeaseStatus fromStatus, LeaseStatus toStatus) + { + var validStates = GetValidNextStates(fromStatus); + return $"Cannot transition from {fromStatus} to {toStatus}. Valid next states: {string.Join(", ", validStates)}"; + } + + #endregion + + #region Core Workflow Methods + + /// + /// Activates a pending lease when all conditions are met (deposit paid, documents signed). + /// Updates property status to Occupied. + /// + public async Task ActivateLeaseAsync(Guid leaseId, DateTime? moveInDate = null) + { + return await ExecuteWorkflowAsync(async () => + { + var lease = await GetLeaseAsync(leaseId); + if (lease == null) + return WorkflowResult.Fail("Lease not found"); + + if (lease.Status != ApplicationConstants.LeaseStatuses.Pending) + return WorkflowResult.Fail( + $"Lease must be in Pending status to activate. Current status: {lease.Status}"); + + // Validate start date is not too far in the future + if (lease.StartDate > DateTime.Today.AddDays(30)) + return WorkflowResult.Fail( + "Cannot activate lease more than 30 days before start date"); + + var userId = await GetCurrentUserIdAsync(); + var orgId = await GetActiveOrganizationIdAsync(); + var oldStatus = lease.Status; + + // Update lease + lease.Status = ApplicationConstants.LeaseStatuses.Active; + lease.SignedOn = moveInDate ?? DateTime.Today; + lease.LastModifiedBy = userId; + lease.LastModifiedOn = DateTime.UtcNow; + + // Update property status + if (lease.Property != null) + { + lease.Property.Status = ApplicationConstants.PropertyStatuses.Occupied; + lease.Property.LastModifiedBy = userId; + lease.Property.LastModifiedOn = DateTime.UtcNow; + } + + // Update tenant status to active + if (lease.Tenant != null) + { + lease.Tenant.IsActive = true; + lease.Tenant.LastModifiedBy = userId; + lease.Tenant.LastModifiedOn = DateTime.UtcNow; + } + + await LogTransitionAsync( + "Lease", + leaseId, + oldStatus, + lease.Status, + "ActivateLease"); + + return WorkflowResult.Ok("Lease activated successfully"); + }); + } + + /// + /// Records a termination notice from tenant or landlord. + /// Sets expected move-out date and changes lease status. + /// + public async Task RecordTerminationNoticeAsync( + Guid leaseId, + DateTime noticeDate, + DateTime expectedMoveOutDate, + string noticeType, // "Tenant", "Landlord", "Mutual" + string reason) + { + return await ExecuteWorkflowAsync(async () => + { + var lease = await GetLeaseAsync(leaseId); + if (lease == null) + return WorkflowResult.Fail("Lease not found"); + + var activeStatuses = new[] { + ApplicationConstants.LeaseStatuses.Active, + ApplicationConstants.LeaseStatuses.MonthToMonth, + ApplicationConstants.LeaseStatuses.Renewed + }; + + if (!activeStatuses.Contains(lease.Status)) + return WorkflowResult.Fail( + $"Can only record termination notice for active leases. Current status: {lease.Status}"); + + if (expectedMoveOutDate <= DateTime.Today) + return WorkflowResult.Fail("Expected move-out date must be in the future"); + + if (string.IsNullOrWhiteSpace(reason)) + return WorkflowResult.Fail("Termination notice reason is required"); + + var userId = await GetCurrentUserIdAsync(); + var oldStatus = lease.Status; + + // Update lease + lease.Status = ApplicationConstants.LeaseStatuses.NoticeGiven; + lease.TerminationNoticedOn = noticeDate; + lease.ExpectedMoveOutDate = expectedMoveOutDate; + lease.TerminationReason = $"[{noticeType}] {reason}"; + lease.LastModifiedBy = userId; + lease.LastModifiedOn = DateTime.UtcNow; + + // Add note for audit trail + var noteContent = $"Termination notice recorded. Type: {noticeType}. Expected move-out: {expectedMoveOutDate:MMM dd, yyyy}. Reason: {reason}"; + await _noteService.AddNoteAsync(ApplicationConstants.EntityTypes.Lease, leaseId, noteContent); + + await LogTransitionAsync( + "Lease", + leaseId, + oldStatus, + lease.Status, + "RecordTerminationNotice", + reason); + + return WorkflowResult.Ok($"Termination notice recorded. Move-out date: {expectedMoveOutDate:MMM dd, yyyy}"); + }); + } + + /// + /// Converts an active fixed-term lease to month-to-month when term expires + /// without renewal. + /// + public async Task ConvertToMonthToMonthAsync(Guid leaseId, decimal? newMonthlyRent = null) + { + return await ExecuteWorkflowAsync(async () => + { + var lease = await GetLeaseAsync(leaseId); + if (lease == null) + return WorkflowResult.Fail("Lease not found"); + + var validStatuses = new[] { + ApplicationConstants.LeaseStatuses.Active, + ApplicationConstants.LeaseStatuses.Expired + }; + + if (!validStatuses.Contains(lease.Status)) + return WorkflowResult.Fail( + $"Can only convert to month-to-month from Active or Expired status. Current status: {lease.Status}"); + + var userId = await GetCurrentUserIdAsync(); + var oldStatus = lease.Status; + + // Update lease + lease.Status = ApplicationConstants.LeaseStatuses.MonthToMonth; + if (newMonthlyRent.HasValue && newMonthlyRent > 0) + { + lease.MonthlyRent = newMonthlyRent.Value; + } + lease.LastModifiedBy = userId; + lease.LastModifiedOn = DateTime.UtcNow; + + await LogTransitionAsync( + "Lease", + leaseId, + oldStatus, + lease.Status, + "ConvertToMonthToMonth"); + + return WorkflowResult.Ok("Lease converted to month-to-month successfully"); + }); + } + + /// + /// Creates a lease renewal (extends existing lease with new terms). + /// Option to update rent, deposit, and end date. + /// + public async Task> RenewLeaseAsync( + Guid leaseId, + LeaseRenewalModel model) + { + return await ExecuteWorkflowAsync(async () => + { + var existingLease = await GetLeaseAsync(leaseId); + if (existingLease == null) + return WorkflowResult.Fail("Lease not found"); + + var renewableStatuses = new[] { + ApplicationConstants.LeaseStatuses.Active, + ApplicationConstants.LeaseStatuses.MonthToMonth, + ApplicationConstants.LeaseStatuses.NoticeGiven // Can be cancelled with renewal + }; + + if (!renewableStatuses.Contains(existingLease.Status)) + return WorkflowResult.Fail( + $"Lease must be in an active state to renew. Current status: {existingLease.Status}"); + + // Validate renewal terms + if (model.NewEndDate <= existingLease.EndDate) + return WorkflowResult.Fail("New end date must be after current end date"); + + if (model.NewMonthlyRent <= 0) + return WorkflowResult.Fail("Monthly rent must be greater than zero"); + + var userId = await GetCurrentUserIdAsync(); + var orgId = await GetActiveOrganizationIdAsync(); + var oldStatus = existingLease.Status; + + // Create renewal record (new lease linked to existing) + var renewalLease = new Lease + { + Id = Guid.NewGuid(), + OrganizationId = orgId, + PropertyId = existingLease.PropertyId, + TenantId = existingLease.TenantId, + PreviousLeaseId = existingLease.Id, // Link to previous lease + StartDate = model.NewStartDate ?? existingLease.EndDate.AddDays(1), + EndDate = model.NewEndDate, + MonthlyRent = model.NewMonthlyRent, + SecurityDeposit = model.UpdatedSecurityDeposit ?? existingLease.SecurityDeposit, + Terms = model.NewTerms ?? existingLease.Terms, + Status = ApplicationConstants.LeaseStatuses.Active, + SignedOn = DateTime.Today, + RenewalNumber = existingLease.RenewalNumber + 1, + CreatedBy = userId, + CreatedOn = DateTime.UtcNow + }; + + _context.Leases.Add(renewalLease); + + // Update existing lease status + existingLease.Status = ApplicationConstants.LeaseStatuses.Renewed; + existingLease.LastModifiedBy = userId; + existingLease.LastModifiedOn = DateTime.UtcNow; + + // Log transitions + await LogTransitionAsync( + "Lease", + existingLease.Id, + oldStatus, + existingLease.Status, + "RenewLease"); + + await LogTransitionAsync( + "Lease", + renewalLease.Id, + null, + renewalLease.Status, + "CreateRenewal"); + + // Add note about renewal + var noteContent = $"Lease renewed. New term: {renewalLease.StartDate:MMM dd, yyyy} - {renewalLease.EndDate:MMM dd, yyyy}. Rent: ${renewalLease.MonthlyRent:N2}/month."; + await _noteService.AddNoteAsync(ApplicationConstants.EntityTypes.Lease, renewalLease.Id, noteContent); + + return WorkflowResult.Ok( + renewalLease, + "Lease renewed successfully"); + }); + } + + /// + /// Completes the move-out process after tenant vacates. + /// Updates property to Available status. + /// + public async Task CompleteMoveOutAsync( + Guid leaseId, + DateTime actualMoveOutDate, + MoveOutModel? model = null) + { + return await ExecuteWorkflowAsync(async () => + { + var lease = await GetLeaseAsync(leaseId); + if (lease == null) + return WorkflowResult.Fail("Lease not found"); + + var moveOutStatuses = new[] { + ApplicationConstants.LeaseStatuses.NoticeGiven, + ApplicationConstants.LeaseStatuses.Expired, + ApplicationConstants.LeaseStatuses.Active // Emergency move-out + }; + + if (!moveOutStatuses.Contains(lease.Status)) + return WorkflowResult.Fail( + $"Cannot complete move-out for lease in {lease.Status} status"); + + var userId = await GetCurrentUserIdAsync(); + var orgId = await GetActiveOrganizationIdAsync(); + var oldStatus = lease.Status; + + // Update lease + lease.Status = ApplicationConstants.LeaseStatuses.Terminated; + lease.ActualMoveOutDate = actualMoveOutDate; + lease.LastModifiedBy = userId; + lease.LastModifiedOn = DateTime.UtcNow; + + // Update property status to Available (ready for new tenant) + if (lease.Property != null) + { + lease.Property.Status = ApplicationConstants.PropertyStatuses.Available; + lease.Property.LastModifiedBy = userId; + lease.Property.LastModifiedOn = DateTime.UtcNow; + } + + // Deactivate tenant if no other active leases + if (lease.Tenant != null) + { + var hasOtherActiveLeases = await _context.Leases + .AnyAsync(l => l.TenantId == lease.TenantId && + l.Id != leaseId && + l.OrganizationId == orgId && + (l.Status == ApplicationConstants.LeaseStatuses.Active || + l.Status == ApplicationConstants.LeaseStatuses.MonthToMonth) && + !l.IsDeleted); + + if (!hasOtherActiveLeases) + { + lease.Tenant.IsActive = false; + lease.Tenant.LastModifiedBy = userId; + lease.Tenant.LastModifiedOn = DateTime.UtcNow; + } + } + + await LogTransitionAsync( + "Lease", + leaseId, + oldStatus, + lease.Status, + "CompleteMoveOut", + model?.Notes); + + // Add note with move-out details + var noteContent = $"Move-out completed on {actualMoveOutDate:MMM dd, yyyy}."; + if (model?.FinalInspectionCompleted == true) + noteContent += " Final inspection completed."; + if (model?.KeysReturned == true) + noteContent += " Keys returned."; + if (!string.IsNullOrWhiteSpace(model?.Notes)) + noteContent += $" Notes: {model.Notes}"; + + await _noteService.AddNoteAsync(ApplicationConstants.EntityTypes.Lease, leaseId, noteContent); + + return WorkflowResult.Ok("Move-out completed successfully"); + }); + } + + /// + /// Early terminates a lease (eviction, breach, mutual agreement). + /// + public async Task EarlyTerminateAsync( + Guid leaseId, + string terminationType, // "Eviction", "Breach", "Mutual", "Emergency" + string reason, + DateTime effectiveDate) + { + return await ExecuteWorkflowAsync(async () => + { + var lease = await GetLeaseAsync(leaseId); + if (lease == null) + return WorkflowResult.Fail("Lease not found"); + + var terminableStatuses = new[] { + ApplicationConstants.LeaseStatuses.Active, + ApplicationConstants.LeaseStatuses.MonthToMonth, + ApplicationConstants.LeaseStatuses.NoticeGiven, + ApplicationConstants.LeaseStatuses.Pending + }; + + if (!terminableStatuses.Contains(lease.Status)) + return WorkflowResult.Fail( + $"Cannot terminate lease in {lease.Status} status"); + + if (string.IsNullOrWhiteSpace(reason)) + return WorkflowResult.Fail("Termination reason is required"); + + var userId = await GetCurrentUserIdAsync(); + var oldStatus = lease.Status; + + // Update lease + lease.Status = ApplicationConstants.LeaseStatuses.Terminated; + lease.TerminationReason = $"[{terminationType}] {reason}"; + lease.ActualMoveOutDate = effectiveDate; + lease.LastModifiedBy = userId; + lease.LastModifiedOn = DateTime.UtcNow; + + // Update property status + if (lease.Property != null && effectiveDate <= DateTime.Today) + { + lease.Property.Status = ApplicationConstants.PropertyStatuses.Available; + lease.Property.LastModifiedBy = userId; + lease.Property.LastModifiedOn = DateTime.UtcNow; + } + + // Deactivate tenant if no other active leases + if (lease.Tenant != null) + { + var orgId = await GetActiveOrganizationIdAsync(); + var hasOtherActiveLeases = await _context.Leases + .AnyAsync(l => l.TenantId == lease.TenantId && + l.Id != leaseId && + l.OrganizationId == orgId && + (l.Status == ApplicationConstants.LeaseStatuses.Active || + l.Status == ApplicationConstants.LeaseStatuses.MonthToMonth) && + !l.IsDeleted); + + if (!hasOtherActiveLeases) + { + lease.Tenant.IsActive = false; + lease.Tenant.LastModifiedBy = userId; + lease.Tenant.LastModifiedOn = DateTime.UtcNow; + } + } + + await LogTransitionAsync( + "Lease", + leaseId, + oldStatus, + lease.Status, + "EarlyTerminate", + $"[{terminationType}] {reason}"); + + return WorkflowResult.Ok($"Lease terminated ({terminationType})"); + }); + } + + /// + /// Expires leases that have passed their end date without renewal. + /// Called by ScheduledTaskService. + /// + public async Task> ExpireOverdueLeaseAsync() + { + return await ExecuteWorkflowAsync(async () => + { + var orgId = await GetActiveOrganizationIdAsync(); + var userId = await GetCurrentUserIdAsync(); + + // Find active leases past their end date + var expiredLeases = await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.OrganizationId == orgId && + l.Status == ApplicationConstants.LeaseStatuses.Active && + l.EndDate < DateTime.Today && + !l.IsDeleted) + .ToListAsync(); + + var count = 0; + foreach (var lease in expiredLeases) + { + var oldStatus = lease.Status; + lease.Status = ApplicationConstants.LeaseStatuses.Expired; + lease.LastModifiedBy = userId; + lease.LastModifiedOn = DateTime.UtcNow; + + await LogTransitionAsync( + "Lease", + lease.Id, + oldStatus, + lease.Status, + "AutoExpire", + "Lease end date passed without renewal"); + + count++; + } + + return WorkflowResult.Ok(count, $"{count} lease(s) expired"); + }); + } + + #endregion + + #region Security Deposit Workflow Methods + + /// + /// Initiates security deposit settlement at end of lease. + /// Calculates deductions and remaining refund amount. + /// + public async Task> InitiateDepositSettlementAsync( + Guid leaseId, + List deductions) + { + return await ExecuteWorkflowAsync(async () => + { + var lease = await GetLeaseAsync(leaseId); + if (lease == null) + return WorkflowResult.Fail("Lease not found"); + + var settlementStatuses = new[] { + ApplicationConstants.LeaseStatuses.NoticeGiven, + ApplicationConstants.LeaseStatuses.Expired, + ApplicationConstants.LeaseStatuses.Terminated + }; + + if (!settlementStatuses.Contains(lease.Status)) + return WorkflowResult.Fail( + "Can only settle deposit for leases in termination status"); + + var orgId = await GetActiveOrganizationIdAsync(); + + // Get security deposit record + var deposit = await _context.SecurityDeposits + .FirstOrDefaultAsync(sd => sd.LeaseId == leaseId && + sd.OrganizationId == orgId && + !sd.IsDeleted); + + if (deposit == null) + return WorkflowResult.Fail("Security deposit record not found"); + + if (deposit.Status == "Returned") + return WorkflowResult.Fail("Security deposit has already been settled"); + + // Calculate settlement + var totalDeductions = deductions.Sum(d => d.Amount); + var refundAmount = deposit.Amount - totalDeductions; + + var settlement = new SecurityDepositSettlement + { + LeaseId = leaseId, + TenantId = lease.TenantId, + OriginalAmount = deposit.Amount, + TotalDeductions = totalDeductions, + RefundAmount = Math.Max(0, refundAmount), + AmountOwed = Math.Max(0, -refundAmount), // If negative, tenant owes money + Deductions = deductions, + SettlementDate = DateTime.Today + }; + + // Update deposit record status + var userId = await GetCurrentUserIdAsync(); + deposit.Status = refundAmount > 0 ? "Pending Return" : "Forfeited"; + deposit.LastModifiedBy = userId; + deposit.LastModifiedOn = DateTime.UtcNow; + + return WorkflowResult.Ok( + settlement, + $"Deposit settlement calculated. Refund amount: ${refundAmount:N2}"); + }); + } + + /// + /// Records the security deposit refund payment. + /// + public async Task RecordDepositRefundAsync( + Guid leaseId, + decimal refundAmount, + string paymentMethod, + string? referenceNumber = null) + { + return await ExecuteWorkflowAsync(async () => + { + var orgId = await GetActiveOrganizationIdAsync(); + + var deposit = await _context.SecurityDeposits + .FirstOrDefaultAsync(sd => sd.LeaseId == leaseId && + sd.OrganizationId == orgId && + !sd.IsDeleted); + + if (deposit == null) + return WorkflowResult.Fail("Security deposit record not found"); + + if (deposit.Status == "Returned") + return WorkflowResult.Fail("Deposit has already been returned"); + + var userId = await GetCurrentUserIdAsync(); + + deposit.Status = "Refunded"; + deposit.RefundProcessedDate = DateTime.Today; + deposit.RefundAmount = refundAmount; + deposit.RefundMethod = paymentMethod; + deposit.RefundReference = referenceNumber; + deposit.Notes = $"Refund: ${refundAmount:N2} via {paymentMethod}. Ref: {referenceNumber ?? "N/A"}"; + deposit.LastModifiedBy = userId; + deposit.LastModifiedOn = DateTime.UtcNow; + + await LogTransitionAsync( + "SecurityDeposit", + deposit.Id, + "Pending Return", + "Refunded", + "RecordDepositRefund", + $"Refunded ${refundAmount:N2}"); + + return WorkflowResult.Ok("Security deposit refund recorded"); + }); + } + + #endregion + + #region Query Methods + + /// + /// Returns a comprehensive view of the lease's workflow state, + /// including tenant, property, security deposit, and audit history. + /// + public async Task GetLeaseWorkflowStateAsync(Guid leaseId) + { + var orgId = await GetActiveOrganizationIdAsync(); + + var lease = await _context.Leases + .Include(l => l.Tenant) + .Include(l => l.Property) + .FirstOrDefaultAsync(l => l.Id == leaseId && l.OrganizationId == orgId && !l.IsDeleted); + + if (lease == null) + return new LeaseWorkflowState + { + Lease = null, + AuditHistory = new List() + }; + + var securityDeposit = await _context.SecurityDeposits + .FirstOrDefaultAsync(sd => sd.LeaseId == leaseId && sd.OrganizationId == orgId && !sd.IsDeleted); + + var renewals = await _context.Leases + .Where(l => l.PreviousLeaseId == leaseId && l.OrganizationId == orgId && !l.IsDeleted) + .OrderByDescending(l => l.StartDate) + .ToListAsync(); + + var auditHistory = await _context.WorkflowAuditLogs + .Where(w => w.EntityType == "Lease" && w.EntityId == leaseId && w.OrganizationId == orgId) + .OrderByDescending(w => w.PerformedOn) + .ToListAsync(); + + return new LeaseWorkflowState + { + Lease = lease, + Tenant = lease.Tenant, + Property = lease.Property, + SecurityDeposit = securityDeposit, + Renewals = renewals, + AuditHistory = auditHistory, + DaysUntilExpiration = (lease.EndDate - DateTime.Today).Days, + IsExpiring = (lease.EndDate - DateTime.Today).Days <= 60, + CanRenew = lease.Status == ApplicationConstants.LeaseStatuses.Active || + lease.Status == ApplicationConstants.LeaseStatuses.MonthToMonth, + CanTerminate = lease.Status != ApplicationConstants.LeaseStatuses.Terminated && + lease.Status != ApplicationConstants.LeaseStatuses.Expired + }; + } + + /// + /// Gets leases that are expiring within the specified number of days. + /// + public async Task> GetExpiringLeasesAsync(int withinDays = 60) + { + var orgId = await GetActiveOrganizationIdAsync(); + var cutoffDate = DateTime.Today.AddDays(withinDays); + + return await _context.Leases + .Include(l => l.Tenant) + .Include(l => l.Property) + .Where(l => l.OrganizationId == orgId && + l.Status == ApplicationConstants.LeaseStatuses.Active && + l.EndDate <= cutoffDate && + l.EndDate >= DateTime.Today && + !l.IsDeleted) + .OrderBy(l => l.EndDate) + .ToListAsync(); + } + + /// + /// Gets all leases with termination notices. + /// + public async Task> GetLeasesWithNoticeAsync() + { + var orgId = await GetActiveOrganizationIdAsync(); + + return await _context.Leases + .Include(l => l.Tenant) + .Include(l => l.Property) + .Where(l => l.OrganizationId == orgId && + l.Status == ApplicationConstants.LeaseStatuses.NoticeGiven && + !l.IsDeleted) + .OrderBy(l => l.ExpectedMoveOutDate) + .ToListAsync(); + } + + #endregion + + #region Helper Methods + + private async Task GetLeaseAsync(Guid leaseId) + { + var orgId = await GetActiveOrganizationIdAsync(); + return await _context.Leases + .Include(l => l.Tenant) + .Include(l => l.Property) + .FirstOrDefaultAsync(l => + l.Id == leaseId && + l.OrganizationId == orgId && + !l.IsDeleted); + } + + #endregion + } + + #region Models + + /// + /// Model for lease renewal. + /// + public class LeaseRenewalModel + { + public DateTime? NewStartDate { get; set; } + public DateTime NewEndDate { get; set; } + public decimal NewMonthlyRent { get; set; } + public decimal? UpdatedSecurityDeposit { get; set; } + public string? NewTerms { get; set; } + } + + /// + /// Model for move-out completion. + /// + public class MoveOutModel + { + public bool FinalInspectionCompleted { get; set; } + public bool KeysReturned { get; set; } + public string? Notes { get; set; } + } + + /// + /// Model for deposit deductions. + /// + public class DepositDeductionModel + { + public string Description { get; set; } = string.Empty; + public decimal Amount { get; set; } + public string Category { get; set; } = string.Empty; // "Cleaning", "Repair", "UnpaidRent", "Other" + } + + /// + /// Result of security deposit settlement calculation. + /// + public class SecurityDepositSettlement + { + public Guid LeaseId { get; set; } + public Guid TenantId { get; set; } + public decimal OriginalAmount { get; set; } + public decimal TotalDeductions { get; set; } + public decimal RefundAmount { get; set; } + public decimal AmountOwed { get; set; } + public List Deductions { get; set; } = new(); + public DateTime SettlementDate { get; set; } + } + + /// + /// Aggregated workflow state for a lease. + /// + public class LeaseWorkflowState + { + public Lease? Lease { get; set; } + public Tenant? Tenant { get; set; } + public Property? Property { get; set; } + public SecurityDeposit? SecurityDeposit { get; set; } + public List Renewals { get; set; } = new(); + public List AuditHistory { get; set; } = new(); + public int DaysUntilExpiration { get; set; } + public bool IsExpiring { get; set; } + public bool CanRenew { get; set; } + public bool CanTerminate { get; set; } + } + + #endregion +} diff --git a/Aquiis.Professional/Application/Services/Workflows/WorkflowAuditLog.cs b/Aquiis.Professional/Application/Services/Workflows/WorkflowAuditLog.cs new file mode 100644 index 0000000..716789d --- /dev/null +++ b/Aquiis.Professional/Application/Services/Workflows/WorkflowAuditLog.cs @@ -0,0 +1,58 @@ +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Application.Services.Workflows +{ + /// + /// Audit log for workflow state transitions. + /// Tracks all status changes with context and timestamp. + /// + public class WorkflowAuditLog : BaseModel + { + /// + /// Type of entity (Application, Lease, MaintenanceRequest, etc.) + /// + public required string EntityType { get; set; } + + /// + /// ID of the entity that transitioned + /// + public required Guid EntityId { get; set; } + /// + public string? FromStatus { get; set; } + + /// + /// New status after transition + /// + public required string ToStatus { get; set; } + + /// + /// Action that triggered the transition (e.g., "Submit", "Approve", "Deny") + /// + public required string Action { get; set; } + + /// + /// Optional reason/notes for the transition + /// + public string? Reason { get; set; } + + /// + /// User who performed the action (from UserContextService) + /// + public required string PerformedBy { get; set; } + + /// + /// When the action occurred + /// + public required DateTime PerformedOn { get; set; } + + /// + /// Organization context for the workflow action + /// + public required Guid OrganizationId { get; set; } + + /// + /// Additional context data (JSON serialized) + /// + public string? Metadata { get; set; } + } +} diff --git a/Aquiis.Professional/Application/Services/Workflows/WorkflowResult.cs b/Aquiis.Professional/Application/Services/Workflows/WorkflowResult.cs new file mode 100644 index 0000000..8b90ddb --- /dev/null +++ b/Aquiis.Professional/Application/Services/Workflows/WorkflowResult.cs @@ -0,0 +1,78 @@ +namespace Aquiis.Professional.Application.Services.Workflows +{ + /// + /// Standard result object for workflow operations. + /// Provides success/failure status, error messages, and metadata. + /// + public class WorkflowResult + { + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + public List Errors { get; set; } = new(); + public Dictionary Metadata { get; set; } = new(); + + public static WorkflowResult Ok(string message = "Operation completed successfully") + { + return new WorkflowResult + { + Success = true, + Message = message + }; + } + + public static WorkflowResult Fail(string error) + { + return new WorkflowResult + { + Success = false, + Errors = new List { error } + }; + } + + public static WorkflowResult Fail(List errors) + { + return new WorkflowResult + { + Success = false, + Errors = errors + }; + } + } + + /// + /// Workflow result with typed data payload. + /// Used when operation returns a created/updated entity. + /// + public class WorkflowResult : WorkflowResult + { + public T? Data { get; set; } + + public static WorkflowResult Ok(T data, string message = "Operation completed successfully") + { + return new WorkflowResult + { + Success = true, + Message = message, + Data = data + }; + } + + public new static WorkflowResult Fail(string error) + { + return new WorkflowResult + { + Success = false, + Errors = new List { error } + }; + } + + public new static WorkflowResult Fail(List errors) + { + return new WorkflowResult + { + Success = false, + Errors = errors + }; + } + } +} diff --git a/Aquiis.Professional/Aquiis.Professional.csproj b/Aquiis.Professional/Aquiis.Professional.csproj new file mode 100644 index 0000000..82f8cf0 --- /dev/null +++ b/Aquiis.Professional/Aquiis.Professional.csproj @@ -0,0 +1,52 @@ + + + + net9.0 + enable + enable + aspnet-Aquiis.Professional-ae1e0851-3ba3-4d71-bc57-597eb787b7d8 + true + Infrastructure/Data/Migrations + + + 0.2.0 + 0.2.0.0 + 0.2.0.0 + 0.2.0 + + + + + + + + PreserveNewest + Assets\splash.png + + + PreserveNewest + Assets\splash.svg + + + + PreserveNewest + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/Aquiis.Professional/Components/App.razor b/Aquiis.Professional/Components/App.razor new file mode 100644 index 0000000..d18299a --- /dev/null +++ b/Aquiis.Professional/Components/App.razor @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Aquiis.Professional/Components/Layout/MainLayout.razor b/Aquiis.Professional/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..9e253ba --- /dev/null +++ b/Aquiis.Professional/Components/Layout/MainLayout.razor @@ -0,0 +1,23 @@ +@inherits LayoutComponentBase + +
+ + +
+
+ About +
+ +
+ @Body +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/Aquiis.Professional/Components/Layout/MainLayout.razor.css b/Aquiis.Professional/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..f29f3c3 --- /dev/null +++ b/Aquiis.Professional/Components/Layout/MainLayout.razor.css @@ -0,0 +1,98 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/Aquiis.Professional/Components/Layout/NavMenu.razor b/Aquiis.Professional/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..ac6cc16 --- /dev/null +++ b/Aquiis.Professional/Components/Layout/NavMenu.razor @@ -0,0 +1,92 @@ +@implements IDisposable + +@inject NavigationManager NavigationManager + + + + + + + +@code { + private string? currentUrl; + + protected override void OnInitialized() + { + currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); + NavigationManager.LocationChanged += OnLocationChanged; + } + + private void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + currentUrl = NavigationManager.ToBaseRelativePath(e.Location); + StateHasChanged(); + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + } +} + diff --git a/Aquiis.Professional/Components/Layout/NavMenu.razor.css b/Aquiis.Professional/Components/Layout/NavMenu.razor.css new file mode 100644 index 0000000..b0ed49f --- /dev/null +++ b/Aquiis.Professional/Components/Layout/NavMenu.razor.css @@ -0,0 +1,125 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + min-height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.bi-lock-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E"); +} + +.bi-person-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E"); +} + +.bi-person-badge-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E"); +} + +.bi-person-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E"); +} + +.bi-arrow-bar-left-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/Aquiis.Professional/Components/_Imports.razor b/Aquiis.Professional/Components/_Imports.razor new file mode 100644 index 0000000..680bc36 --- /dev/null +++ b/Aquiis.Professional/Components/_Imports.razor @@ -0,0 +1,11 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Aquiis.Professional +@using Aquiis.Professional.Components diff --git a/Aquiis.Professional/Core/Constants/ApplicationConstants.cs b/Aquiis.Professional/Core/Constants/ApplicationConstants.cs new file mode 100644 index 0000000..992fe0b --- /dev/null +++ b/Aquiis.Professional/Core/Constants/ApplicationConstants.cs @@ -0,0 +1,789 @@ +using System.Security.Cryptography.X509Certificates; + +namespace Aquiis.Professional.Core.Constants +{ + public static class ApplicationConstants + { + /// + /// System service account for background jobs and automated processes + /// + public static class SystemUser + { + /// + /// Well-known GUID for system service account. + /// Used by background jobs, scheduled tasks, and automated processes. + /// + public static readonly string Id = "00000000-0000-0000-0000-000000000001"; + + public const string Email = "system@aquiis.local"; + public const string UserName = "system@aquiis.local"; // UserName = Email in this system + public const string DisplayName = "System"; + + // Service account details + public const string FirstName = "System User"; + public const string LastName = "Account"; + } + + // DEPRECATED: Legacy Identity roles - kept for backward compatibility but not used for authorization + public static string DefaultSuperAdminRole { get; } = "SuperAdministrator"; + public static string DefaultAdminRole { get; } = "Administrator"; + public static string DefaultPropertyManagerRole { get; } = "PropertyManager"; + public static string DefaultTenantRole { get; } = "Tenant"; + public static string DefaultUserRole { get; } = "User"; + public static string DefaultGuestRole { get; } = "Guest"; + + /// + /// Organization-scoped roles for multi-organization support + /// + public static class OrganizationRoles + { + /// + /// Owner - Full data sovereignty (create/delete orgs, backup/delete data, all features) + /// + public const string Owner = "Owner"; + + /// + /// Administrator - Delegated owner access (all features except org creation/deletion/data management) + /// + public const string Administrator = "Administrator"; + + /// + /// PropertyManager - Full property management features (no admin/settings access) + /// + public const string PropertyManager = "Property Manager"; + + /// + /// Maintenance - Maintenance requests, work orders, and vendors + /// + public const string Maintenance = "Maintenance"; + + /// + /// User - Limited feature access (view-only or basic operations) + /// + public const string User = "User"; + + public static readonly string[] AllRoles = { Owner, Administrator, PropertyManager, User }; + + public static bool IsValid(string role) => AllRoles.Contains(role); + + public static bool CanManageUsers(string role) => role == Owner || role == Administrator; + + public static bool CanEditSettings(string role) => role == Owner || role == Administrator; + + public static bool CanManageOrganizations(string role) => role == Owner; + + public static bool CanManageProperties(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageTenants(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageLeases(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageInvoices(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManagePayments(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageSecurityDeposits(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageDocuments(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageMaintenanceRequests(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageInspections(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageProspectiveTenants(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageApplications(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageTours(string role) => role == Owner || role == Administrator || role == PropertyManager; + + public static bool CanManageChecklists(string role) => role == Owner || role == Administrator || role == PropertyManager; + + + + public static bool CanViewRecords(string role) => AllRoles.Contains(role); + + public static bool CanEditRecords(string role) => role == Owner || role == Administrator || role == PropertyManager; + } + + public static string DefaultSuperAdminPassword { get; } = "SuperAdmin@123!"; + public static string DefaultAdminPassword { get; } = "Admin@123!"; + public static string DefaultPropertyManagerPassword { get; } = "PropertyManager@123!"; + public static string DefaultTenantPassword { get; } = "Tenant@123!"; + public static string DefaultUserPassword { get; } = "User@123!"; + public static string DefaultGuestPassword { get; } = "Guest@123!"; + + public static string AdministrationPath { get; } = "/Administration"; + public static string PropertyManagementPath { get; } = "/PropertyManagement"; + public static string TenantPortalPath { get; } = "/TenantPortal"; + + + public static string SuperAdminUserName { get; } = "superadmin"; + public static string SuperAdminEmail { get; } = "superadmin@example.local"; + + public static IReadOnlyList DefaultRoles { get; } = new List + { + DefaultSuperAdminRole, + DefaultAdminRole, + DefaultPropertyManagerRole, + DefaultTenantRole, + DefaultUserRole, + DefaultGuestRole + }; + + public static IReadOnlyList DefaultPasswords { get; } = new List + { + DefaultSuperAdminPassword, + DefaultAdminPassword, + DefaultPropertyManagerPassword, + DefaultTenantPassword, + DefaultUserPassword, + DefaultGuestPassword + }; + + public static string[] USStateAbbreviations { get; } = States.Abbreviations(); + public static string[] USStateNames { get; } = States.Names(); + + public static State[] USStates { get; } = States.StatesArray(); + + public static class PaymentMethods + { + public const string OnlinePayment = "Online Payment"; + public const string DebitCard = "Debit Card"; + public const string CreditCard = "Credit Card"; + public const string BankTransfer = "Bank Transfer"; + public const string CryptoCurrency = "Crypto Currency"; + public const string Cash = "Cash"; + public const string Check = "Check"; + public const string Other = "Other"; + + public static IReadOnlyList AllPaymentMethods { get; } = new List + { + OnlinePayment, + DebitCard, + CreditCard, + BankTransfer, + CryptoCurrency, + Cash, + Check, + Other + }; + } + + public static class InvoiceStatuses + { + public const string Pending = "Pending"; + public const string PaidPartial = "Paid Partial"; + public const string Paid = "Paid"; + public const string Overdue = "Overdue"; + public const string Cancelled = "Cancelled"; + + public static IReadOnlyList AllInvoiceStatuses { get; } = new List + { + Pending, + PaidPartial, + Paid, + Overdue, + Cancelled + }; + } + + public static class PaymentStatuses + { + public const string Completed = "Completed"; + public const string Pending = "Pending"; + public const string Failed = "Failed"; + public const string Refunded = "Refunded"; + + public static IReadOnlyList AllPaymentStatuses { get; } = new List + { + Completed, + Pending, + Failed, + Refunded + }; + } + public static class InspectionTypes + { + public const string MoveIn = "Move-In"; + public const string MoveOut = "Move-Out"; + public const string Routine = "Routine"; + public const string Maintenance = "Maintenance"; + public const string Other = "Other"; + + public static IReadOnlyList AllInspectionTypes { get; } = new List + { + MoveIn, + MoveOut, + Routine, + Maintenance, + Other + }; + } + + public static class LeaseTypes { + public const string FixedTerm = "Fixed-Term"; + public const string MonthToMonth = "Month-to-Month"; + public const string Sublease = "Sublease"; + public const string Other = "Other"; + + public static IReadOnlyList AllLeaseTypes { get; } = new List + { + FixedTerm, + MonthToMonth, + Sublease, + Other + }; + + } + + public static class LeaseStatuses { + public const string Offered = "Offered"; + public const string Pending = "Pending"; + public const string Accepted = "Accepted"; + public const string AcceptedPendingStart = "Accepted - Pending Start"; + public const string Active = "Active"; + public const string Declined = "Declined"; + public const string Renewed = "Renewed"; + public const string MonthToMonth = "Month-to-Month"; + public const string NoticeGiven = "Notice Given"; + public const string Interrupted = "Interrupted"; + public const string Terminated = "Terminated"; + public const string Expired = "Expired"; + + public static IReadOnlyList RenewalStatuses { get; } = new List + { + "NotRequired", + "Pending", + "Offered", + "Accepted", + "Declined", + "Expired" + }; + + public static IReadOnlyList AllLeaseStatuses { get; } = new List + { + Offered, + Pending, + Accepted, + AcceptedPendingStart, + Active, + Declined, + Renewed, + MonthToMonth, + NoticeGiven, + Interrupted, + Terminated, + Expired + }; + } + + + + public static class PropertyTypes + { + public const string House = "House"; + public const string Apartment = "Apartment"; + public const string Condo = "Condo"; + public const string Townhouse = "Townhouse"; + public const string Duplex = "Duplex"; + public const string Studio = "Studio"; + public const string Loft = "Loft"; + public const string Other = "Other"; + + public static IReadOnlyList AllPropertyTypes { get; } = new List + { + House, + Apartment, + Condo, + Townhouse, + Duplex, + Studio, + Loft, + Other + }; + + } + + public static class PropertyStatuses + { + public const string Available = "Available"; + public const string ApplicationPending = "Application Pending"; + public const string LeasePending = "Lease Pending"; + public const string MoveInPending = "Accepted - Move-In Pending"; + public const string Occupied = "Occupied"; + public const string MoveOutPending = "Move-Out Pending"; + public const string UnderRenovation = "Under Renovation"; + public const string OffMarket = "Off Market"; + + public static IReadOnlyList OccupiedStatuses { get; } = new List + { + MoveInPending, + Occupied, + MoveOutPending + }; + public static IReadOnlyList AllPropertyStatuses { get; } = new List + { + Available, + ApplicationPending, + LeasePending, + Occupied, + UnderRenovation, + OffMarket + }; + } + + + + public static class MaintenanceRequestTypes + { + + public const string Plumbing = "Plumbing"; + public const string Electrical = "Electrical"; + public const string HeatingCooling = "Heating/Cooling"; + public const string Appliance = "Appliance"; + public const string Structural = "Structural"; + public const string Landscaping = "Landscaping"; + public const string PestControl = "Pest Control"; + public const string Other = "Other"; + + public static IReadOnlyList AllMaintenanceRequestTypes { get; } = new List + { + Plumbing, + Electrical, + HeatingCooling, + Appliance, + Structural, + Landscaping, + PestControl, + Other + }; + } + + public static class MaintenanceRequestPriorities + { + public const string Low = "Low"; + public const string Medium = "Medium"; + public const string High = "High"; + public const string Urgent = "Urgent"; + + public static IReadOnlyList AllMaintenanceRequestPriorities { get; } = new List + { + Low, + Medium, + High, + Urgent + }; + } + + public static class MaintenanceRequestStatuses + { + public const string Submitted = "Submitted"; + public const string InProgress = "In Progress"; + public const string Completed = "Completed"; + public const string Cancelled = "Cancelled"; + + public static IReadOnlyList AllMaintenanceRequestStatuses { get; } = new List + { + Submitted, + InProgress, + Completed, + Cancelled + }; + } + + public static class TenantStatuses + { + public const string Prospective = "Prospective"; + public const string Pending = "Pending"; + public const string MoveInPending = "Move-In Pending"; + public const string Active = "Active"; + public const string MoveOutPending = "Move-Out Pending"; + public const string Inactive = "Inactive"; + public const string Evicted = "Evicted"; + + public static IReadOnlyList AllTenantStatuses { get; } = new List + { + Prospective, + Pending, + MoveInPending, + Active, + MoveOutPending, + Inactive, + Evicted + }; + + } + + public static class DocumentTypes + { + public const string LeaseApplication = "Lease Application"; + public const string LeaseAgreement = "Lease Agreement"; + public const string InspectionReport = "Inspection Report"; + public const string MaintenanceRecord = "Maintenance Record"; + public const string Invoice = "Invoice"; + public const string PaymentReceipt = "Payment Receipt"; + public const string Other = "Other"; + + public static IReadOnlyList AllDocumentTypes { get; } = new List + { + LeaseApplication, + LeaseAgreement, + InspectionReport, + MaintenanceRecord, + Invoice, + PaymentReceipt, + Other + }; + + } + + public static class ChecklistTypes + { + public const string MoveIn = "Move-In"; + public const string MoveOut = "Move-Out"; + public const string OpenHouse = "Open House"; + public const string Tour = "Tour"; + public const string Custom = "Custom"; + + public static IReadOnlyList AllChecklistTypes { get; } = new List + { + MoveIn, + MoveOut, + OpenHouse, + Tour, + Custom + }; + } + + public static class ChecklistStatuses + { + public const string Draft = "Draft"; + public const string InProgress = "In Progress"; + public const string Completed = "Completed"; + + public static IReadOnlyList AllChecklistStatuses { get; } = new List + { + Draft, + InProgress, + Completed + }; + } + + public static class ProspectiveStatuses + { + public const string Lead = "Lead"; + public const string TourScheduled = "Tour Scheduled"; + public const string Applied = "Applied"; + public const string Screening = "Screening"; + public const string Approved = "Approved"; + public const string Denied = "Denied"; + public const string Withdrawn = "Withdrawn"; + public const string LeaseOffered = "Lease Offered"; + public const string LeaseDeclined = "Lease Declined"; + public const string ConvertedToTenant = "Converted To Tenant"; + + public static IReadOnlyList AllProspectiveStatuses { get; } = new List + { + Lead, + TourScheduled, + Applied, + Screening, + Approved, + Denied, + Withdrawn, + LeaseOffered, + LeaseDeclined, + ConvertedToTenant + }; + } + + public static class ProspectiveSources + { + public const string Website = "Website"; + public const string Referral = "Referral"; + public const string WalkIn = "Walk-in"; + public const string Zillow = "Zillow"; + public const string Apartments = "Apartments.com"; + public const string SignCall = "Sign Call"; + public const string SocialMedia = "Social Media"; + public const string Other = "Other"; + + public static IReadOnlyList AllProspectiveSources { get; } = new List + { + Website, + Referral, + WalkIn, + Zillow, + Apartments, + SignCall, + SocialMedia, + Other + }; + } + + public static class TourStatuses + { + public const string Scheduled = "Scheduled"; + public const string Completed = "Completed"; + public const string Cancelled = "Cancelled"; + public const string NoShow = "NoShow"; + + public static IReadOnlyList AllTourStatuses { get; } = new List + { + Scheduled, + Completed, + Cancelled, + NoShow + }; + } + + public static class TourInterestLevels + { + public const string VeryInterested = "Very Interested"; + public const string Interested = "Interested"; + public const string Neutral = "Neutral"; + public const string NotInterested = "Not Interested"; + + public static IReadOnlyList AllTourInterestLevels { get; } = new List + { + VeryInterested, + Interested, + Neutral, + NotInterested + }; + } + + public static class ApplicationStatuses + { + public const string Submitted = "Submitted"; + public const string UnderReview = "Under Review"; + public const string Screening = "Screening"; + public const string Approved = "Approved"; + public const string Denied = "Denied"; + public const string Expired = "Expired"; + public const string Withdrawn = "Withdrawn"; + public const string LeaseOffered = "Lease Offered"; + public const string LeaseAccepted = "Lease Accepted"; + public const string LeaseDeclined = "Lease Declined"; + + public static IReadOnlyList AllApplicationStatuses { get; } = new List + { + Submitted, + UnderReview, + Screening, + Approved, + Denied, + Expired, + Withdrawn, + LeaseOffered, + LeaseAccepted, + LeaseDeclined + }; + } + + public static class ScreeningResults + { + public const string Pending = "Pending"; + public const string Passed = "Passed"; + public const string Failed = "Failed"; + public const string ConditionalPass = "Conditional Pass"; + + public static IReadOnlyList AllScreeningResults { get; } = new List + { + Pending, + Passed, + Failed, + ConditionalPass + }; + } + + public static class SecurityDepositStatuses + { + public const string Held = "Held"; + public const string Released = "Released"; + public const string Refunded = "Refunded"; + public const string Forfeited = "Forfeited"; + public const string PartiallyRefunded = "Partially Refunded"; + + public static IReadOnlyList AllSecurityDepositStatuses { get; } = new List + { + Held, + Released, + Refunded, + Forfeited, + PartiallyRefunded + }; + } + + public static class InvestmentPoolStatuses + { + public const string Open = "Open"; + public const string Calculated = "Calculated"; + public const string Distributed = "Distributed"; + public const string Closed = "Closed"; + + public static IReadOnlyList AllInvestmentPoolStatuses { get; } = new List + { + Open, + Calculated, + Distributed, + Closed + }; + } + + public static class DividendPaymentMethods + { + public const string Pending = "Pending"; + public const string LeaseCredit = "Lease Credit"; + public const string Check = "Check"; + + public static IReadOnlyList AllDividendPaymentMethods { get; } = new List + { + Pending, + LeaseCredit, + Check + }; + } + + public static class DividendStatuses + { + public const string Pending = "Pending"; + public const string ChoiceMade = "Choice Made"; + public const string Applied = "Applied"; + public const string Paid = "Paid"; + + public static IReadOnlyList AllDividendStatuses { get; } = new List + { + Pending, + ChoiceMade, + Applied, + Paid + }; + } + + public static class EntityTypes + { + public const string Property = "Property"; + public const string Tenant = "Tenant"; + public const string Lease = "Lease"; + public const string Invoice = "Invoice"; + public const string Payment = "Payment"; + public const string MaintenanceRequest = "MaintenanceRequest"; + public const string Document = "Document"; + public const string Inspection = "Inspection"; + public const string ProspectiveTenant = "ProspectiveTenant"; + public const string Application = "Application"; + public const string Tour = "Tour"; + public const string Checklist = "Checklist"; + public const string Note = "Note"; + } + + + + } + static class States + { + + static List _states = new List(50); + + static States() + { + _states.Add(new State("AL", "Alabama")); + _states.Add(new State("AK", "Alaska")); + _states.Add(new State("AZ", "Arizona")); + _states.Add(new State("AR", "Arkansas")); + _states.Add(new State("CA", "California")); + _states.Add(new State("CO", "Colorado")); + _states.Add(new State("CT", "Connecticut")); + _states.Add(new State("DE", "Delaware")); + _states.Add(new State("DC", "District Of Columbia")); + _states.Add(new State("FL", "Florida")); + _states.Add(new State("GA", "Georgia")); + _states.Add(new State("HI", "Hawaii")); + _states.Add(new State("ID", "Idaho")); + _states.Add(new State("IL", "Illinois")); + _states.Add(new State("IN", "Indiana")); + _states.Add(new State("IA", "Iowa")); + _states.Add(new State("KS", "Kansas")); + _states.Add(new State("KY", "Kentucky")); + _states.Add(new State("LA", "Louisiana")); + _states.Add(new State("ME", "Maine")); + _states.Add(new State("MD", "Maryland")); + _states.Add(new State("MA", "Massachusetts")); + _states.Add(new State("MI", "Michigan")); + _states.Add(new State("MN", "Minnesota")); + _states.Add(new State("MS", "Mississippi")); + _states.Add(new State("MO", "Missouri")); + _states.Add(new State("MT", "Montana")); + _states.Add(new State("NE", "Nebraska")); + _states.Add(new State("NV", "Nevada")); + _states.Add(new State("NH", "New Hampshire")); + _states.Add(new State("NJ", "New Jersey")); + _states.Add(new State("NM", "New Mexico")); + _states.Add(new State("NY", "New York")); + _states.Add(new State("NC", "North Carolina")); + _states.Add(new State("ND", "North Dakota")); + _states.Add(new State("OH", "Ohio")); + _states.Add(new State("OK", "Oklahoma")); + _states.Add(new State("OR", "Oregon")); + _states.Add(new State("PA", "Pennsylvania")); + _states.Add(new State("RI", "Rhode Island")); + _states.Add(new State("SC", "South Carolina")); + _states.Add(new State("SD", "South Dakota")); + _states.Add(new State("TN", "Tennessee")); + _states.Add(new State("TX", "Texas")); + _states.Add(new State("UT", "Utah")); + _states.Add(new State("VT", "Vermont")); + _states.Add(new State("VA", "Virginia")); + _states.Add(new State("WA", "Washington")); + _states.Add(new State("WV", "West Virginia")); + _states.Add(new State("WI", "Wisconsin")); + _states.Add(new State("WY", "Wyoming")); + } + + public static string[] Abbreviations() + { + List abbrevList = new List(_states.Count); + foreach (var state in _states) + { + abbrevList.Add(state.Abbreviation); + } + return abbrevList.ToArray(); + } + + public static string[] Names() + { + List nameList = new List(_states.Count); + foreach (var state in _states) + { + nameList.Add(state.Name); + } + return nameList.ToArray(); + } + + public static State[] StatesArray() + { + return _states.ToArray(); + } + + } + + public class State + { + public State(string ab, string name) + { + Name = name; + Abbreviation = ab; + } + + public string Name { get; set; } + + public string Abbreviation { get; set; } + + public override string ToString() + { + return string.Format("{0} - {1}", Abbreviation, Name); + } + + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Constants/ApplicationSettings.cs b/Aquiis.Professional/Core/Constants/ApplicationSettings.cs new file mode 100644 index 0000000..20d7e6d --- /dev/null +++ b/Aquiis.Professional/Core/Constants/ApplicationSettings.cs @@ -0,0 +1,108 @@ +namespace Aquiis.Professional.Core.Constants +{ + public class ApplicationSettings + { + public string AppName { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string Author { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Repository { get; set; } = string.Empty; + public bool SoftDeleteEnabled { get; set; } + public string SchemaVersion { get; set; } = "1.0.0"; + } + + // Property & Tenant Lifecycle Enums + + /// + /// Property status in the rental lifecycle + /// + public enum PropertyStatus + { + Available, // Ready to market and show + ApplicationPending, // One or more applications under review + LeasePending, // Application approved, lease offered, awaiting signature + Occupied, // Active lease in place + UnderRenovation, // Not marketable, undergoing repairs/upgrades + OffMarket // Temporarily unavailable + } + + /// + /// Prospect status through the application journey + /// + public enum ProspectStatus + { + Inquiry, // Initial contact/lead + Contacted, // Follow-up made + TourScheduled, // Tour appointment set + Toured, // Tour completed + ApplicationSubmitted, // Application submitted, awaiting review + UnderReview, // Screening in progress + ApplicationApproved, // Approved, lease offer pending + ApplicationDenied, // Application rejected + LeaseOffered, // Lease document sent for signature + LeaseSigned, // Lease accepted and signed + LeaseDeclined, // Lease offer declined + ConvertedToTenant, // Successfully converted to tenant + Inactive // No longer pursuing or expired + } + + /// + /// Rental application status + /// + public enum ApplicationStatus + { + Pending, // Application received, awaiting review + UnderReview, // Screening in progress + Approved, // Approved for lease + Denied, // Application rejected + Expired, // Not processed within 30 days + Withdrawn // Applicant withdrew + } + + /// + /// Lease status through its lifecycle + /// + public enum LeaseStatus + { + Offered, // Lease generated, awaiting tenant signature + Active, // Signed and currently active + Expired, // Past end date, not renewed + Terminated, // Ended early or declined + Renewed, // Superseded by renewal lease + MonthToMonth // Converted to month-to-month + } + + /// + /// Security deposit disposition status + /// + public enum DepositDispositionStatus + { + Held, // Currently escrowed + PartiallyReturned, // Part returned, part withheld + FullyReturned, // Fully returned to tenant + Withheld, // Fully withheld for damages/unpaid rent + PartiallyWithheld // Same as PartiallyReturned (choose one) + } + + /// + /// Dividend payment method chosen by tenant + /// + public enum DividendPaymentMethod + { + TenantChoice, // Not yet chosen + LeaseCredit, // Apply as credit to next invoice + Check // Send check to tenant + } + + /// + /// Dividend payment status + /// + public enum DividendPaymentStatus + { + Pending, // Calculated but not yet distributed + Applied, // Applied as lease credit + CheckIssued, // Check sent to tenant + Completed, // Fully processed + Forfeited // Tenant did not claim (rare) + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Constants/EntityTypeNames.cs b/Aquiis.Professional/Core/Constants/EntityTypeNames.cs new file mode 100644 index 0000000..b7bb70d --- /dev/null +++ b/Aquiis.Professional/Core/Constants/EntityTypeNames.cs @@ -0,0 +1,65 @@ +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Core.Constants; + +/// +/// Centralized entity type names for integration tables (Notes, Audit Logs, etc.) +/// Uses fully-qualified type names to prevent collisions with external systems +/// +public static class EntityTypeNames +{ + // Property Management Domain + public const string Property = "Aquiis.Professional.Core.Entities.Property"; + public const string Tenant = "Aquiis.Professional.Core.Entities.Tenant"; + public const string Lease = "Aquiis.Professional.Core.Entities.Lease"; + public const string LeaseOffer = "Aquiis.Professional.Core.Entities.LeaseOffer"; + public const string Invoice = "Aquiis.Professional.Core.Entities.Invoice"; + public const string Payment = "Aquiis.Professional.Core.Entities.Payment"; + public const string MaintenanceRequest = "Aquiis.Professional.Core.Entities.MaintenanceRequest"; + public const string Inspection = "Aquiis.Professional.Core.Entities.Inspection"; + public const string Document = "Aquiis.Professional.Core.Entities.Document"; + + // Application/Prospect Domain + public const string ProspectiveTenant = "Aquiis.Professional.Core.Entities.ProspectiveTenant"; + public const string Application = "Aquiis.Professional.Core.Entities.Application"; + public const string Tour = "Aquiis.Professional.Core.Entities.Tour"; + + // Checklist Domain + public const string Checklist = "Aquiis.Professional.Core.Entities.Checklist"; + public const string ChecklistTemplate = "Aquiis.Professional.Core.Entities.ChecklistTemplate"; + + // Calendar/Events + public const string CalendarEvent = "Aquiis.Professional.Core.Entities.CalendarEvent"; + + // Security Deposits + public const string SecurityDepositPool = "Aquiis.Professional.Core.Entities.SecurityDepositPool"; + public const string SecurityDepositTransaction = "Aquiis.Professional.Core.Entities.SecurityDepositTransaction"; + + /// + /// Get the fully-qualified type name for an entity type + /// + public static string GetTypeName() where T : BaseModel + { + return typeof(T).FullName ?? typeof(T).Name; + } + + /// + /// Get the display name (simple name) from a fully-qualified type name + /// + public static string GetDisplayName(string fullyQualifiedName) + { + return fullyQualifiedName.Split('.').Last(); + } + + /// + /// Validate that an entity type string is recognized + /// + public static bool IsValidEntityType(string entityType) + { + return typeof(EntityTypeNames) + .GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static) + .Where(f => f.FieldType == typeof(string)) + .Select(f => f.GetValue(null) as string) + .Contains(entityType); + } +} diff --git a/Aquiis.Professional/Core/Constants/NotificationConstants.cs b/Aquiis.Professional/Core/Constants/NotificationConstants.cs new file mode 100644 index 0000000..ef3035f --- /dev/null +++ b/Aquiis.Professional/Core/Constants/NotificationConstants.cs @@ -0,0 +1,62 @@ +public static class NotificationConstants +{ + public static class Types + { + public const string Info = "Info"; + public const string Warning = "Warning"; + public const string Error = "Error"; + public const string Success = "Success"; + } + + public static class Categories + { + public const string Application = "Application"; + public const string Document = "Document"; + public const string Inspection = "Inspection"; + public const string Lease = "Lease"; + public const string Maintenance = "Maintenance"; + public const string Message = "Message"; + public const string Note = "Note"; + public const string Payment = "Payment"; + public const string Property = "Property"; + public const string Report = "Report"; + public const string Security = "Security"; + public const string System = "System"; + } + + public static class Templates + { + // Lease notifications + public const string LeaseExpiring90Days = "lease_expiring_90"; + public const string LeaseExpiring60Days = "lease_expiring_60"; + public const string LeaseExpiring30Days = "lease_expiring_30"; + public const string LeaseActivated = "lease_activated"; + public const string LeaseTerminated = "lease_terminated"; + + // Payment notifications + public const string PaymentDueReminder = "payment_due_reminder"; + public const string PaymentReceived = "payment_received"; + public const string PaymentLate = "payment_late"; + public const string LateFeeApplied = "late_fee_applied"; + + // Maintenance notifications + public const string MaintenanceRequestCreated = "maintenance_created"; + public const string MaintenanceRequestAssigned = "maintenance_assigned"; + public const string MaintenanceRequestStarted = "maintenance_started"; + public const string MaintenanceRequestCompleted = "maintenance_completed"; + + // Application notifications + public const string ApplicationSubmitted = "application_submitted"; + public const string ApplicationUnderReview = "application_under_review"; + public const string ApplicationApproved = "application_approved"; + public const string ApplicationRejected = "application_rejected"; + + // Inspection notifications + public const string InspectionScheduled = "inspection_scheduled"; + public const string InspectionCompleted = "inspection_completed"; + + // Document notifications + public const string DocumentUploaded = "document_uploaded"; + public const string DocumentExpiring = "document_expiring"; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/ApplicationScreening.cs b/Aquiis.Professional/Core/Entities/ApplicationScreening.cs new file mode 100644 index 0000000..39ca49c --- /dev/null +++ b/Aquiis.Professional/Core/Entities/ApplicationScreening.cs @@ -0,0 +1,68 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + public class ApplicationScreening : BaseModel + { + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [RequiredGuid] + [Display(Name = "Rental Application")] + public Guid RentalApplicationId { get; set; } + + // Background Check + [Display(Name = "Background Check Requested")] + public bool BackgroundCheckRequested { get; set; } + + [Display(Name = "Background Check Requested Date")] + public DateTime? BackgroundCheckRequestedOn { get; set; } + + [Display(Name = "Background Check Passed")] + public bool? BackgroundCheckPassed { get; set; } + + [Display(Name = "Background Check Completed Date")] + public DateTime? BackgroundCheckCompletedOn { get; set; } + + [StringLength(1000)] + [Display(Name = "Background Check Notes")] + public string? BackgroundCheckNotes { get; set; } + + // Credit Check + [Display(Name = "Credit Check Requested")] + public bool CreditCheckRequested { get; set; } + + [Display(Name = "Credit Check Requested Date")] + public DateTime? CreditCheckRequestedOn { get; set; } + + [Display(Name = "Credit Score")] + public int? CreditScore { get; set; } + + [Display(Name = "Credit Check Passed")] + public bool? CreditCheckPassed { get; set; } + + [Display(Name = "Credit Check Completed Date")] + public DateTime? CreditCheckCompletedOn { get; set; } + + [StringLength(1000)] + [Display(Name = "Credit Check Notes")] + public string? CreditCheckNotes { get; set; } + + // Overall Result + [Required] + [StringLength(50)] + [Display(Name = "Overall Result")] + public string OverallResult { get; set; } = string.Empty; // Pending, Passed, Failed, ConditionalPass + + [StringLength(2000)] + [Display(Name = "Result Notes")] + public string? ResultNotes { get; set; } + + // Navigation properties + [ForeignKey(nameof(RentalApplicationId))] + public virtual RentalApplication? RentalApplication { get; set; } + } +} diff --git a/Aquiis.Professional/Core/Entities/BaseModel.cs b/Aquiis.Professional/Core/Entities/BaseModel.cs new file mode 100644 index 0000000..35a30e3 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/BaseModel.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using Aquiis.Professional.Core.Interfaces; + +namespace Aquiis.Professional.Core.Entities +{ + public class BaseModel : IAuditable + { + [Key] + [JsonInclude] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public Guid Id { get; set; } + + [Required] + [JsonInclude] + [DataType(DataType.DateTime)] + [Display(Name = "Created On")] + public DateTime CreatedOn { get; set; } = DateTime.UtcNow; + + [Required] + [JsonInclude] + [StringLength(100)] + [DataType(DataType.Text)] + [Display(Name = "Created By")] + public string CreatedBy { get; set; } = string.Empty; + + [JsonInclude] + [DataType(DataType.DateTime)] + [Display(Name = "Last Modified On")] + public DateTime? LastModifiedOn { get; set; } + + [JsonInclude] + [StringLength(100)] + [DataType(DataType.Text)] + [Display(Name = "Last Modified By")] + public string? LastModifiedBy { get; set; } + + [JsonInclude] + [Display(Name = "Is Deleted?")] + public bool IsDeleted { get; set; } = false; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/CalendarEvent.cs b/Aquiis.Professional/Core/Entities/CalendarEvent.cs new file mode 100644 index 0000000..a8f72fd --- /dev/null +++ b/Aquiis.Professional/Core/Entities/CalendarEvent.cs @@ -0,0 +1,78 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Represents a calendar event that can be either domain-linked (Tour, Inspection, etc.) + /// or a custom user-created event + /// + public class CalendarEvent : BaseModel + { + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + [StringLength(200)] + [Display(Name = "Title")] + public string Title { get; set; } = string.Empty; + + [Required] + [Display(Name = "Start Date & Time")] + public DateTime StartOn { get; set; } + + [Display(Name = "End Date & Time")] + public DateTime? EndOn { get; set; } + + [Display(Name = "Duration (Minutes)")] + public int DurationMinutes { get; set; } + + [Required] + [StringLength(50)] + [Display(Name = "Event Type")] + public string EventType { get; set; } = string.Empty; + + [StringLength(50)] + [Display(Name = "Status")] + public string Status { get; set; } = string.Empty; + + [StringLength(2000)] + [Display(Name = "Description")] + public string? Description { get; set; } + + [Display(Name = "Property")] + public Guid? PropertyId { get; set; } + + [StringLength(500)] + [Display(Name = "Location")] + public string? Location { get; set; } + + [StringLength(20)] + [Display(Name = "Color")] + public string Color { get; set; } = "#6c757d"; // Default gray + + [StringLength(50)] + [Display(Name = "Icon")] + public string Icon { get; set; } = "bi-calendar-event"; + + // Polymorphic reference to source entity (null for custom events) + [Display(Name = "Source Entity ID")] + public Guid? SourceEntityId { get; set; } + + [StringLength(100)] + [Display(Name = "Source Entity Type")] + public string? SourceEntityType { get; set; } + + // Navigation properties + [ForeignKey(nameof(PropertyId))] + public virtual Property? Property { get; set; } + + /// + /// Indicates if this is a custom event (not linked to a domain entity) + /// + [NotMapped] + public bool IsCustomEvent => string.IsNullOrEmpty(SourceEntityType); + } +} diff --git a/Aquiis.Professional/Core/Entities/CalendarEventTypes.cs b/Aquiis.Professional/Core/Entities/CalendarEventTypes.cs new file mode 100644 index 0000000..b17193a --- /dev/null +++ b/Aquiis.Professional/Core/Entities/CalendarEventTypes.cs @@ -0,0 +1,66 @@ +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Defines calendar event type constants and their visual properties + /// + public static class CalendarEventTypes + { + // Event Type Constants + public const string Tour = "Tour"; + public const string Inspection = "Inspection"; + public const string Maintenance = "Maintenance"; + public const string LeaseExpiry = "LeaseExpiry"; + public const string RentDue = "RentDue"; + public const string Custom = "Custom"; + + /// + /// Configuration for each event type (color and icon) + /// + public static readonly Dictionary Config = new() + { + [Tour] = new EventTypeConfig("#0dcaf0", "bi-calendar-check", "Property Tour"), + [Inspection] = new EventTypeConfig("#fd7e14", "bi-clipboard-check", "Property Inspection"), + [Maintenance] = new EventTypeConfig("#dc3545", "bi-tools", "Maintenance Request"), + [LeaseExpiry] = new EventTypeConfig("#ffc107", "bi-calendar-x", "Lease Expiry"), + [RentDue] = new EventTypeConfig("#198754", "bi-cash-coin", "Rent Due"), + [Custom] = new EventTypeConfig("#6c757d", "bi-calendar-event", "Custom Event") + }; + + /// + /// Get the color for an event type + /// + public static string GetColor(string eventType) + { + return Config.TryGetValue(eventType, out var config) ? config.Color : Config[Custom].Color; + } + + /// + /// Get the icon for an event type + /// + public static string GetIcon(string eventType) + { + return Config.TryGetValue(eventType, out var config) ? config.Icon : Config[Custom].Icon; + } + + /// + /// Get the display name for an event type + /// + public static string GetDisplayName(string eventType) + { + return Config.TryGetValue(eventType, out var config) ? config.DisplayName : eventType; + } + + /// + /// Get all available event types + /// + public static List GetAllTypes() + { + return Config.Keys.ToList(); + } + } + + /// + /// Configuration record for event type visual properties + /// + public record EventTypeConfig(string Color, string Icon, string DisplayName); +} diff --git a/Aquiis.Professional/Core/Entities/CalendarSettings.cs b/Aquiis.Professional/Core/Entities/CalendarSettings.cs new file mode 100644 index 0000000..cf71c8d --- /dev/null +++ b/Aquiis.Professional/Core/Entities/CalendarSettings.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities; + +public class CalendarSettings : BaseModel +{ + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + public string EntityType { get; set; } = string.Empty; + public bool AutoCreateEvents { get; set; } = true; + public bool ShowOnCalendar { get; set; } = true; + public string? DefaultColor { get; set; } + public string? DefaultIcon { get; set; } + public int DisplayOrder { get; set; } +} diff --git a/Aquiis.Professional/Core/Entities/Checklist.cs b/Aquiis.Professional/Core/Entities/Checklist.cs new file mode 100644 index 0000000..e79eeca --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Checklist.cs @@ -0,0 +1,67 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + public class Checklist : BaseModel + { + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Display(Name = "Property ID")] + public Guid? PropertyId { get; set; } + + [Display(Name = "Lease ID")] + public Guid? LeaseId { get; set; } + + [RequiredGuid] + [Display(Name = "Checklist Template ID")] + public Guid ChecklistTemplateId { get; set; } + + [Required] + [StringLength(200)] + [Display(Name = "Checklist Name")] + public string Name { get; set; } = string.Empty; + + [Required] + [StringLength(50)] + [Display(Name = "Checklist Type")] + public string ChecklistType { get; set; } = string.Empty; + + [Required] + [StringLength(50)] + [Display(Name = "Status")] + public string Status { get; set; } = string.Empty; + + [StringLength(100)] + [Display(Name = "Completed By")] + public string? CompletedBy { get; set; } + + [Display(Name = "Completed On")] + public DateTime? CompletedOn { get; set; } + + [Display(Name = "Document ID")] + public Guid? DocumentId { get; set; } + + [StringLength(2000)] + [Display(Name = "General Notes")] + public string? GeneralNotes { get; set; } + + // Navigation properties + [ForeignKey(nameof(PropertyId))] + public virtual Property? Property { get; set; } + + [ForeignKey(nameof(LeaseId))] + public virtual Lease? Lease { get; set; } + + [ForeignKey(nameof(ChecklistTemplateId))] + public virtual ChecklistTemplate? ChecklistTemplate { get; set; } + + [ForeignKey(nameof(DocumentId))] + public virtual Document? Document { get; set; } + + public virtual ICollection Items { get; set; } = new List(); + } +} diff --git a/Aquiis.Professional/Core/Entities/ChecklistItem.cs b/Aquiis.Professional/Core/Entities/ChecklistItem.cs new file mode 100644 index 0000000..6cf99cb --- /dev/null +++ b/Aquiis.Professional/Core/Entities/ChecklistItem.cs @@ -0,0 +1,56 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + public class ChecklistItem : BaseModel + { + + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [RequiredGuid] + [Display(Name = "Checklist ID")] + public Guid ChecklistId { get; set; } + + [Required] + [StringLength(500)] + [Display(Name = "Item Text")] + public string ItemText { get; set; } = string.Empty; + + [Required] + [Display(Name = "Item Order")] + public int ItemOrder { get; set; } + + [StringLength(100)] + [Display(Name = "Category Section")] + public string? CategorySection { get; set; } + + [Display(Name = "Section Order")] + public int SectionOrder { get; set; } = 0; + + [Display(Name = "Requires Value")] + public bool RequiresValue { get; set; } = false; + + [StringLength(200)] + [Display(Name = "Value")] + public string? Value { get; set; } + + [StringLength(1000)] + [Display(Name = "Notes")] + public string? Notes { get; set; } + + [StringLength(500)] + [Display(Name = "Photo URL")] + public string? PhotoUrl { get; set; } + + [Display(Name = "Is Checked")] + public bool IsChecked { get; set; } = false; + + // Navigation properties + [ForeignKey(nameof(ChecklistId))] + public virtual Checklist? Checklist { get; set; } + } +} diff --git a/Aquiis.Professional/Core/Entities/ChecklistTemplate.cs b/Aquiis.Professional/Core/Entities/ChecklistTemplate.cs new file mode 100644 index 0000000..f1d1dea --- /dev/null +++ b/Aquiis.Professional/Core/Entities/ChecklistTemplate.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + public class ChecklistTemplate : BaseModel + { + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + [StringLength(100)] + [Display(Name = "Template Name")] + public string Name { get; set; } = string.Empty; + + [StringLength(500)] + [Display(Name = "Description")] + public string? Description { get; set; } + + [Required] + [StringLength(50)] + [Display(Name = "Category")] + public string Category { get; set; } = string.Empty; + + [Display(Name = "Is System Template")] + public bool IsSystemTemplate { get; set; } = false; + + // Navigation properties + public virtual ICollection Items { get; set; } = new List(); + public virtual ICollection Checklists { get; set; } = new List(); + } +} diff --git a/Aquiis.Professional/Core/Entities/ChecklistTemplateItem.cs b/Aquiis.Professional/Core/Entities/ChecklistTemplateItem.cs new file mode 100644 index 0000000..1d3916f --- /dev/null +++ b/Aquiis.Professional/Core/Entities/ChecklistTemplateItem.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + public class ChecklistTemplateItem : BaseModel + { + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [RequiredGuid] + [Display(Name = "Checklist Template ID")] + public Guid ChecklistTemplateId { get; set; } + + [Required] + [StringLength(500)] + [Display(Name = "Item Text")] + public string ItemText { get; set; } = string.Empty; + + [Required] + [Display(Name = "Item Order")] + public int ItemOrder { get; set; } + + [StringLength(100)] + [Display(Name = "Category Section")] + public string? CategorySection { get; set; } + + [Display(Name = "Section Order")] + public int SectionOrder { get; set; } = 0; + + [Display(Name = "Is Required")] + public bool IsRequired { get; set; } = false; + + [Display(Name = "Requires Value")] + public bool RequiresValue { get; set; } = false; + + [Display(Name = "Allows Notes")] + public bool AllowsNotes { get; set; } = true; + + // Navigation properties + [ForeignKey(nameof(ChecklistTemplateId))] + public virtual ChecklistTemplate? ChecklistTemplate { get; set; } + } +} diff --git a/Aquiis.Professional/Core/Entities/Document.cs b/Aquiis.Professional/Core/Entities/Document.cs new file mode 100644 index 0000000..47b109d --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Document.cs @@ -0,0 +1,84 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.Professional.Core.Entities { + + public class Document:BaseModel + { + + [Required] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + [StringLength(255)] + public string FileName { get; set; } = string.Empty; + + [Required] + [StringLength(10)] + public string FileExtension { get; set; } = string.Empty; // .pdf, .jpg, .docx, etc. + + [Required] + public byte[] FileData { get; set; } = Array.Empty(); + + [StringLength(255)] + public string FilePath { get; set; } = string.Empty; + + [StringLength(500)] + public string ContentType { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string FileType { get; set; } = string.Empty; // PDF, Image, etc. + + public long FileSize { get; set; } + + [Required] + [StringLength(100)] + public string DocumentType { get; set; } = string.Empty; // Lease Agreement, Invoice, Receipt, Photo, etc. + + [StringLength(500)] + public string Description { get; set; } = string.Empty; + + // Foreign keys - at least one must be set + public Guid? PropertyId { get; set; } + public Guid? TenantId { get; set; } + public Guid? LeaseId { get; set; } + public Guid? InvoiceId { get; set; } + public Guid? PaymentId { get; set; } + + // Navigation properties + [ForeignKey("PropertyId")] + public virtual Property? Property { get; set; } + + [ForeignKey("TenantId")] + public virtual Tenant? Tenant { get; set; } + + [ForeignKey("LeaseId")] + public virtual Lease? Lease { get; set; } + + [ForeignKey("InvoiceId")] + public virtual Invoice? Invoice { get; set; } + + [ForeignKey("PaymentId")] + public virtual Payment? Payment { get; set; } + + // Computed property + public string FileSizeFormatted + { + get + { + string[] sizes = { "B", "KB", "MB", "GB" }; + double len = FileSize; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/ISchedulableEntity.cs b/Aquiis.Professional/Core/Entities/ISchedulableEntity.cs new file mode 100644 index 0000000..c4c5698 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/ISchedulableEntity.cs @@ -0,0 +1,64 @@ +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Interface for entities that can be scheduled on the calendar. + /// Provides a contract for automatic calendar event creation and synchronization. + /// + public interface ISchedulableEntity + { + /// + /// Entity ID + /// + Guid Id { get; set; } + + /// + /// Organization ID + /// + Guid OrganizationId { get; set; } + + /// + /// Created By User ID + /// + string CreatedBy { get; set; } + + /// + /// Link to the associated CalendarEvent + /// + Guid? CalendarEventId { get; set; } + + /// + /// Get the title to display on the calendar + /// + string GetEventTitle(); + + /// + /// Get the start date/time of the event + /// + DateTime GetEventStart(); + + /// + /// Get the duration of the event in minutes + /// + int GetEventDuration(); + + /// + /// Get the event type (from CalendarEventTypes constants) + /// + string GetEventType(); + + /// + /// Get the associated property ID (if applicable) + /// + Guid? GetPropertyId(); + + /// + /// Get the description/details for the event + /// + string GetEventDescription(); + + /// + /// Get the current status of the event + /// + string GetEventStatus(); + } +} diff --git a/Aquiis.Professional/Core/Entities/IncomeStatement.cs b/Aquiis.Professional/Core/Entities/IncomeStatement.cs new file mode 100644 index 0000000..93ef50c --- /dev/null +++ b/Aquiis.Professional/Core/Entities/IncomeStatement.cs @@ -0,0 +1,107 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities; + +/// +/// Income statement for a specific period +/// +public class IncomeStatement +{ + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public Guid? PropertyId { get; set; } + public string? PropertyName { get; set; } + + // Income + public decimal TotalRentIncome { get; set; } + public decimal TotalOtherIncome { get; set; } + public decimal TotalIncome => TotalRentIncome + TotalOtherIncome; + + // Expenses + public decimal MaintenanceExpenses { get; set; } + public decimal UtilityExpenses { get; set; } + public decimal InsuranceExpenses { get; set; } + public decimal TaxExpenses { get; set; } + public decimal ManagementFees { get; set; } + public decimal OtherExpenses { get; set; } + public decimal TotalExpenses => MaintenanceExpenses + UtilityExpenses + InsuranceExpenses + + TaxExpenses + ManagementFees + OtherExpenses; + + // Net Income + public decimal NetIncome => TotalIncome - TotalExpenses; + public decimal ProfitMargin => TotalIncome > 0 ? (NetIncome / TotalIncome) * 100 : 0; +} + +/// +/// Rent roll item showing tenant and payment information +/// +public class RentRollItem +{ + [RequiredGuid] + public Guid PropertyId { get; set; } + public string PropertyName { get; set; } = string.Empty; + public string PropertyAddress { get; set; } = string.Empty; + public Guid? TenantId { get; set; } + public string? TenantName { get; set; } + public string LeaseStatus { get; set; } = string.Empty; + public DateTime? LeaseStartDate { get; set; } + public DateTime? LeaseEndDate { get; set; } + public decimal MonthlyRent { get; set; } + public decimal SecurityDeposit { get; set; } + public decimal TotalPaid { get; set; } + public decimal TotalDue { get; set; } + public decimal Balance => TotalDue - TotalPaid; + public string PaymentStatus => Balance <= 0 ? "Current" : "Outstanding"; +} + +/// +/// Property performance summary +/// +public class PropertyPerformance +{ + [RequiredGuid] + public Guid PropertyId { get; set; } + public string PropertyName { get; set; } = string.Empty; + public string PropertyAddress { get; set; } = string.Empty; + public decimal TotalIncome { get; set; } + public decimal TotalExpenses { get; set; } + public decimal NetIncome => TotalIncome - TotalExpenses; + public decimal ROI { get; set; } + public int OccupancyDays { get; set; } + public int TotalDays { get; set; } + public decimal OccupancyRate => TotalDays > 0 ? (decimal)OccupancyDays / TotalDays * 100 : 0; +} + +/// +/// Tax report data +/// +public class TaxReportData +{ + public int Year { get; set; } + public Guid? PropertyId { get; set; } + public string? PropertyName { get; set; } + public decimal TotalRentIncome { get; set; } + public decimal TotalExpenses { get; set; } + public decimal NetRentalIncome => TotalRentIncome - TotalExpenses; + public decimal DepreciationAmount { get; set; } + public decimal TaxableIncome => NetRentalIncome - DepreciationAmount; + + // Expense breakdown for Schedule E + public decimal Advertising { get; set; } + public decimal Auto { get; set; } + public decimal Cleaning { get; set; } + public decimal Insurance { get; set; } + public decimal Legal { get; set; } + public decimal Management { get; set; } + public decimal MortgageInterest { get; set; } + public decimal Repairs { get; set; } + public decimal Supplies { get; set; } + public decimal Taxes { get; set; } + public decimal Utilities { get; set; } + public decimal Other { get; set; } +} diff --git a/Aquiis.Professional/Core/Entities/Inspection.cs b/Aquiis.Professional/Core/Entities/Inspection.cs new file mode 100644 index 0000000..bb3727e --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Inspection.cs @@ -0,0 +1,153 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.Professional.Core.Entities +{ + + public class Inspection : BaseModel, ISchedulableEntity + { + [Required] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + public Guid PropertyId { get; set; } + + public Guid? CalendarEventId { get; set; } + + public Guid? LeaseId { get; set; } + + [Required] + public DateTime CompletedOn { get; set; } = DateTime.Now; + + [Required] + [StringLength(50)] + public string InspectionType { get; set; } = "Routine"; // Routine, Move-In, Move-Out, Maintenance + + [StringLength(100)] + public string? InspectedBy { get; set; } = string.Empty; + + // Exterior Checklist + public bool ExteriorRoofGood { get; set; } + public string? ExteriorRoofNotes { get; set; } + + public bool ExteriorGuttersGood { get; set; } + public string? ExteriorGuttersNotes { get; set; } + + public bool ExteriorSidingGood { get; set; } + public string? ExteriorSidingNotes { get; set; } + + public bool ExteriorWindowsGood { get; set; } + public string? ExteriorWindowsNotes { get; set; } + + public bool ExteriorDoorsGood { get; set; } + public string? ExteriorDoorsNotes { get; set; } + + public bool ExteriorFoundationGood { get; set; } + public string? ExteriorFoundationNotes { get; set; } + + public bool LandscapingGood { get; set; } + public string? LandscapingNotes { get; set; } + + // Interior Checklist + public bool InteriorWallsGood { get; set; } + public string? InteriorWallsNotes { get; set; } + + public bool InteriorCeilingsGood { get; set; } + public string? InteriorCeilingsNotes { get; set; } + + public bool InteriorFloorsGood { get; set; } + public string? InteriorFloorsNotes { get; set; } + + public bool InteriorDoorsGood { get; set; } + public string? InteriorDoorsNotes { get; set; } + + public bool InteriorWindowsGood { get; set; } + public string? InteriorWindowsNotes { get; set; } + + // Kitchen + public bool KitchenAppliancesGood { get; set; } + public string? KitchenAppliancesNotes { get; set; } + + public bool KitchenCabinetsGood { get; set; } + public string? KitchenCabinetsNotes { get; set; } + + public bool KitchenCountersGood { get; set; } + public string? KitchenCountersNotes { get; set; } + + public bool KitchenSinkPlumbingGood { get; set; } + public string? KitchenSinkPlumbingNotes { get; set; } + + // Bathroom + public bool BathroomToiletGood { get; set; } + public string? BathroomToiletNotes { get; set; } + + public bool BathroomSinkGood { get; set; } + public string? BathroomSinkNotes { get; set; } + + public bool BathroomTubShowerGood { get; set; } + public string? BathroomTubShowerNotes { get; set; } + + public bool BathroomVentilationGood { get; set; } + public string? BathroomVentilationNotes { get; set; } + + // Systems + public bool HvacSystemGood { get; set; } + public string? HvacSystemNotes { get; set; } + + public bool ElectricalSystemGood { get; set; } + public string? ElectricalSystemNotes { get; set; } + + public bool PlumbingSystemGood { get; set; } + public string? PlumbingSystemNotes { get; set; } + + public bool SmokeDetectorsGood { get; set; } + public string? SmokeDetectorsNotes { get; set; } + + public bool CarbonMonoxideDetectorsGood { get; set; } + public string? CarbonMonoxideDetectorsNotes { get; set; } + + // Overall Assessment + [Required] + [StringLength(20)] + public string OverallCondition { get; set; } = "Good"; // Excellent, Good, Fair, Poor + + [StringLength(2000)] + public string? GeneralNotes { get; set; } + + [StringLength(2000)] + public string? ActionItemsRequired { get; set; } + + // Generated PDF Document + public Guid? DocumentId { get; set; } + + // Navigation Properties + [ForeignKey("PropertyId")] + public Property? Property { get; set; } + + [ForeignKey("LeaseId")] + public Lease? Lease { get; set; } + + [ForeignKey("DocumentId")] + public Document? Document { get; set; } + + // Audit Fields + // SEE BASE MODEL + + // ISchedulableEntity implementation + public string GetEventTitle() => $"{InspectionType} Inspection: {Property?.Address ?? "Property"}"; + + public DateTime GetEventStart() => CompletedOn; + + public int GetEventDuration() => 60; // Default 1 hour for inspections + + public string GetEventType() => CalendarEventTypes.Inspection; + + public Guid? GetPropertyId() => PropertyId; + + public string GetEventDescription() => $"{InspectionType} - {OverallCondition}"; + + public string GetEventStatus() => OverallCondition; + } +} diff --git a/Aquiis.Professional/Core/Entities/Invoice.cs b/Aquiis.Professional/Core/Entities/Invoice.cs new file mode 100644 index 0000000..9cdc4df --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Invoice.cs @@ -0,0 +1,78 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.Professional.Core.Entities +{ + public class Invoice : BaseModel + { + + [Required] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + public Guid LeaseId { get; set; } + + [Required] + [StringLength(50)] + public string InvoiceNumber { get; set; } = string.Empty; + + [Required] + [DataType(DataType.Date)] + public DateTime InvoicedOn { get; set; } + + [Required] + [DataType(DataType.Date)] + public DateTime DueOn { get; set; } + + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal Amount { get; set; } + + [Required] + [StringLength(100)] + public string Description { get; set; } = string.Empty; + + [StringLength(50)] + public string Status { get; set; } = "Pending"; // Pending, Paid, Overdue, Cancelled + + public DateTime? PaidOn { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal AmountPaid { get; set; } + + [StringLength(500)] + public string Notes { get; set; } = string.Empty; + + // Late Fee Properties + [Column(TypeName = "decimal(18,2)")] + public decimal? LateFeeAmount { get; set; } + + public bool? LateFeeApplied { get; set; } + + public DateTime? LateFeeAppliedOn { get; set; } + + // Reminder Properties + public bool? ReminderSent { get; set; } + + public DateTime? ReminderSentOn { get; set; } + + // Document Tracking + public Guid? DocumentId { get; set; } + + // Navigation properties + [ForeignKey("LeaseId")] + public virtual Lease Lease { get; set; } = null!; + + [ForeignKey("DocumentId")] + public virtual Document? Document { get; set; } + + public virtual ICollection Payments { get; set; } = new List(); + + // Computed properties + public decimal BalanceDue => Amount - AmountPaid; + public bool IsOverdue => Status != "Paid" && DueOn < DateTime.Now; + public int DaysOverdue => IsOverdue ? (DateTime.Now - DueOn).Days : 0; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/Lease.cs b/Aquiis.Professional/Core/Entities/Lease.cs new file mode 100644 index 0000000..d354e32 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Lease.cs @@ -0,0 +1,113 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + + public class Lease : BaseModel + { + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [RequiredGuid] + public Guid PropertyId { get; set; } + + [RequiredGuid] + public Guid TenantId { get; set; } + + // Reference to the lease offer if this lease was created from an accepted offer + public Guid? LeaseOfferId { get; set; } + + [Required] + [DataType(DataType.Date)] + public DateTime StartDate { get; set; } + + [Required] + [DataType(DataType.Date)] + public DateTime EndDate { get; set; } + + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal MonthlyRent { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal SecurityDeposit { get; set; } + + [StringLength(50)] + public string Status { get; set; } = "Active"; // Active, Pending, Expired, Terminated + + [StringLength(1000)] + public string Terms { get; set; } = string.Empty; + + [StringLength(500)] + public string Notes { get; set; } = string.Empty; + + // Lease Offer & Acceptance Tracking + public DateTime? OfferedOn { get; set; } + + public DateTime? SignedOn { get; set; } + + public DateTime? DeclinedOn { get; set; } + + public DateTime? ExpiresOn { get; set; } // Lease offer expires 30 days from OfferedOn + + // Lease Renewal Tracking + public bool? RenewalNotificationSent { get; set; } + + public DateTime? RenewalNotificationSentOn { get; set; } + + public DateTime? RenewalReminderSentOn { get; set; } + + [StringLength(50)] + public string? RenewalStatus { get; set; } // NotRequired, Pending, Offered, Accepted, Declined, Expired + + public DateTime? RenewalOfferedOn { get; set; } + + public DateTime? RenewalResponseOn { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal? ProposedRenewalRent { get; set; } + + [StringLength(1000)] + public string? RenewalNotes { get; set; } + + // Lease Chain Tracking + public Guid? PreviousLeaseId { get; set; } + + public int RenewalNumber { get; set; } = 0; // 0 for original, 1 for first renewal, etc. + + // Termination Tracking + public DateTime? TerminationNoticedOn { get; set; } + + public DateTime? ExpectedMoveOutDate { get; set; } + + public DateTime? ActualMoveOutDate { get; set; } + + [StringLength(500)] + public string? TerminationReason { get; set; } + + // Document Tracking + public Guid? DocumentId { get; set; } + + // Navigation properties + [ForeignKey("PropertyId")] + public virtual Property Property { get; set; } = null!; + + [ForeignKey("TenantId")] + public virtual Tenant? Tenant { get; set; } + + [ForeignKey("DocumentId")] + public virtual Document? Document { get; set; } + + public virtual ICollection Invoices { get; set; } = new List(); + public virtual ICollection Documents { get; set; } = new List(); + + // Computed properties + public bool IsActive => Status == "Active" && DateTime.Now >= StartDate && DateTime.Now <= EndDate; + public int DaysRemaining => EndDate > DateTime.Now ? (EndDate - DateTime.Now).Days : 0; + public bool IsExpiringSoon => DaysRemaining > 0 && DaysRemaining <= 90; + public bool IsExpired => DateTime.Now > EndDate; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/LeaseOffer.cs b/Aquiis.Professional/Core/Entities/LeaseOffer.cs new file mode 100644 index 0000000..dfb8e04 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/LeaseOffer.cs @@ -0,0 +1,71 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.Professional.Core.Entities +{ + public class LeaseOffer : BaseModel + { + [Required] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + public Guid RentalApplicationId { get; set; } + + [Required] + public Guid PropertyId { get; set; } + + [Required] + public Guid ProspectiveTenantId { get; set; } + + [Required] + [DataType(DataType.Date)] + public DateTime StartDate { get; set; } + + [Required] + [DataType(DataType.Date)] + public DateTime EndDate { get; set; } + + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal MonthlyRent { get; set; } + + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal SecurityDeposit { get; set; } + + [Required] + [StringLength(2000)] + public string Terms { get; set; } = string.Empty; + + [StringLength(1000)] + public string Notes { get; set; } = string.Empty; + + [Required] + public DateTime OfferedOn { get; set; } + + [Required] + public DateTime ExpiresOn { get; set; } + + [StringLength(50)] + public string Status { get; set; } = "Pending"; // Pending, Accepted, Declined, Expired, Withdrawn + + public DateTime? RespondedOn { get; set; } + + [StringLength(500)] + public string? ResponseNotes { get; set; } + + public Guid? ConvertedLeaseId { get; set; } // Set when offer is accepted and converted to lease + + // Navigation properties + [ForeignKey("RentalApplicationId")] + public virtual RentalApplication RentalApplication { get; set; } = null!; + + [ForeignKey("PropertyId")] + public virtual Property Property { get; set; } = null!; + + [ForeignKey("ProspectiveTenantId")] + public virtual ProspectiveTenant ProspectiveTenant { get; set; } = null!; + } +} diff --git a/Aquiis.Professional/Core/Entities/MaintenanceRequest.cs b/Aquiis.Professional/Core/Entities/MaintenanceRequest.cs new file mode 100644 index 0000000..2d7a81b --- /dev/null +++ b/Aquiis.Professional/Core/Entities/MaintenanceRequest.cs @@ -0,0 +1,145 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + public class MaintenanceRequest : BaseModel, ISchedulableEntity + { + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [RequiredGuid] + public Guid PropertyId { get; set; } + + public Guid? CalendarEventId { get; set; } + + public Guid? LeaseId { get; set; } + + [Required] + [StringLength(100)] + public string Title { get; set; } = string.Empty; + + [Required] + [StringLength(2000)] + public string Description { get; set; } = string.Empty; + + [Required] + [StringLength(50)] + public string RequestType { get; set; } = string.Empty; // From ApplicationConstants.MaintenanceRequestTypes + + [Required] + [StringLength(20)] + public string Priority { get; set; } = "Medium"; // From ApplicationConstants.MaintenanceRequestPriorities + + [Required] + [StringLength(20)] + public string Status { get; set; } = "Submitted"; // From ApplicationConstants.MaintenanceRequestStatuses + + [StringLength(500)] + public string RequestedBy { get; set; } = string.Empty; // Name of person requesting + + [StringLength(100)] + public string RequestedByEmail { get; set; } = string.Empty; + + [StringLength(20)] + public string RequestedByPhone { get; set; } = string.Empty; + + public DateTime RequestedOn { get; set; } = DateTime.Today; + + public DateTime? ScheduledOn { get; set; } + + public DateTime? CompletedOn { get; set; } + [Column(TypeName = "decimal(18,2)")] + public decimal EstimatedCost { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal ActualCost { get; set; } + + [StringLength(100)] + public string AssignedTo { get; set; } = string.Empty; // Contractor or maintenance person + + [StringLength(2000)] + public string ResolutionNotes { get; set; } = string.Empty; + + // Navigation properties + public virtual Property? Property { get; set; } + public virtual Lease? Lease { get; set; } + + // Computed property for days open + [NotMapped] + public int DaysOpen + { + get + { + if (CompletedOn.HasValue) + return (CompletedOn.Value.Date - RequestedOn.Date).Days; + + return (DateTime.Today - RequestedOn.Date).Days; + } + } + + [NotMapped] + public bool IsOverdue + { + get + { + if (Status == "Completed" || Status == "Cancelled") + return false; + + if (!ScheduledOn.HasValue) + return false; + + return DateTime.Today > ScheduledOn.Value.Date; + } + } + + [NotMapped] + public string PriorityBadgeClass + { + get + { + return Priority switch + { + "Urgent" => "bg-danger", + "High" => "bg-warning", + "Medium" => "bg-info", + "Low" => "bg-secondary", + _ => "bg-secondary" + }; + } + } + + [NotMapped] + public string StatusBadgeClass + { + get + { + return Status switch + { + "Submitted" => "bg-primary", + "In Progress" => "bg-warning", + "Completed" => "bg-success", + "Cancelled" => "bg-secondary", + _ => "bg-secondary" + }; + } + } + + // ISchedulableEntity implementation + public string GetEventTitle() => $"{RequestType}: {Title}"; + + public DateTime GetEventStart() => ScheduledOn ?? RequestedOn; + + public int GetEventDuration() => 120; // Default 2 hours for maintenance + + public string GetEventType() => CalendarEventTypes.Maintenance; + + public Guid? GetPropertyId() => PropertyId; + + public string GetEventDescription() => $"{Property?.Address ?? "Property"} - {Priority} Priority"; + + public string GetEventStatus() => Status; + } +} diff --git a/Aquiis.Professional/Core/Entities/Note.cs b/Aquiis.Professional/Core/Entities/Note.cs new file mode 100644 index 0000000..181f442 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Note.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Shared.Components.Account; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Represents a timeline note/comment that can be attached to any entity + /// + public class Note : BaseModel + { + [Required] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + [StringLength(5000)] + [Display(Name = "Content")] + public string Content { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + [Display(Name = "Entity Type")] + public string EntityType { get; set; } = string.Empty; + + [Required] + [Display(Name = "Entity ID")] + public Guid EntityId { get; set; } + + [StringLength(100)] + [Display(Name = "User Full Name")] + public string? UserFullName { get; set; } + + // Navigation to user who created the note + [ForeignKey(nameof(CreatedBy))] + public virtual ApplicationUser? User { get; set; } + } +} diff --git a/Aquiis.Professional/Core/Entities/Notification.cs b/Aquiis.Professional/Core/Entities/Notification.cs new file mode 100644 index 0000000..3f81476 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Notification.cs @@ -0,0 +1,64 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Validation; + +public class Notification : BaseModel +{ + [RequiredGuid] + public Guid OrganizationId { get; set; } + + [Required] + [StringLength(200)] + public string Title { get; set; } = string.Empty; + + [Required] + [StringLength(2000)] + public string Message { get; set; } = string.Empty; + + [Required] + [StringLength(50)] + public string Type { get; set; } = string.Empty; // Info, Warning, Error, Success + + [Required] + [StringLength(50)] + public string Category { get; set; } = string.Empty; // Lease, Payment, Maintenance, Application + + [Required] + public string RecipientUserId { get; set; } = string.Empty; + + [Required] + public DateTime SentOn { get; set; } + + public DateTime? ReadOn { get; set; } + + public bool IsRead { get; set; } + + // Optional entity reference for "view details" link + public Guid? RelatedEntityId { get; set; } + + [StringLength(50)] + public string? RelatedEntityType { get; set; } + + // Delivery channels + public bool SendInApp { get; set; } = true; + public bool SendEmail { get; set; } + public bool SendSMS { get; set; } + + // Delivery status + public bool EmailSent { get; set; } + public DateTime? EmailSentOn { get; set; } + + public bool SMSSent { get; set; } + public DateTime? SMSSentOn { get; set; } + + [StringLength(500)] + public string? EmailError { get; set; } + + [StringLength(500)] + public string? SMSError { get; set; } + + // Navigation + [ForeignKey(nameof(OrganizationId))] + public virtual Organization? Organization { get; set; } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/NotificationPreferences.cs b/Aquiis.Professional/Core/Entities/NotificationPreferences.cs new file mode 100644 index 0000000..a9cbe51 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/NotificationPreferences.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities; + +public class NotificationPreferences : BaseModel +{ + [RequiredGuid] + public Guid OrganizationId { get; set; } + + [Required] + public string UserId { get; set; } = string.Empty; + + // In-App Notification Preferences + public bool EnableInAppNotifications { get; set; } = true; + + // Email Preferences + public bool EnableEmailNotifications { get; set; } = true; + + [StringLength(200)] + public string? EmailAddress { get; set; } + + public bool EmailLeaseExpiring { get; set; } = true; + public bool EmailPaymentDue { get; set; } = true; + public bool EmailPaymentReceived { get; set; } = true; + public bool EmailApplicationStatusChange { get; set; } = true; + public bool EmailMaintenanceUpdate { get; set; } = true; + public bool EmailInspectionScheduled { get; set; } = true; + + // SMS Preferences + public bool EnableSMSNotifications { get; set; } = false; + + [StringLength(20)] + public string? PhoneNumber { get; set; } + + public bool SMSPaymentDue { get; set; } = false; + public bool SMSMaintenanceEmergency { get; set; } = true; + public bool SMSLeaseExpiringUrgent { get; set; } = false; // 30 days or less + + // Digest Preferences + public bool EnableDailyDigest { get; set; } = false; + public TimeSpan DailyDigestTime { get; set; } = new TimeSpan(9, 0, 0); // 9 AM + + public bool EnableWeeklyDigest { get; set; } = false; + public DayOfWeek WeeklyDigestDay { get; set; } = DayOfWeek.Monday; + + // Navigation + [ForeignKey(nameof(OrganizationId))] + public virtual Organization? Organization { get; set; } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/OperationResult.cs b/Aquiis.Professional/Core/Entities/OperationResult.cs new file mode 100644 index 0000000..a7e8c7c --- /dev/null +++ b/Aquiis.Professional/Core/Entities/OperationResult.cs @@ -0,0 +1,24 @@ +namespace Aquiis.Professional.Core.Entities +{ + public class OperationResult + { + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + public List Errors { get; set; } = new(); + + public static OperationResult SuccessResult(string message = "Operation completed successfully") + { + return new OperationResult { Success = true, Message = message }; + } + + public static OperationResult FailureResult(string message, List? errors = null) + { + return new OperationResult + { + Success = false, + Message = message, + Errors = errors ?? new List() + }; + } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/Organization.cs b/Aquiis.Professional/Core/Entities/Organization.cs new file mode 100644 index 0000000..34116a5 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Organization.cs @@ -0,0 +1,49 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + public class Organization + { + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid Id { get; set; } = Guid.Empty; + + /// + /// UserId of the account owner who created this organization + /// + public string OwnerId { get; set; } = string.Empty; + + /// + /// Full organization name (e.g., "California Properties LLC") + /// + public string Name { get; set; } = string.Empty; + + /// + /// Short display name for UI (e.g., "CA Properties") + /// + public string? DisplayName { get; set; } + + /// + /// US state code (CA, TX, FL, etc.) - determines applicable regulations + /// + public string? State { get; set; } + + /// + /// Active/inactive flag for soft delete + /// + public bool IsActive { get; set; } = true; + + public string CreatedBy { get; set; } = string.Empty; + public DateTime CreatedOn { get; set; } = DateTime.UtcNow; + public string? LastModifiedBy { get; set; } = string.Empty; + public DateTime? LastModifiedOn { get; set; } + public bool IsDeleted { get; set; } = false; + + // Navigation properties + public virtual ICollection UserOrganizations { get; set; } = new List(); + public virtual ICollection Properties { get; set; } = new List(); + public virtual ICollection Tenants { get; set; } = new List(); + public virtual ICollection Leases { get; set; } = new List(); + } +} diff --git a/Aquiis.Professional/Core/Entities/OrganizationEmailSettings.cs b/Aquiis.Professional/Core/Entities/OrganizationEmailSettings.cs new file mode 100644 index 0000000..f8ce643 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/OrganizationEmailSettings.cs @@ -0,0 +1,64 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Stores SendGrid email configuration per organization. + /// Each organization manages their own SendGrid account. + /// + public class OrganizationEmailSettings : BaseModel + { + [RequiredGuid] + public Guid OrganizationId { get; set; } + + // SendGrid Configuration + public bool IsEmailEnabled { get; set; } + + /// + /// Encrypted SendGrid API key using Data Protection API + /// + [StringLength(1000)] + public string? SendGridApiKeyEncrypted { get; set; } + + [StringLength(200)] + [EmailAddress] + public string? FromEmail { get; set; } + + [StringLength(200)] + public string? FromName { get; set; } + + // Email Usage Tracking (local cache) + public int EmailsSentToday { get; set; } + public int EmailsSentThisMonth { get; set; } + public DateTime? LastEmailSentOn { get; set; } + public DateTime? StatsLastUpdatedOn { get; set; } + public DateTime? DailyCountResetOn { get; set; } + public DateTime? MonthlyCountResetOn { get; set; } + + // SendGrid Account Info (cached from API) + public int? DailyLimit { get; set; } + public int? MonthlyLimit { get; set; } + + [StringLength(100)] + public string? PlanType { get; set; } // Free, Essentials, Pro, etc. + + // Verification Status + public bool IsVerified { get; set; } + public DateTime? LastVerifiedOn { get; set; } + + /// + /// Last error encountered when sending email or verifying API key + /// + [StringLength(1000)] + public string? LastError { get; set; } + + public DateTime? LastErrorOn { get; set; } + + // Navigation + [ForeignKey(nameof(OrganizationId))] + public virtual Organization? Organization { get; set; } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/OrganizationSMSSettings.cs b/Aquiis.Professional/Core/Entities/OrganizationSMSSettings.cs new file mode 100644 index 0000000..cdd674b --- /dev/null +++ b/Aquiis.Professional/Core/Entities/OrganizationSMSSettings.cs @@ -0,0 +1,65 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Stores Twilio SMS configuration per organization. + /// Each organization manages their own Twilio account. + /// + public class OrganizationSMSSettings : BaseModel + { + [RequiredGuid] + public Guid OrganizationId { get; set; } + + // Twilio Configuration + public bool IsSMSEnabled { get; set; } + + /// + /// Encrypted Twilio Account SID using Data Protection API + /// + [StringLength(1000)] + public string? TwilioAccountSidEncrypted { get; set; } + + /// + /// Encrypted Twilio Auth Token using Data Protection API + /// + [StringLength(1000)] + public string? TwilioAuthTokenEncrypted { get; set; } + + [StringLength(20)] + [Phone] + public string? TwilioPhoneNumber { get; set; } + + // SMS Usage Tracking (local cache) + public int SMSSentToday { get; set; } + public int SMSSentThisMonth { get; set; } + public DateTime? LastSMSSentOn { get; set; } + public DateTime? StatsLastUpdatedOn { get; set; } + public DateTime? DailyCountResetOn { get; set; } + public DateTime? MonthlyCountResetOn { get; set; } + + // Twilio Account Info (cached from API) + public decimal? AccountBalance { get; set; } + public decimal? CostPerSMS { get; set; } // Approximate cost + + [StringLength(100)] + public string? AccountType { get; set; } // Trial, Paid + + // Verification Status + public bool IsVerified { get; set; } + public DateTime? LastVerifiedOn { get; set; } + + /// + /// Last error encountered when sending SMS or verifying credentials + /// + [StringLength(1000)] + public string? LastError { get; set; } + + // Navigation + [ForeignKey(nameof(OrganizationId))] + public virtual Organization? Organization { get; set; } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/OrganizationSettings.cs b/Aquiis.Professional/Core/Entities/OrganizationSettings.cs new file mode 100644 index 0000000..4a108d1 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/OrganizationSettings.cs @@ -0,0 +1,130 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Organization-specific settings for late fees, payment reminders, and other configurable features. + /// Each organization can have different policies for their property management operations. + /// + public class OrganizationSettings : BaseModel + { + + [Required] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [MaxLength(200)] + public string? Name { get; set; } + + #region Late Fee Settings + + [Display(Name = "Enable Late Fees")] + public bool LateFeeEnabled { get; set; } = true; + + [Display(Name = "Auto-Apply Late Fees")] + public bool LateFeeAutoApply { get; set; } = true; + + [Required] + [Range(0, 30)] + [Display(Name = "Grace Period (Days)")] + public int LateFeeGracePeriodDays { get; set; } = 3; + + [Required] + [Range(0, 1)] + [Display(Name = "Late Fee Percentage")] + public decimal LateFeePercentage { get; set; } = 0.05m; + + [Required] + [Range(0, 10000)] + [Display(Name = "Maximum Late Fee Amount")] + public decimal MaxLateFeeAmount { get; set; } = 50.00m; + + #endregion + + #region Payment Reminder Settings + + [Display(Name = "Enable Payment Reminders")] + public bool PaymentReminderEnabled { get; set; } = true; + + [Required] + [Range(1, 30)] + [Display(Name = "Send Reminder (Days Before Due)")] + public int PaymentReminderDaysBefore { get; set; } = 3; + + #endregion + + #region Tour Settings + + [Required] + [Range(1, 168)] + [Display(Name = "Tour No-Show Grace Period (Hours)")] + public int TourNoShowGracePeriodHours { get; set; } = 24; + + #endregion + + #region Application Fee Settings + + [Display(Name = "Enable Application Fees")] + public bool ApplicationFeeEnabled { get; set; } = true; + + [Required] + [Range(0, 1000)] + [Display(Name = "Default Application Fee")] + public decimal DefaultApplicationFee { get; set; } = 50.00m; + + [Required] + [Range(1, 90)] + [Display(Name = "Application Expiration (Days)")] + public int ApplicationExpirationDays { get; set; } = 30; + + #endregion + + #region Security Deposit Settings + + [Display(Name = "Enable Security Deposit Investment Pool")] + public bool SecurityDepositInvestmentEnabled { get; set; } = true; + + [Required] + [Range(0, 1)] + [Display(Name = "Organization Share Percentage")] + [Column(TypeName = "decimal(18,6)")] + public decimal OrganizationSharePercentage { get; set; } = 0.20m; // Default 20% + + [Display(Name = "Auto-Calculate Security Deposit from Rent")] + public bool AutoCalculateSecurityDeposit { get; set; } = true; + + [Required] + [Range(0.5, 3.0)] + [Display(Name = "Security Deposit Multiplier")] + [Column(TypeName = "decimal(18,2)")] + public decimal SecurityDepositMultiplier { get; set; } = 1.0m; // Default 1x monthly rent + + [Required] + [Range(1, 12)] + [Display(Name = "Refund Processing Days")] + public int RefundProcessingDays { get; set; } = 30; // Days after move-out to process refund + + [Required] + [Range(1, 12)] + [Display(Name = "Dividend Distribution Month")] + public int DividendDistributionMonth { get; set; } = 1; // January = 1 + + [Display(Name = "Allow Tenant Choice for Dividend Payment")] + public bool AllowTenantDividendChoice { get; set; } = true; + + [Display(Name = "Default Dividend Payment Method")] + [StringLength(50)] + public string DefaultDividendPaymentMethod { get; set; } = "LeaseCredit"; // LeaseCredit or Check + + #endregion + + // Future settings can be added here as new regions: + // - Default lease terms + // - Routine inspection intervals + // - Document retention policies + // - etc. + } +} diff --git a/Aquiis.Professional/Core/Entities/Payment.cs b/Aquiis.Professional/Core/Entities/Payment.cs new file mode 100644 index 0000000..2f1e6c5 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Payment.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.Professional.Core.Entities { + + public class Payment : BaseModel + { + [Required] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + public Guid InvoiceId { get; set; } + + [Required] + [DataType(DataType.Date)] + public DateTime PaidOn { get; set; } + + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal Amount { get; set; } + + [StringLength(50)] + public string PaymentMethod { get; set; } = string.Empty; // e.g., Cash, Check, CreditCard, BankTransfer + + [StringLength(1000)] + public string Notes { get; set; } = string.Empty; + + // Document Tracking + public Guid? DocumentId { get; set; } + + // Navigation properties + [ForeignKey("InvoiceId")] + public virtual Invoice Invoice { get; set; } = null!; + + [ForeignKey("DocumentId")] + public virtual Document? Document { get; set; } + + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/Property.cs b/Aquiis.Professional/Core/Entities/Property.cs new file mode 100644 index 0000000..fd1bbd7 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Property.cs @@ -0,0 +1,188 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using Aquiis.Professional.Core.Constants; + +namespace Aquiis.Professional.Core.Entities +{ + public class Property : BaseModel + { + [Required] + [JsonInclude] + [StringLength(100)] + [DataType(DataType.Text)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + [JsonInclude] + [StringLength(200)] + [DataType(DataType.Text)] + [Display(Name = "Street Address", Description = "Street address of the property", + Prompt = "e.g., 123 Main St", ShortName = "Address")] + public string Address { get; set; } = string.Empty; + + [StringLength(50)] + [JsonInclude] + [DataType(DataType.Text)] + [Display(Name = "Unit Number", Description = "Optional unit or apartment number", + Prompt = "e.g., Apt 2B, Unit 101", ShortName = "Unit")] + public string? UnitNumber { get; set; } + + [StringLength(100)] + [JsonInclude] + [DataType(DataType.Text)] + [Display(Name = "City", Description = "City where the property is located", + Prompt = "e.g., Los Angeles, New York, Chicago", ShortName = "City")] + public string City { get; set; } = string.Empty; + + [StringLength(50)] + [JsonInclude] + [DataType(DataType.Text)] + [Display(Name = "State", Description = "State or province where the property is located", + Prompt = "e.g., CA, NY, TX", ShortName = "State")] + public string State { get; set; } = string.Empty; + + [StringLength(10)] + [JsonInclude] + [RegularExpression(@"^\d{5}(-\d{4})?$", ErrorMessage = "Invalid Zip Code format.")] + [DataType(DataType.PostalCode)] + [Display(Name = "Postal Code", Description = "Postal code for the property", + Prompt = "e.g., 12345 or 12345-6789", ShortName = "ZIP")] + [MaxLength(10, ErrorMessage = "Zip Code cannot exceed 10 characters.")] + public string ZipCode { get; set; } = string.Empty; + + [Required] + [JsonInclude] + [StringLength(50)] + [DataType(DataType.Text)] + [Display(Name = "Property Type", Description = "Type of the property", + Prompt = "e.g., House, Apartment, Condo", ShortName = "Type")] + public string PropertyType { get; set; } = string.Empty; // House, Apartment, Condo, etc. + + [JsonInclude] + [Column(TypeName = "decimal(18,2)")] + [DataType(DataType.Currency)] + [Display(Name = "Monthly Rent", Description = "Monthly rental amount for the property", + Prompt = "e.g., 1200.00", ShortName = "Rent")] + public decimal MonthlyRent { get; set; } + + [JsonInclude] + [Range(0, int.MaxValue, ErrorMessage = "Bedrooms must be a non-negative number.")] + [DataType(DataType.Text)] + [Display(Name = "Bedrooms", Description = "Number of Bedrooms", + Prompt = "e.g., 3", ShortName = "Beds")] + [MaxLength(3, ErrorMessage = "Bedrooms cannot exceed 3 digits.")] + public int Bedrooms { get; set; } + + + [JsonInclude] + [Column(TypeName = "decimal(3,1)")] + [DataType(DataType.Text)] + [MaxLength(3, ErrorMessage = "Bathrooms cannot exceed 3 digits.")] + [Display(Name = "Bathrooms", Description = "Number of Bathrooms", + Prompt = "e.g., 1.5 for one and a half bathrooms", ShortName = "Baths")] + public decimal Bathrooms { get; set; } + + + [JsonInclude] + [Range(0, int.MaxValue, ErrorMessage = "Square Feet must be a non-negative number.")] + [DataType(DataType.Text)] + [MaxLength(7, ErrorMessage = "Square Feet cannot exceed 7 digits.")] + [Display(Name = "Square Feet", Description = "Total square footage of the property", + Prompt = "e.g., 1500", ShortName = "Sq. Ft.")] + public int SquareFeet { get; set; } + + + [JsonInclude] + [StringLength(1000)] + [Display(Name = "Description", Description = "Detailed description of the property", + Prompt = "Provide additional details about the property", ShortName = "Desc.")] + [DataType(DataType.MultilineText)] + [MaxLength(1000, ErrorMessage = "Description cannot exceed 1000 characters.")] + public string Description { get; set; } = string.Empty; + + [JsonInclude] + [Display(Name = "Is Available?", Description = "Indicates if the property is currently available for lease")] + public bool IsAvailable { get; set; } = true; + + [JsonInclude] + [StringLength(50)] + [Display(Name = "Property Status", Description = "Current status in the rental lifecycle")] + public string Status { get; set; } = ApplicationConstants.PropertyStatuses.Available; + + // Inspection tracking + + + [JsonInclude] + public DateTime? LastRoutineInspectionDate { get; set; } + [JsonInclude] + public DateTime? NextRoutineInspectionDueDate { get; set; } + [JsonInclude] + public int RoutineInspectionIntervalMonths { get; set; } = 12; // Default to annual inspections + + // Navigation properties + public virtual ICollection Leases { get; set; } = new List(); + public virtual ICollection Documents { get; set; } = new List(); + + // Computed property for pending application count + [NotMapped] + [JsonInclude] + public int PendingApplicationCount => 0; // Will be populated when RentalApplications are added + + // Computed property for inspection status + [NotMapped] + public bool IsInspectionOverdue + { + get + { + if (!NextRoutineInspectionDueDate.HasValue) + return false; + + return DateTime.Today >= NextRoutineInspectionDueDate.Value.Date; + } + } + + [NotMapped] + public int DaysUntilInspectionDue + { + get + { + if (!NextRoutineInspectionDueDate.HasValue) + return 0; + + return (NextRoutineInspectionDueDate.Value.Date - DateTime.Today).Days; + } + } + + [NotMapped] + public int DaysOverdue + { + get + { + if (!IsInspectionOverdue) + return 0; + + return (DateTime.Today - NextRoutineInspectionDueDate!.Value.Date).Days; + } + } + + [NotMapped] + public string InspectionStatus + { + get + { + if (!NextRoutineInspectionDueDate.HasValue) + return "Not Scheduled"; + + if (IsInspectionOverdue) + return "Overdue"; + + if (DaysUntilInspectionDue <= 30) + return "Due Soon"; + + return "Scheduled"; + } + } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/ProspectiveTenant.cs b/Aquiis.Professional/Core/Entities/ProspectiveTenant.cs new file mode 100644 index 0000000..22a1939 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/ProspectiveTenant.cs @@ -0,0 +1,87 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.Professional.Core.Entities +{ + public class ProspectiveTenant : BaseModel + { + [Required] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + [StringLength(100)] + [Display(Name = "First Name")] + public string FirstName { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + [Display(Name = "Last Name")] + public string LastName { get; set; } = string.Empty; + + [Required] + [StringLength(200)] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } = string.Empty; + + [Required] + [StringLength(20)] + [Phone] + [Display(Name = "Phone")] + public string Phone { get; set; } = string.Empty; + + [DataType(DataType.Date)] + [Display(Name = "Date of Birth")] + public DateTime? DateOfBirth { get; set; } + + [StringLength(100)] + [Display(Name = "Identification Number")] + public string? IdentificationNumber { get; set; } + + [StringLength(2)] + [Display(Name = "Identification State")] + public string? IdentificationState { get; set; } + + [Required] + [StringLength(50)] + [Display(Name = "Status")] + public string Status { get; set; } = string.Empty; // Lead, TourScheduled, Applied, Screening, Approved, Denied, ConvertedToTenant + + [StringLength(100)] + [Display(Name = "Source")] + public string? Source { get; set; } // Website, Referral, Walk-in, Zillow, etc. + + [StringLength(2000)] + [Display(Name = "Notes")] + public string? Notes { get; set; } + + [Display(Name = "Interested Property")] + public Guid? InterestedPropertyId { get; set; } + + [Display(Name = "Desired Move-In Date")] + public DateTime? DesiredMoveInDate { get; set; } + + [Display(Name = "First Contact Date")] + public DateTime? FirstContactedOn { get; set; } + + + + // Computed Property + [NotMapped] + public string FullName => $"{FirstName} {LastName}"; + + // Navigation properties + [ForeignKey(nameof(InterestedPropertyId))] + public virtual Property? InterestedProperty { get; set; } + + public virtual ICollection Tours { get; set; } = new List(); + + /// + /// Collection of all applications submitted by this prospect. + /// A prospect may have multiple applications over time, but only one "active" (non-disposed) application. + /// + public virtual ICollection Applications { get; set; } = new List(); + } +} diff --git a/Aquiis.Professional/Core/Entities/RentalApplication.cs b/Aquiis.Professional/Core/Entities/RentalApplication.cs new file mode 100644 index 0000000..54c5578 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/RentalApplication.cs @@ -0,0 +1,161 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Aquiis.Professional.Core.Entities +{ + public class RentalApplication : BaseModel + { + [Required] + [JsonInclude] + [StringLength(100)] + [DataType(DataType.Text)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + [Display(Name = "Prospective Tenant")] + public Guid ProspectiveTenantId { get; set; } + + [Required] + [Display(Name = "Property")] + public Guid PropertyId { get; set; } + + [Required] + [Display(Name = "Applied On")] + public DateTime AppliedOn { get; set; } + + [Required] + [StringLength(50)] + [Display(Name = "Status")] + public string Status { get; set; } = string.Empty; // Submitted, UnderReview, Screening, Approved, Denied + + // Current Address + [Required] + [StringLength(200)] + [Display(Name = "Current Address")] + public string CurrentAddress { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + [Display(Name = "City")] + public string CurrentCity { get; set; } = string.Empty; + + [Required] + [StringLength(2)] + [Display(Name = "State")] + public string CurrentState { get; set; } = string.Empty; + + [Required] + [StringLength(10)] + [Display(Name = "Zip Code")] + public string CurrentZipCode { get; set; } = string.Empty; + + [Required] + [Display(Name = "Current Rent")] + [Column(TypeName = "decimal(18,2)")] + public decimal CurrentRent { get; set; } + + [Required] + [StringLength(200)] + [Display(Name = "Landlord Name")] + public string LandlordName { get; set; } = string.Empty; + + [Required] + [StringLength(20)] + [Phone] + [Display(Name = "Landlord Phone")] + public string LandlordPhone { get; set; } = string.Empty; + + // Employment + [Required] + [StringLength(200)] + [Display(Name = "Employer Name")] + public string EmployerName { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + [Display(Name = "Job Title")] + public string JobTitle { get; set; } = string.Empty; + + [Required] + [Display(Name = "Monthly Income")] + [Column(TypeName = "decimal(18,2)")] + public decimal MonthlyIncome { get; set; } + + [Required] + [Display(Name = "Employment Length (Months)")] + public int EmploymentLengthMonths { get; set; } + + // References + [Required] + [StringLength(200)] + [Display(Name = "Reference 1 - Name")] + public string Reference1Name { get; set; } = string.Empty; + + [Required] + [StringLength(20)] + [Phone] + [Display(Name = "Reference 1 - Phone")] + public string Reference1Phone { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + [Display(Name = "Reference 1 - Relationship")] + public string Reference1Relationship { get; set; } = string.Empty; + + [StringLength(200)] + [Display(Name = "Reference 2 - Name")] + public string? Reference2Name { get; set; } + + [StringLength(20)] + [Phone] + [Display(Name = "Reference 2 - Phone")] + public string? Reference2Phone { get; set; } + + [StringLength(100)] + [Display(Name = "Reference 2 - Relationship")] + public string? Reference2Relationship { get; set; } + + // Fees + [Required] + [Display(Name = "Application Fee")] + [Column(TypeName = "decimal(18,2)")] + public decimal ApplicationFee { get; set; } + + [Display(Name = "Application Fee Paid")] + public bool ApplicationFeePaid { get; set; } + + [Display(Name = "Fee Paid On")] + public DateTime? ApplicationFeePaidOn { get; set; } + + [StringLength(50)] + [Display(Name = "Payment Method")] + public string? ApplicationFeePaymentMethod { get; set; } + + [Display(Name = "Expires On")] + public DateTime? ExpiresOn { get; set; } + + // Decision + [StringLength(1000)] + [Display(Name = "Denial Reason")] + public string? DenialReason { get; set; } + + [Display(Name = "Decided On")] + public DateTime? DecidedOn { get; set; } + + [StringLength(100)] + [Display(Name = "Decision By")] + public string? DecisionBy { get; set; } // UserId + + + // Navigation properties + [ForeignKey(nameof(ProspectiveTenantId))] + public virtual ProspectiveTenant? ProspectiveTenant { get; set; } + + [ForeignKey(nameof(PropertyId))] + public virtual Property? Property { get; set; } + + public virtual ApplicationScreening? Screening { get; set; } + } +} diff --git a/Aquiis.Professional/Core/Entities/SchemaVersion.cs b/Aquiis.Professional/Core/Entities/SchemaVersion.cs new file mode 100644 index 0000000..6028057 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/SchemaVersion.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Tracks the database schema version for compatibility validation + /// + public class SchemaVersion + { + [Key] + [JsonInclude] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Required] + [StringLength(50)] + public string Version { get; set; } = string.Empty; + + public DateTime AppliedOn { get; set; } = DateTime.UtcNow; + + [StringLength(500)] + public string Description { get; set; } = string.Empty; + } +} diff --git a/Aquiis.Professional/Core/Entities/SecurityDeposit.cs b/Aquiis.Professional/Core/Entities/SecurityDeposit.cs new file mode 100644 index 0000000..8f701e0 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/SecurityDeposit.cs @@ -0,0 +1,100 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Security deposit tracking for each lease with complete lifecycle management. + /// Tracks deposit collection, investment pool participation, and refund disposition. + /// + public class SecurityDeposit : BaseModel + { + [Required] + [JsonInclude] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + [JsonInclude] + public Guid LeaseId { get; set; } + + [Required] + [JsonInclude] + public Guid TenantId { get; set; } + + [Required] + [Column(TypeName = "decimal(18,2)")] + [Range(0.01, double.MaxValue, ErrorMessage = "Deposit amount must be greater than 0")] + public decimal Amount { get; set; } + + [Required] + public DateTime DateReceived { get; set; } = DateTime.UtcNow; + + [Required] + [StringLength(50)] + public string PaymentMethod { get; set; } = string.Empty; // Check, Cash, Bank Transfer, etc. + + [StringLength(100)] + public string? TransactionReference { get; set; } // Check number, transfer ID, etc. + + [Required] + [StringLength(50)] + public string Status { get; set; } = "Held"; // Held, Released, Refunded, Forfeited, PartiallyRefunded + + /// + /// Tracks whether this deposit is included in the investment pool for dividend calculation. + /// Set to true when lease becomes active and deposit is added to pool. + /// + public bool InInvestmentPool { get; set; } = false; + + /// + /// Date when deposit was added to investment pool (typically lease start date). + /// Used for pro-rating dividend calculations for mid-year move-ins. + /// + public DateTime? PoolEntryDate { get; set; } + + /// + /// Date when deposit was removed from investment pool (typically lease end date). + /// Used to stop dividend accrual. + /// + public DateTime? PoolExitDate { get; set; } + + // Refund Tracking + public DateTime? RefundProcessedDate { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal? RefundAmount { get; set; } + + [Column(TypeName = "decimal(18,2)")] + public decimal? DeductionsAmount { get; set; } + + [StringLength(1000)] + public string? DeductionsReason { get; set; } + + [StringLength(50)] + public string? RefundMethod { get; set; } // Check, Bank Transfer, Applied to Balance + + [StringLength(100)] + public string? RefundReference { get; set; } // Check number, transfer ID + + [StringLength(500)] + public string? Notes { get; set; } + + // Navigation properties + [ForeignKey("LeaseId")] + public virtual Lease Lease { get; set; } = null!; + + [ForeignKey("TenantId")] + public virtual Tenant Tenant { get; set; } = null!; + + public virtual ICollection Dividends { get; set; } = new List(); + + // Computed properties + public bool IsRefunded => Status == "Refunded" || Status == "PartiallyRefunded"; + public bool IsActive => Status == "Held" && InInvestmentPool; + public decimal TotalDividendsEarned => Dividends.Sum(d => d.DividendAmount); + public decimal NetRefundDue => Amount + TotalDividendsEarned - (DeductionsAmount ?? 0); + } +} diff --git a/Aquiis.Professional/Core/Entities/SecurityDepositDividend.cs b/Aquiis.Professional/Core/Entities/SecurityDepositDividend.cs new file mode 100644 index 0000000..4d41278 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/SecurityDepositDividend.cs @@ -0,0 +1,114 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Individual dividend payment tracking for each lease's security deposit. + /// Dividends are calculated annually and distributed based on tenant's choice. + /// + public class SecurityDepositDividend : BaseModel + { + [Required] + [JsonInclude] + [StringLength(100)] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + public Guid SecurityDepositId { get; set; } + + [Required] + public Guid InvestmentPoolId { get; set; } + + [Required] + public Guid LeaseId { get; set; } + + [Required] + public Guid TenantId { get; set; } + + [Required] + public int Year { get; set; } + + /// + /// Base dividend amount (TenantShareTotal / ActiveLeaseCount from pool). + /// + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal BaseDividendAmount { get; set; } + + /// + /// Pro-ration factor for mid-year move-ins (0.0 to 1.0). + /// Example: Moved in July 1 = 0.5 (6 months of 12). + /// + [Required] + [Range(0, 1)] + [Column(TypeName = "decimal(18,6)")] + public decimal ProrationFactor { get; set; } = 1.0m; + + /// + /// Actual dividend amount after pro-ration (BaseDividendAmount * ProrationFactor). + /// This is the amount paid to the tenant. + /// + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal DividendAmount { get; set; } + + /// + /// Tenant's choice for dividend payment. + /// + [Required] + [StringLength(50)] + public string PaymentMethod { get; set; } = "Pending"; // Pending, LeaseCredit, Check + + [Required] + [StringLength(50)] + public string Status { get; set; } = "Pending"; // Pending, ChoiceMade, Applied, Paid + + /// + /// Date when tenant made their payment method choice. + /// + public DateTime? ChoiceMadeOn { get; set; } + + /// + /// Date when dividend was applied as lease credit or check was issued. + /// + public DateTime? PaymentProcessedOn { get; set; } + + [StringLength(100)] + public string? PaymentReference { get; set; } // Check number, invoice ID + + /// + /// Mailing address if tenant chose check and has moved out. + /// + [StringLength(500)] + public string? MailingAddress { get; set; } + + /// + /// Number of months deposit was in pool during the year (for pro-ration calculation). + /// + public int MonthsInPool { get; set; } = 12; + + [StringLength(500)] + public string? Notes { get; set; } + + // Navigation properties + [ForeignKey("SecurityDepositId")] + public virtual SecurityDeposit SecurityDeposit { get; set; } = null!; + + [ForeignKey("InvestmentPoolId")] + public virtual SecurityDepositInvestmentPool InvestmentPool { get; set; } = null!; + + [ForeignKey("LeaseId")] + public virtual Lease Lease { get; set; } = null!; + + [ForeignKey("TenantId")] + public virtual Tenant Tenant { get; set; } = null!; + + // Computed properties + public bool IsPending => Status == "Pending"; + public bool IsProcessed => Status == "Applied" || Status == "Paid"; + public bool TenantHasChosen => !string.IsNullOrEmpty(PaymentMethod) && PaymentMethod != "Pending"; + } +} diff --git a/Aquiis.Professional/Core/Entities/SecurityDepositInvestmentPool.cs b/Aquiis.Professional/Core/Entities/SecurityDepositInvestmentPool.cs new file mode 100644 index 0000000..8d321a5 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/SecurityDepositInvestmentPool.cs @@ -0,0 +1,108 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Annual investment pool performance tracking. + /// All security deposits are pooled and invested, with annual earnings distributed as dividends. + /// + public class SecurityDepositInvestmentPool : BaseModel + { + [Required] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + public int Year { get; set; } + + /// + /// Total security deposit amount in pool at start of year. + /// + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal StartingBalance { get; set; } + + /// + /// Total security deposit amount in pool at end of year. + /// + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal EndingBalance { get; set; } + + /// + /// Total investment earnings for the year (can be negative for losses). + /// + [Required] + [Column(TypeName = "decimal(18,2)")] + public decimal TotalEarnings { get; set; } + + /// + /// Rate of return for the year (as decimal, e.g., 0.05 = 5%). + /// Calculated as TotalEarnings / StartingBalance. + /// + [Column(TypeName = "decimal(18,6)")] + public decimal ReturnRate { get; set; } + + /// + /// Organization's share percentage (default 20%). + /// Configurable per organization via OrganizationSettings. + /// + [Required] + [Range(0, 1)] + [Column(TypeName = "decimal(18,6)")] + public decimal OrganizationSharePercentage { get; set; } = 0.20m; + + /// + /// Amount retained by organization (TotalEarnings * OrganizationSharePercentage). + /// Only applies if TotalEarnings > 0 (losses absorbed by organization). + /// + [Column(TypeName = "decimal(18,2)")] + public decimal OrganizationShare { get; set; } + + /// + /// Amount available for distribution to tenants (TotalEarnings - OrganizationShare). + /// Zero if TotalEarnings <= 0 (no negative dividends). + /// + [Column(TypeName = "decimal(18,2)")] + public decimal TenantShareTotal { get; set; } + + /// + /// Number of active leases in the pool for the year. + /// Used to calculate per-lease dividend (TenantShareTotal / ActiveLeaseCount). + /// + [Required] + public int ActiveLeaseCount { get; set; } + + /// + /// Dividend amount per active lease (TenantShareTotal / ActiveLeaseCount). + /// Pro-rated for mid-year move-ins. + /// + [Column(TypeName = "decimal(18,2)")] + public decimal DividendPerLease { get; set; } + + /// + /// Date when dividends were calculated. + /// + public DateTime? DividendsCalculatedOn { get; set; } + + /// + /// Date when dividends were distributed to tenants. + /// + public DateTime? DividendsDistributedOn { get; set; } + + [Required] + [StringLength(50)] + public string Status { get; set; } = "Open"; // Open, Calculated, Distributed, Closed + + [StringLength(1000)] + public string? Notes { get; set; } + + // Navigation properties + public virtual ICollection Dividends { get; set; } = new List(); + + // Computed properties + public bool HasEarnings => TotalEarnings > 0; + public bool HasLosses => TotalEarnings < 0; + public decimal AbsorbedLosses => TotalEarnings < 0 ? Math.Abs(TotalEarnings) : 0; + } +} diff --git a/Aquiis.Professional/Core/Entities/Tenant.cs b/Aquiis.Professional/Core/Entities/Tenant.cs new file mode 100644 index 0000000..46d8591 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Tenant.cs @@ -0,0 +1,57 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities { + + public class Tenant : BaseModel + { + + [RequiredGuid] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [Required] + [StringLength(100)] + public string FirstName { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string LastName { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string IdentificationNumber { get; set; } = string.Empty; + + [Required] + [EmailAddress] + [StringLength(255)] + public string Email { get; set; } = string.Empty; + + [Phone] + [StringLength(20)] + public string PhoneNumber { get; set; } = string.Empty; + + [DataType(DataType.Date)] + public DateTime? DateOfBirth { get; set; } + + public bool IsActive { get; set; } = true; + + [StringLength(200)] + public string EmergencyContactName { get; set; } = string.Empty; + + [Phone] + [StringLength(20)] + public string? EmergencyContactPhone { get; set; } + + [StringLength(500)] + public string Notes { get; set; } = string.Empty; + + // Link back to prospect for audit trail + public Guid? ProspectiveTenantId { get; set; } + + // Navigation properties + public virtual ICollection Leases { get; set; } = new List(); + + // Computed property + public string FullName => $"{FirstName} {LastName}"; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Entities/Tour.cs b/Aquiis.Professional/Core/Entities/Tour.cs new file mode 100644 index 0000000..5daecdb --- /dev/null +++ b/Aquiis.Professional/Core/Entities/Tour.cs @@ -0,0 +1,75 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + public class Tour : BaseModel, ISchedulableEntity + { + [RequiredGuid] + [Display(Name = "Organization ID")] + public Guid OrganizationId { get; set; } = Guid.Empty; + + [RequiredGuid] + [Display(Name = "Prospective Tenant")] + public Guid ProspectiveTenantId { get; set; } + + [RequiredGuid] + [Display(Name = "Property")] + public Guid PropertyId { get; set; } + + [Required] + [Display(Name = "Scheduled Date & Time")] + public DateTime ScheduledOn { get; set; } + + [Display(Name = "Duration (Minutes)")] + public int DurationMinutes { get; set; } + + [StringLength(50)] + [Display(Name = "Status")] + public string Status { get; set; } = string.Empty; // Scheduled, Completed, Cancelled, NoShow + + [StringLength(2000)] + [Display(Name = "Feedback")] + public string? Feedback { get; set; } + + [StringLength(50)] + [Display(Name = "Interest Level")] + public string? InterestLevel { get; set; } // VeryInterested, Interested, Neutral, NotInterested + + [StringLength(100)] + [Display(Name = "Conducted By")] + public string? ConductedBy { get; set; } = string.Empty; // UserId of property manager + + [Display(Name = "Property Tour Checklist")] + public Guid? ChecklistId { get; set; } // Links to property tour checklist + + [Display(Name = "Calendar Event")] + public Guid? CalendarEventId { get; set; } + + // Navigation properties + [ForeignKey(nameof(ProspectiveTenantId))] + public virtual ProspectiveTenant? ProspectiveTenant { get; set; } + + [ForeignKey(nameof(PropertyId))] + public virtual Property? Property { get; set; } + + [ForeignKey(nameof(ChecklistId))] + public virtual Checklist? Checklist { get; set; } + + // ISchedulableEntity implementation + public string GetEventTitle() => $"Tour: {ProspectiveTenant?.FullName ?? "Prospect"}"; + + public DateTime GetEventStart() => ScheduledOn; + + public int GetEventDuration() => DurationMinutes; + + public string GetEventType() => CalendarEventTypes.Tour; + + public Guid? GetPropertyId() => PropertyId; + + public string GetEventDescription() => Property?.Address ?? string.Empty; + + public string GetEventStatus() => Status; + } +} diff --git a/Aquiis.Professional/Core/Entities/UserOrganization.cs b/Aquiis.Professional/Core/Entities/UserOrganization.cs new file mode 100644 index 0000000..6d841e7 --- /dev/null +++ b/Aquiis.Professional/Core/Entities/UserOrganization.cs @@ -0,0 +1,67 @@ + + +using System.ComponentModel.DataAnnotations; +using Aquiis.Professional.Core.Validation; + +namespace Aquiis.Professional.Core.Entities +{ + /// + /// Junction table for multi-organization user assignments with role-based permissions + /// + public class UserOrganization + { + + [RequiredGuid] + [Display(Name = "UserOrganization ID")] + public Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// The user being granted access + /// + public string UserId { get; set; } = string.Empty; + + /// + /// The organization they're being granted access to + /// + [RequiredGuid] + public Guid OrganizationId { get; set; } = Guid.Empty; + + /// + /// Role within this organization: "Owner", "Administrator", "PropertyManager", "User" + /// + public string Role { get; set; } = string.Empty; + + /// + /// UserId of the user who granted this access + /// + public string GrantedBy { get; set; } = string.Empty; + + /// + /// When access was granted + /// + public DateTime GrantedOn { get; set; } + + /// + /// When access was revoked (NULL if still active) + /// + public DateTime? RevokedOn { get; set; } + + /// + /// Active assignment flag + /// + public bool IsActive { get; set; } = true; + + public string CreatedBy { get; set; } = string.Empty; + + public DateTime CreatedOn { get; set; } = DateTime.UtcNow; + + public string? LastModifiedBy { get; set; } = string.Empty; + + public DateTime? LastModifiedOn { get; set; } + + public bool IsDeleted { get; set; } = false; + + // Navigation properties + public virtual Organization Organization { get; set; } = null!; + } +} diff --git a/Aquiis.Professional/Core/Interfaces/IAuditable.cs b/Aquiis.Professional/Core/Interfaces/IAuditable.cs new file mode 100644 index 0000000..c3ce3ac --- /dev/null +++ b/Aquiis.Professional/Core/Interfaces/IAuditable.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.Professional.Core.Interfaces +{ + /// + /// Interface for entities that track audit information (creation and modification). + /// Entities implementing this interface will have their audit fields automatically + /// managed by the BaseService during create and update operations. + /// + public interface IAuditable + { + /// + /// Date and time when the entity was created (UTC). + /// + DateTime CreatedOn { get; set; } + + /// + /// User ID of the user who created the entity. + /// + string CreatedBy { get; set; } + + /// + /// Date and time when the entity was last modified (UTC). + /// + DateTime? LastModifiedOn { get; set; } + + /// + /// User ID of the user who last modified the entity. + /// + string? LastModifiedBy { get; set; } + } +} diff --git a/Aquiis.Professional/Core/Interfaces/ICalendarEventService.cs b/Aquiis.Professional/Core/Interfaces/ICalendarEventService.cs new file mode 100644 index 0000000..5092b34 --- /dev/null +++ b/Aquiis.Professional/Core/Interfaces/ICalendarEventService.cs @@ -0,0 +1,21 @@ +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Core.Interfaces +{ + /// + /// Service interface for managing calendar events and synchronizing with schedulable entities + /// + public interface ICalendarEventService + { + /// + /// Create or update a calendar event from a schedulable entity + /// + Task CreateOrUpdateEventAsync(T entity) + where T : BaseModel, ISchedulableEntity; + + /// + /// Delete a calendar event + /// + Task DeleteEventAsync(Guid? calendarEventId); + } +} diff --git a/Aquiis.Professional/Core/Interfaces/Services/IEmailService.cs b/Aquiis.Professional/Core/Interfaces/Services/IEmailService.cs new file mode 100644 index 0000000..bf36103 --- /dev/null +++ b/Aquiis.Professional/Core/Interfaces/Services/IEmailService.cs @@ -0,0 +1,9 @@ + +namespace Aquiis.Professional.Core.Interfaces.Services; +public interface IEmailService +{ + Task SendEmailAsync(string to, string subject, string body); + Task SendEmailAsync(string to, string subject, string body, string? fromName = null); + Task SendTemplateEmailAsync(string to, string templateId, Dictionary templateData); + Task ValidateEmailAddressAsync(string email); +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Interfaces/Services/ISMSService.cs b/Aquiis.Professional/Core/Interfaces/Services/ISMSService.cs new file mode 100644 index 0000000..4719046 --- /dev/null +++ b/Aquiis.Professional/Core/Interfaces/Services/ISMSService.cs @@ -0,0 +1,7 @@ + +namespace Aquiis.Professional.Core.Interfaces.Services; +public interface ISMSService +{ + Task SendSMSAsync(string phoneNumber, string message); + Task ValidatePhoneNumberAsync(string phoneNumber); +} \ No newline at end of file diff --git a/Aquiis.Professional/Core/Services/BaseService.cs b/Aquiis.Professional/Core/Services/BaseService.cs new file mode 100644 index 0000000..bbcdfe3 --- /dev/null +++ b/Aquiis.Professional/Core/Services/BaseService.cs @@ -0,0 +1,394 @@ +using System.Linq.Expressions; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Interfaces; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Core.Services +{ + /// + /// Abstract base service providing common CRUD operations for entities. + /// Implements organization-based multi-tenancy, soft delete support, + /// and automatic audit field management. + /// + /// Entity type that inherits from BaseModel + public abstract class BaseService where TEntity : BaseModel + { + protected readonly ApplicationDbContext _context; + protected readonly ILogger> _logger; + protected readonly UserContextService _userContext; + protected readonly ApplicationSettings _settings; + protected readonly DbSet _dbSet; + + protected BaseService( + ApplicationDbContext context, + ILogger> logger, + UserContextService userContext, + IOptions settings) + { + _context = context; + _logger = logger; + _userContext = userContext; + _settings = settings.Value; + _dbSet = context.Set(); + } + + #region CRUD Operations + + /// + /// Retrieves an entity by its ID with organization isolation. + /// Returns null if entity not found or belongs to different organization. + /// Automatically filters out soft-deleted entities. + /// + public virtual async Task GetByIdAsync(Guid id) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var entity = await _dbSet + .FirstOrDefaultAsync(e => e.Id == id && !e.IsDeleted); + + if (entity == null) + { + _logger.LogWarning($"{typeof(TEntity).Name} not found: {id}"); + return null; + } + + // Verify organization access if entity has OrganizationId property + if (HasOrganizationIdProperty(entity)) + { + var entityOrgId = GetOrganizationId(entity); + if (entityOrgId != organizationId) + { + _logger.LogWarning($"Unauthorized access to {typeof(TEntity).Name} {id} from organization {organizationId}"); + return null; + } + } + + return entity; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, $"GetById{typeof(TEntity).Name}"); + throw; + } + } + + /// + /// Retrieves all entities for the current organization. + /// Automatically filters out soft-deleted entities and applies organization isolation. + /// + public virtual async Task> GetAllAsync() + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + IQueryable query = _dbSet.Where(e => !e.IsDeleted); + + // Apply organization filter if entity has OrganizationId property + if (typeof(TEntity).GetProperty("OrganizationId") != null) + { + var parameter = Expression.Parameter(typeof(TEntity), "e"); + var property = Expression.Property(parameter, "OrganizationId"); + var constant = Expression.Constant(organizationId); + var condition = Expression.Equal(property, constant); + var lambda = Expression.Lambda>(condition, parameter); + + query = query.Where(lambda); + } + + return await query.ToListAsync(); + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, $"GetAll{typeof(TEntity).Name}"); + throw; + } + } + + /// + /// Creates a new entity with automatic audit field and organization assignment. + /// Validates entity before creation and sets CreatedBy, CreatedOn, and OrganizationId. + /// + public virtual async Task CreateAsync(TEntity entity) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Set organization ID BEFORE validation so validation rules can check it + if (HasOrganizationIdProperty(entity) && organizationId.HasValue) + { + SetOrganizationId(entity, organizationId.Value); + } + + // Call hook to set default values + entity = await SetCreateDefaultsAsync(entity); + + // Validate entity + await ValidateEntityAsync(entity); + + // Ensure ID is set + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + + // Set audit fields + SetAuditFieldsForCreate(entity, userId); + + _dbSet.Add(entity); + await _context.SaveChangesAsync(); + + _logger.LogInformation($"{typeof(TEntity).Name} created: {entity.Id} by user {userId}"); + + // Call hook for post-create operations + await AfterCreateAsync(entity); + + return entity; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, $"Create{typeof(TEntity).Name}"); + throw; + } + } + + /// + /// Updates an existing entity with automatic audit field management. + /// Validates entity and organization ownership before update. + /// Sets LastModifiedBy and LastModifiedOn automatically. + /// + public virtual async Task UpdateAsync(TEntity entity) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Validate entity + await ValidateEntityAsync(entity); + + // Verify entity exists and belongs to organization + var existing = await _dbSet + .FirstOrDefaultAsync(e => e.Id == entity.Id && !e.IsDeleted); + + if (existing == null) + { + throw new InvalidOperationException($"{typeof(TEntity).Name} not found: {entity.Id}"); + } + + // Verify organization access + if (HasOrganizationIdProperty(existing) && organizationId.HasValue) + { + var existingOrgId = GetOrganizationId(existing); + if (existingOrgId != organizationId) + { + throw new UnauthorizedAccessException( + $"Cannot update {typeof(TEntity).Name} {entity.Id} - belongs to different organization."); + } + + // Prevent organization hijacking + SetOrganizationId(entity, organizationId.Value); + } + + // Set audit fields + SetAuditFieldsForUpdate(entity, userId); + + // Update entity + _context.Entry(existing).CurrentValues.SetValues(entity); + await _context.SaveChangesAsync(); + + _logger.LogInformation($"{typeof(TEntity).Name} updated: {entity.Id} by user {userId}"); + + return entity; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, $"Update{typeof(TEntity).Name}"); + throw; + } + } + + /// + /// Deletes an entity (soft delete if enabled, hard delete otherwise). + /// Verifies organization ownership before deletion. + /// + public virtual async Task DeleteAsync(Guid id) + { + try + { + var userId = await _userContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + var entity = await _dbSet + .FirstOrDefaultAsync(e => e.Id == id && !e.IsDeleted); + + if (entity == null) + { + _logger.LogWarning($"{typeof(TEntity).Name} not found for deletion: {id}"); + return false; + } + + // Verify organization access + if (HasOrganizationIdProperty(entity) && organizationId.HasValue) + { + var entityOrgId = GetOrganizationId(entity); + if (entityOrgId != organizationId) + { + throw new UnauthorizedAccessException( + $"Cannot delete {typeof(TEntity).Name} {id} - belongs to different organization."); + } + } + + // Soft delete or hard delete based on settings + if (_settings.SoftDeleteEnabled) + { + entity.IsDeleted = true; + SetAuditFieldsForUpdate(entity, userId); + await _context.SaveChangesAsync(); + _logger.LogInformation($"{typeof(TEntity).Name} soft deleted: {id} by user {userId}"); + } + else + { + _dbSet.Remove(entity); + await _context.SaveChangesAsync(); + _logger.LogInformation($"{typeof(TEntity).Name} hard deleted: {id} by user {userId}"); + } + + return true; + } + catch (Exception ex) + { + await HandleExceptionAsync(ex, $"Delete{typeof(TEntity).Name}"); + throw; + } + } + + #endregion + + #region Helper Methods + + /// + /// Virtual method for entity-specific validation. + /// Override in derived classes to implement custom validation logic. + /// + protected virtual async Task ValidateEntityAsync(TEntity entity) + { + // Default: no validation + // Override in derived classes for specific validation + await Task.CompletedTask; + } + + /// + /// Virtual method for centralized exception handling. + /// Override in derived classes for custom error handling logic. + /// + protected virtual async Task HandleExceptionAsync(Exception ex, string operation) + { + _logger.LogError(ex, $"Error in {operation} for {typeof(TEntity).Name}"); + await Task.CompletedTask; + } + + /// + /// Sets audit fields when creating a new entity. + /// + protected virtual void SetAuditFieldsForCreate(TEntity entity, string userId) + { + entity.CreatedBy = userId; + entity.CreatedOn = DateTime.UtcNow; + } + + /// + /// Sets audit fields when updating an existing entity. + /// + protected virtual void SetAuditFieldsForUpdate(TEntity entity, string userId) + { + entity.LastModifiedBy = userId; + entity.LastModifiedOn = DateTime.UtcNow; + } + + /// + /// Checks if entity has OrganizationId property via reflection. + /// + private bool HasOrganizationIdProperty(TEntity entity) + { + return typeof(TEntity).GetProperty("OrganizationId") != null; + } + + /// + /// Gets the OrganizationId value from entity via reflection. + /// + private Guid? GetOrganizationId(TEntity entity) + { + var property = typeof(TEntity).GetProperty("OrganizationId"); + if (property == null) return null; + + var value = property.GetValue(entity); + return value is Guid guidValue ? guidValue : null; + } + + /// + /// Sets the OrganizationId value on entity via reflection. + /// + private void SetOrganizationId(TEntity entity, Guid organizationId) + { + var property = typeof(TEntity).GetProperty("OrganizationId"); + property?.SetValue(entity, organizationId); + } + + /// + /// Hook method called before creating entity to set default values. + /// Override in derived services to customize default behavior. + /// + protected virtual async Task SetCreateDefaultsAsync(TEntity entity) + { + await Task.CompletedTask; + return entity; + } + + /// + /// Hook method called after creating entity for post-creation operations. + /// Override in derived services to handle side effects like updating related entities. + /// + protected virtual async Task AfterCreateAsync(TEntity entity) + { + await Task.CompletedTask; + } + + #endregion + } +} diff --git a/Aquiis.Professional/Core/Validation/OptionalGuidAttribute.cs b/Aquiis.Professional/Core/Validation/OptionalGuidAttribute.cs new file mode 100644 index 0000000..0327c42 --- /dev/null +++ b/Aquiis.Professional/Core/Validation/OptionalGuidAttribute.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.Professional.Core.Validation; + +/// +/// Validates that an optional Guid property, if provided, is not Guid.Empty. +/// Use this for Guid? properties where null is acceptable but Guid.Empty is not. +/// +/// Example: LeaseId on MaintenanceRequest - can be null (no lease yet) but shouldn't be Guid.Empty (invalid reference) +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +public class OptionalGuidAttribute : ValidationAttribute +{ + /// + /// Initializes a new instance of OptionalGuidAttribute with a default error message. + /// + public OptionalGuidAttribute() + : base("The {0} field cannot be empty if provided. Either leave it null or provide a valid value.") + { + } + + /// + /// Initializes a new instance of OptionalGuidAttribute with a custom error message. + /// + /// The error message to display when validation fails. + public OptionalGuidAttribute(string errorMessage) + : base(errorMessage) + { + } + + /// + /// Validates that if the value is not null, it must not be Guid.Empty. + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + // Null is acceptable for optional fields + if (value == null) + { + return ValidationResult.Success; + } + + // Type check + if (value is not Guid guidValue) + { + return new ValidationResult( + $"The {validationContext.DisplayName} field must be a valid Guid or null.", + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + // Reject Guid.Empty (if you provide a value, it must be real) + if (guidValue == Guid.Empty) + { + return new ValidationResult( + FormatErrorMessage(validationContext.DisplayName), + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + return ValidationResult.Success; + } + + public override bool IsValid(object? value) + { + if (value == null) + return true; + + if (value is not Guid guidValue) + return false; + + return guidValue != Guid.Empty; + } +} diff --git a/Aquiis.Professional/Core/Validation/RequiredGuidAttribute.cs b/Aquiis.Professional/Core/Validation/RequiredGuidAttribute.cs new file mode 100644 index 0000000..27d99f8 --- /dev/null +++ b/Aquiis.Professional/Core/Validation/RequiredGuidAttribute.cs @@ -0,0 +1,83 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.Professional.Core.Validation; + +/// +/// Validates that a Guid property has a value other than Guid.Empty. +/// Use this instead of [Required] for non-nullable Guid properties. +/// +/// Note: For nullable Guid? properties, use [Required] to check for null, +/// and optionally combine with [RequiredGuid] to also reject Guid.Empty. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +public class RequiredGuidAttribute : ValidationAttribute +{ + /// + /// Initializes a new instance of RequiredGuidAttribute with a default error message. + /// + public RequiredGuidAttribute() + : base("The {0} field is required and cannot be empty.") + { + } + + /// + /// Initializes a new instance of RequiredGuidAttribute with a custom error message. + /// + /// The error message to display when validation fails. + public RequiredGuidAttribute(string errorMessage) + : base(errorMessage) + { + } + + /// + /// Validates that the value is not null, not Guid.Empty, and is a valid Guid. + /// + /// The value to validate. + /// The context information about the validation operation. + /// ValidationResult.Success if valid, otherwise a ValidationResult with error message. + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + // Null check (for Guid? properties) + if (value == null) + { + return new ValidationResult( + FormatErrorMessage(validationContext.DisplayName), + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + // Type check + if (value is not Guid guidValue) + { + return new ValidationResult( + $"The {validationContext.DisplayName} field must be a valid Guid.", + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + // Empty Guid check + if (guidValue == Guid.Empty) + { + return new ValidationResult( + FormatErrorMessage(validationContext.DisplayName), + new[] { validationContext.MemberName ?? string.Empty } + ); + } + + return ValidationResult.Success; + } + + /// + /// Simple validation for attribute usage without ValidationContext. + /// + public override bool IsValid(object? value) + { + if (value == null) + return false; + + if (value is not Guid guidValue) + return false; + + return guidValue != Guid.Empty; + } +} diff --git a/Aquiis.Professional/Data/ApplicationDbContext.cs b/Aquiis.Professional/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..9f271ec --- /dev/null +++ b/Aquiis.Professional/Data/ApplicationDbContext.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Professional.Data; + +public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext(options) +{ +} diff --git a/Aquiis.Professional/Data/ApplicationUser.cs b/Aquiis.Professional/Data/ApplicationUser.cs new file mode 100644 index 0000000..b4f3e05 --- /dev/null +++ b/Aquiis.Professional/Data/ApplicationUser.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity; + +namespace Aquiis.Professional.Data; + +// Add profile data for application users by adding properties to the ApplicationUser class +public class ApplicationUser : IdentityUser +{ +} + diff --git a/Aquiis.Professional/Features/Administration/Application/Pages/DailyReport.razor b/Aquiis.Professional/Features/Administration/Application/Pages/DailyReport.razor new file mode 100644 index 0000000..9d741c0 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Application/Pages/DailyReport.razor @@ -0,0 +1,148 @@ +@page "/administration/application/dailyreport" + +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization + +@inject ApplicationService ApplicationService +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Daily Payment Report + +
+

Daily Payment Report

+ +
+ +@if (isLoading) +{ +
+
+ Loading... +
+
+} +else +{ +
+
+
+
+
Today's Total
+

$@todayTotal.ToString("N2")

+ @DateTime.Today.ToString("MMM dd, yyyy") +
+
+
+
+
+
+
This Week
+

$@weekTotal.ToString("N2")

+ Last 7 days +
+
+
+
+
+
+
This Month
+

$@monthTotal.ToString("N2")

+ @DateTime.Today.ToString("MMM yyyy") +
+
+
+
+
+
+
Expiring Leases
+

@expiringLeases

+ Next 30 days +
+
+
+
+ + @if (statistics != null) + { +
+
+
Payment Statistics
+
+
+
+
+

Period: @statistics.StartDate.ToString("MMM dd, yyyy") - @statistics.EndDate.ToString("MMM dd, yyyy")

+

Total Payments: @statistics.PaymentCount

+

Average Payment: $@statistics.AveragePayment.ToString("N2")

+
+
+
Payment Methods
+ @if (statistics.PaymentsByMethod.Any()) + { +
    + @foreach (var method in statistics.PaymentsByMethod) + { +
  • + @method.Key: $@method.Value.ToString("N2") +
  • + } +
+ } + else + { +

No payment methods recorded

+ } +
+
+
+
+ } +} + +@code { + private bool isLoading = true; + private decimal todayTotal = 0; + private decimal weekTotal = 0; + private decimal monthTotal = 0; + private int expiringLeases = 0; + private PaymentStatistics? statistics; + + protected override async Task OnInitializedAsync() + { + await LoadReport(); + } + + private async Task LoadReport() + { + isLoading = true; + try + { + var today = DateTime.Today; + var weekStart = today.AddDays(-7); + var monthStart = new DateTime(today.Year, today.Month, 1); + + // Get payment totals + todayTotal = await ApplicationService.GetTodayPaymentTotalAsync(); + weekTotal = await ApplicationService.GetPaymentTotalForRangeAsync(weekStart, today); + monthTotal = await ApplicationService.GetPaymentTotalForRangeAsync(monthStart, today); + + // Get expiring leases count + expiringLeases = await ApplicationService.GetLeasesExpiringCountAsync(30); + + // Get detailed statistics for this month + statistics = await ApplicationService.GetPaymentStatisticsAsync(monthStart, today); + } + finally + { + isLoading = false; + } + } + + private async Task RefreshReport() + { + await LoadReport(); + } +} diff --git a/Aquiis.Professional/Features/Administration/Application/Pages/InitializeSchemaVersion.razor b/Aquiis.Professional/Features/Administration/Application/Pages/InitializeSchemaVersion.razor new file mode 100644 index 0000000..aa93eef --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Application/Pages/InitializeSchemaVersion.razor @@ -0,0 +1,64 @@ +@page "/administration/application/initialize-schema" +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.Extensions.Options +@inject SchemaValidationService SchemaService +@inject IOptions AppSettings +@inject NavigationManager Navigation +@rendermode InteractiveServer + +

Initialize Schema Version

+ +
+
+
+

Initialize Schema Version

+

This page will manually insert the initial schema version record into the database.

+ + @if (!string.IsNullOrEmpty(message)) + { +
+ @message +
+ } + +
+
+

Application Schema Version: @AppSettings.Value.SchemaVersion

+ +
+
+
+
+
+ +@code { + private string message = ""; + private bool isSuccess = false; + + private async Task InitializeSchema() + { + try + { + await SchemaService.UpdateSchemaVersionAsync( + AppSettings.Value.SchemaVersion, + "Manual initialization via admin page"); + + message = $"Schema version {AppSettings.Value.SchemaVersion} has been initialized successfully!"; + isSuccess = true; + + // Reload page after 2 seconds + await Task.Delay(2000); + Navigation.NavigateTo("/", true); + } + catch (Exception ex) + { + message = $"Error: {ex.Message}"; + isSuccess = false; + } + } +} diff --git a/Aquiis.Professional/Features/Administration/Application/Pages/ManageDatabase.razor b/Aquiis.Professional/Features/Administration/Application/Pages/ManageDatabase.razor new file mode 100644 index 0000000..1aa0db5 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Application/Pages/ManageDatabase.razor @@ -0,0 +1,777 @@ +@page "/administration/application/database" +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using Microsoft.Extensions.Options +@using ElectronNET.API +@inject DatabaseBackupService BackupService +@inject ElectronPathService ElectronPathService +@inject NavigationManager Navigation +@inject SchemaValidationService SchemaService +@inject IOptions AppSettings +@inject IJSRuntime JSRuntime +@inject IHostApplicationLifetime AppLifetime +@attribute [OrganizationAuthorize("Owner", "Administrator")] +@rendermode InteractiveServer + +Database Backup & Recovery + +
+
+
+

+ Database Backup & Recovery

+

Manage database backups and recover from corruption

+
+
+ +
+
+ + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + +
+
+
+
+
Database Health
+
+
+ @if (isCheckingHealth) + { +
+
+ Checking health... +
+

Checking database health...

+
+ } + else if (healthCheckResult != null) + { +
+ @if (healthCheckResult.Value.IsHealthy) + { + +
+
Healthy
+

@healthCheckResult.Value.Message

+
+ } + else + { + +
+
Unhealthy
+

@healthCheckResult.Value.Message

+
+ } +
+ Last checked: @lastHealthCheck?.ToString("g") + } + else + { +

Click "Check Health" to validate database integrity

+ } + +
+ +
+
+
+
+ +
+
+
+
Backup Actions
+
+
+

Create manual backups or recover from corruption

+ +
+ + + +
+
+
+
+
+ + +
+
+
+
+
Available Backups
+
+
+ @if (isLoadingBackups) + { +
+
+ Loading backups... +
+
+ } + else if (backups.Any()) + { +
+ + + + + + + + + + + @foreach (var backup in backups) + { + + + + + + + } + +
File NameCreated DateSizeActions
+ + @backup.FileName + @backup.CreatedDate.ToString("g")@backup.SizeFormatted + + + +
+
+ } + else + { +
+ +

No backup files found

+
+ } + +
+ +
+
+
+
+
+
+
+

Initialize Schema Version

+

This page will manually insert the initial schema version record into the database.

+ + @if (!string.IsNullOrEmpty(message)) + { +
+ @message +
+ } + +
+
+

Application Schema Version: @AppSettings.Value.SchemaVersion

+ +
+
+
+
+ +
+
+
+
Important Information
+
    +
  • Automatic Backups: Created before each migration
  • +
  • Health Check: Validates database integrity using SQLite's built-in PRAGMA integrity_check
  • +
  • Auto-Recovery: Attempts to restore from the most recent valid backup
  • +
  • Retention: Last 10 backups are kept automatically, older ones are deleted
  • +
  • Restore: Creates a copy of the current database before restoring (saved as .corrupted)
  • +
+
+
+
+
+ +@code { + private List backups = new(); + private string? successMessage; + private string? errorMessage; + private bool isLoadingBackups = false; + private bool isCreatingBackup = false; + private bool isRestoring = false; + private bool isRecovering = false; + private bool isCheckingHealth = false; + private bool isDownloading = false; + private bool isUploading = false; + private bool isResetting = false; + private bool isDeleting = false; + private bool isRestarting = false; + private (bool IsHealthy, string Message)? healthCheckResult; + private DateTime? lastHealthCheck; + + private string message = ""; + private bool isSuccess = false; + + private async Task InitializeSchema() + { + try + { + await SchemaService.UpdateSchemaVersionAsync( + AppSettings.Value.SchemaVersion, + "Manual initialization via admin page"); + + message = $"Schema version {AppSettings.Value.SchemaVersion} has been initialized successfully!"; + isSuccess = true; + + // Reload page after 2 seconds + await Task.Delay(2000); + Navigation.NavigateTo("/", true); + } + catch (Exception ex) + { + message = $"Error: {ex.Message}"; + isSuccess = false; + } + } + + private async Task ResetDatabase() + { + // Show confirmation dialog + bool confirmed = await JSRuntime.InvokeAsync("confirm", + "WARNING: This will delete the current database and create a new blank one. All data will be lost!\n\n" + + "A backup will be created before deletion.\n\n" + + "Are you absolutely sure you want to continue?"); + + if (!confirmed) + return; + + try + { + isResetting = true; + errorMessage = null; + successMessage = null; + StateHasChanged(); + + // Create backup of current database before deletion + var backupPath = await BackupService.CreateBackupAsync("BeforeReset"); + + if (string.IsNullOrEmpty(backupPath)) + { + errorMessage = "Failed to create backup before reset. Reset cancelled."; + return; + } + + // Verify backup was created successfully + if (!File.Exists(backupPath)) + { + errorMessage = $"Backup file not found at {backupPath}. Reset cancelled."; + return; + } + + var backupSize = new FileInfo(backupPath).Length; + if (backupSize == 0) + { + errorMessage = "Backup file is empty. Reset cancelled."; + File.Delete(backupPath); // Clean up empty backup + return; + } + + successMessage = $"Backup created successfully ({FormatFileSize(backupSize)}). Deleting database..."; + StateHasChanged(); + await Task.Delay(1000); + + // Get database path + var dbPath = await ElectronPathService.GetDatabasePathAsync(); + + if (File.Exists(dbPath)) + { + File.Delete(dbPath); + successMessage = "Database deleted successfully. Application will restart to create a new blank database."; + StateHasChanged(); + + // Wait a moment for user to see message + await Task.Delay(2000); + + // Restart the application using Electron API + if (HybridSupport.IsElectronActive) + { + Electron.App.Relaunch(); + Electron.App.Exit(); + } + else + { + Navigation.NavigateTo("/", true); + } + } + else + { + errorMessage = "Database file not found."; + } + } + catch (Exception ex) + { + errorMessage = $"Error resetting database: {ex.Message}"; + } + finally + { + isResetting = false; + } + } + + private async Task RestartApplication() + { + var confirmed = await JSRuntime.InvokeAsync("confirm", + "Are you sure you want to restart the application?\n\n" + + "All users will be disconnected and the application will reload."); + + if (!confirmed) return; + + isRestarting = true; + successMessage = "Restarting application..."; + StateHasChanged(); + + try + { + await Task.Delay(1000); // Give time for the message to display + + // Stop the application - the host will automatically restart it + AppLifetime.StopApplication(); + } + catch (Exception ex) + { + errorMessage = $"Error restarting application: {ex.Message}"; + isRestarting = false; + } + } + + protected override async Task OnInitializedAsync() + { + await LoadBackups(); + await CheckDatabaseHealth(); + } + + private async Task LoadBackups() + { + isLoadingBackups = true; + errorMessage = null; + + try + { + backups = await BackupService.GetAvailableBackupsAsync(); + } + catch (Exception ex) + { + errorMessage = $"Failed to load backups: {ex.Message}"; + } + finally + { + isLoadingBackups = false; + } + } + + private void BackToDashboard() + { + Navigation.NavigateTo("/administration/dashboard"); + } + + private async Task CreateManualBackup() + { + try + { + await JSRuntime.InvokeVoidAsync("console.log", "CreateManualBackup called"); + + isCreatingBackup = true; + errorMessage = null; + successMessage = null; + StateHasChanged(); // Force UI update to show spinner + + await JSRuntime.InvokeVoidAsync("console.log", "About to call BackupService.CreateBackupAsync"); + + await Task.Delay(100); // Small delay to ensure UI updates + var backupPath = await BackupService.CreateBackupAsync("Manual"); + + await JSRuntime.InvokeVoidAsync("console.log", $"Backup result: {backupPath ?? "null"}"); + + if (backupPath != null) + { + successMessage = $"Backup created successfully: {Path.GetFileName(backupPath)}"; + await LoadBackups(); + StateHasChanged(); // Force UI update to show success message + } + else + { + errorMessage = "Failed to create backup - no path returned"; + } + } + catch (Exception ex) + { + errorMessage = $"Error creating backup: {ex.Message}"; + await JSRuntime.InvokeVoidAsync("console.error", $"Backup error: {ex}"); + Console.WriteLine($"Backup error: {ex}"); // Log full exception to console + } + finally + { + isCreatingBackup = false; + StateHasChanged(); // Force UI update + } + } + + private async Task RestoreBackup(BackupInfo backup) + { + var confirmed = await JSRuntime.InvokeAsync("confirm", + $"Are you sure you want to restore from '{backup.FileName}'?\n\n" + + $"This will replace your current database and the application will restart automatically.\n\n" + + $"Current database will be saved as .beforeRestore backup."); + + if (!confirmed) return; + + isRestoring = true; + errorMessage = null; + successMessage = null; + + try + { + // Get database path (works for both Electron and web mode) + var dbPath = await BackupService.GetDatabasePathAsync(); + var stagedRestorePath = $"{dbPath}.restore_pending"; + + // Verify backup exists + if (!File.Exists(backup.FilePath)) + { + errorMessage = $"Backup file not found: {backup.FileName}"; + return; + } + + // Copy backup to staged restore location + // On next startup, Program.cs will move this into place BEFORE opening any connections + File.Copy(backup.FilePath, stagedRestorePath, overwrite: true); + + successMessage = $"Restore staged successfully! Restarting application..."; + StateHasChanged(); + + // Wait for user to see message + await Task.Delay(1500); + + // Restart the application - on startup it will apply the staged restore + if (HybridSupport.IsElectronActive) + { + Electron.App.Relaunch(); + Electron.App.Exit(); + } + else + { + // Web mode - stop the application, which will trigger a restart by the host + AppLifetime.StopApplication(); + } + } + catch (Exception ex) + { + errorMessage = $"Error staging restore: {ex.Message}"; + } + finally + { + isRestoring = false; + } + } + + private async Task DeleteBackup(BackupInfo backup) + { + var confirmed = await JSRuntime.InvokeAsync("confirm", + $"Are you sure you want to delete '{backup.FileName}'?\n\nThis cannot be undone."); + + if (!confirmed) return; + + isDeleting = true; + errorMessage = null; + successMessage = null; + + try + { + // Delete the backup file + if (File.Exists(backup.FilePath)) + { + File.Delete(backup.FilePath); + successMessage = $"Backup '{backup.FileName}' deleted successfully."; + + // Refresh the backup list + await LoadBackups(); + } + else + { + errorMessage = $"Backup file not found: {backup.FileName}"; + } + } + catch (Exception ex) + { + errorMessage = $"Error deleting backup: {ex.Message}"; + } + finally + { + isDeleting = false; + } + } + + private async Task AttemptAutoRecovery() + { + var confirmed = await JSRuntime.InvokeAsync("confirm", + "This will attempt to restore from the most recent valid backup. Continue?"); + + if (!confirmed) return; + + isRecovering = true; + errorMessage = null; + successMessage = null; + + try + { + var (success, message) = await BackupService.AutoRecoverFromCorruptionAsync(); + if (success) + { + successMessage = message; + await CheckDatabaseHealth(); + } + else + { + errorMessage = message; + } + } + catch (Exception ex) + { + errorMessage = $"Recovery error: {ex.Message}"; + } + finally + { + isRecovering = false; + } + } + + private async Task CheckDatabaseHealth() + { + isCheckingHealth = true; + errorMessage = null; + + try + { + healthCheckResult = await BackupService.ValidateDatabaseHealthAsync(); + lastHealthCheck = DateTime.Now; + } + catch (Exception ex) + { + errorMessage = $"Health check error: {ex.Message}"; + } + finally + { + isCheckingHealth = false; + } + } + + private async Task DownloadBackup(BackupInfo backup) + { + isDownloading = true; + errorMessage = null; + + try + { + // Read the backup file + var fileBytes = await File.ReadAllBytesAsync(backup.FilePath); + var base64 = Convert.ToBase64String(fileBytes); + + // Trigger download in browser + await JSRuntime.InvokeVoidAsync("downloadFile", backup.FileName, base64, "application/x-sqlite3"); + + successMessage = $"Backup '{backup.FileName}' downloaded successfully"; + } + catch (Exception ex) + { + errorMessage = $"Error downloading backup: {ex.Message}"; + } + finally + { + isDownloading = false; + } + } + + private async Task TriggerFileUpload() + { + await JSRuntime.InvokeVoidAsync("document.getElementById('backupFileInput').click"); + } + + private async Task HandleFileUpload(InputFileChangeEventArgs e) + { + isUploading = true; + errorMessage = null; + successMessage = null; + StateHasChanged(); + + try + { + var file = e.File; + + // Validate file extension + if (!file.Name.EndsWith(".db", StringComparison.OrdinalIgnoreCase)) + { + errorMessage = "Invalid file type. Please upload a .db file."; + return; + } + + // Limit file size to 500MB + if (file.Size > 500 * 1024 * 1024) + { + errorMessage = "File too large. Maximum size is 500MB."; + return; + } + + // Read the file + using var stream = file.OpenReadStream(maxAllowedSize: 500 * 1024 * 1024); + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); + var fileBytes = memoryStream.ToArray(); + + // Get database path and backup directory + var dbPath = HybridSupport.IsElectronActive + ? await ElectronPathService.GetDatabasePathAsync() + : Path.Combine(Directory.GetCurrentDirectory(), "Data/app.db"); + var backupDir = Path.Combine(Path.GetDirectoryName(dbPath)!, "Backups"); + Directory.CreateDirectory(backupDir); + + // Create filename with timestamp + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + var uploadedFileName = Path.GetFileNameWithoutExtension(file.Name); + var backupFileName = $"Aquiis_Backup_Uploaded_{uploadedFileName}_{timestamp}.db"; + var backupPath = Path.Combine(backupDir, backupFileName); + + // Save the uploaded file + await File.WriteAllBytesAsync(backupPath, fileBytes); + + successMessage = $"Backup '{file.Name}' uploaded successfully as '{backupFileName}'"; + await LoadBackups(); + } + catch (Exception ex) + { + errorMessage = $"Error uploading backup: {ex.Message}"; + await JSRuntime.InvokeVoidAsync("console.error", $"Upload error: {ex}"); + } + finally + { + isUploading = false; + StateHasChanged(); + } + } + + private string FormatFileSize(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB" }; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } +} diff --git a/Aquiis.Professional/Features/Administration/Dashboard.razor b/Aquiis.Professional/Features/Administration/Dashboard.razor new file mode 100644 index 0000000..a3eb4dc --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Dashboard.razor @@ -0,0 +1,194 @@ +@page "/administration/dashboard" + +@using Aquiis.Professional.Shared.Components.Shared + +@inject NavigationManager NavigationManager + +@rendermode InteractiveServer + +Application Management Dashboard + + + + +
+

Application Management

+
+ Admin Only +
+
+ @if (isLoading) + { +
+
+ Loading... +
+
+ } + else + { +
+
+
+
+
+
+

@totalAccounts

+

Total Accounts

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@activeAccounts

+

Active Accounts

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@usersOnline

+

Users Online

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@lockedAccounts

+

Locked Accounts

+
+
+ +
+
+
+
+
+
+ } + +
+ + + + + + + + +
+
+ + @{ + NavigationManager.NavigateTo("/Account/Login", forceLoad: true); + } + +
+ +@code { + private bool isLoading = false; + //private int totalProperties = 0; + //private int availableProperties = 0; + //private int totalTenants = 0; + //private int activeLeases = 0; + + private int lockedAccounts = 0; + + private int totalAccounts = 0; + private int activeAccounts = 0; + + private int usersOnline = 0; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateTask; + if (authState.User.Identity?.IsAuthenticated == true) + { + await LoadDashboardData(); + } + isLoading = false; + } + + private async Task LoadDashboardData() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + return; + + } + + private void GoToEmailSettings() + { + // Logic to navigate to the email notification settings page + NavigationManager.NavigateTo("/administration/settings/email"); + } + + private void GoToSMSSettings() + { + // Logic to navigate to the SMS notification settings page + NavigationManager.NavigateTo("/administration/settings/sms"); + } + + private void GoToLateFeeSettings() + { + // Logic to view the daily system status report + NavigationManager.NavigateTo("/administration/settings/latefees"); + } + + private void GoToServiceSettings() + { + // Logic to navigate to the application settings page + NavigationManager.NavigateTo("/administration/settings/services"); + } + private void GoToDatabaseSettings() + { + // Logic to navigate to the database management page + NavigationManager.NavigateTo("/administration/application/database"); + } + + private void GoToCalendarSettings() + { + // Logic to navigate to the calendar settings page + NavigationManager.NavigateTo("/administration/settings/calendar"); + } + + private void GoToUserManagement() + { + // Logic to navigate to the user management page + NavigationManager.NavigateTo("/administration/users/manage"); + } + private void GoToOrganizationSettings() + { + // Logic to navigate to the organization settings page + NavigationManager.NavigateTo("/administration/settings/organization"); + } +} diff --git a/Aquiis.Professional/Features/Administration/Organizations/Pages/CreateOrganization.razor b/Aquiis.Professional/Features/Administration/Organizations/Pages/CreateOrganization.razor new file mode 100644 index 0000000..75c5bb7 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Organizations/Pages/CreateOrganization.razor @@ -0,0 +1,197 @@ +@page "/administration/organizations/create" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations + +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService + +@attribute [OrganizationAuthorize("Owner")] +@rendermode InteractiveServer + +Create Organization - Administration + +
+
+

Create Organization

+ +
+ +
+
+
+
+
New Organization
+
+
+ + + + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + +
+ + + + Official legal name of the organization +
+ +
+ + + + Short name for UI display (optional) +
+ +
+ + + + @foreach (var state in GetUsStates()) + { + + } + + + Primary state where organization operates +
+ +
+ + +
+
+
+
+
+ +
+
+
+
Information
+
+
+

What happens when you create an organization?

+
    +
  • A new organization record is created
  • +
  • You are automatically assigned as the Owner
  • +
  • The organization will have its own independent settings
  • +
  • You can grant access to other users
  • +
  • All data will be isolated to this organization
  • +
+
+

Note: You can switch between your organizations using the organization switcher in the navigation menu.

+
+
+
+
+
+ +@code { + private CreateOrganizationModel model = new(); + private bool isSubmitting = false; + private string errorMessage = string.Empty; + + private Organization? createdOrganization; + + private async Task HandleSubmit() + { + isSubmitting = true; + errorMessage = string.Empty; + + try + { + var userId = await UserContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + errorMessage = "User not found. Please log in again."; + return; + } + createdOrganization = new Organization + { + Name = model.Name!, + DisplayName = model.DisplayName, + State = model.State + }; + var organization = await OrganizationService.CreateOrganizationAsync(createdOrganization); + + if (organization != null) + { + ToastService.ShowSuccess($"Organization '{organization.Name}' created successfully!"); + Navigation.NavigateTo("/administration/organizations"); + } + else + { + errorMessage = "Failed to create organization. Please try again."; + } + } + catch (Exception ex) + { + errorMessage = $"Error creating organization: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private void Cancel() + { + Navigation.NavigateTo("/administration/organizations"); + } + + private List<(string Code, string Name)> GetUsStates() + { + return new List<(string, string)> + { + ("AL", "Alabama"), ("AK", "Alaska"), ("AZ", "Arizona"), ("AR", "Arkansas"), + ("CA", "California"), ("CO", "Colorado"), ("CT", "Connecticut"), ("DE", "Delaware"), + ("FL", "Florida"), ("GA", "Georgia"), ("HI", "Hawaii"), ("ID", "Idaho"), + ("IL", "Illinois"), ("IN", "Indiana"), ("IA", "Iowa"), ("KS", "Kansas"), + ("KY", "Kentucky"), ("LA", "Louisiana"), ("ME", "Maine"), ("MD", "Maryland"), + ("MA", "Massachusetts"), ("MI", "Michigan"), ("MN", "Minnesota"), ("MS", "Mississippi"), + ("MO", "Missouri"), ("MT", "Montana"), ("NE", "Nebraska"), ("NV", "Nevada"), + ("NH", "New Hampshire"), ("NJ", "New Jersey"), ("NM", "New Mexico"), ("NY", "New York"), + ("NC", "North Carolina"), ("ND", "North Dakota"), ("OH", "Ohio"), ("OK", "Oklahoma"), + ("OR", "Oregon"), ("PA", "Pennsylvania"), ("RI", "Rhode Island"), ("SC", "South Carolina"), + ("SD", "South Dakota"), ("TN", "Tennessee"), ("TX", "Texas"), ("UT", "Utah"), + ("VT", "Vermont"), ("VA", "Virginia"), ("WA", "Washington"), ("WV", "West Virginia"), + ("WI", "Wisconsin"), ("WY", "Wyoming") + }; + } + + public class CreateOrganizationModel + { + [Required(ErrorMessage = "Organization name is required")] + [StringLength(200, ErrorMessage = "Organization name cannot exceed 200 characters")] + public string? Name { get; set; } + + [StringLength(200, ErrorMessage = "Display name cannot exceed 200 characters")] + public string? DisplayName { get; set; } + + [StringLength(2, ErrorMessage = "State code must be 2 characters")] + public string? State { get; set; } + } +} diff --git a/Aquiis.Professional/Features/Administration/Organizations/Pages/EditOrganization.razor b/Aquiis.Professional/Features/Administration/Organizations/Pages/EditOrganization.razor new file mode 100644 index 0000000..2114660 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Organizations/Pages/EditOrganization.razor @@ -0,0 +1,296 @@ +@page "/administration/organizations/edit/{Id:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations + +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService + +@attribute [OrganizationAuthorize("Owner")] +@rendermode InteractiveServer + +Edit Organization - Administration + +
+
+

Edit Organization

+ +
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (organization == null) + { +
+ Organization not found or you don't have permission to edit it. +
+ } + else + { +
+
+
+
+
Organization Details
+
+
+ + + + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + +
+ + + + Official legal name of the organization +
+ +
+ + + + Short name for UI display (optional) +
+ +
+ + + + @foreach (var state in GetUsStates()) + { + + } + + + Primary state where organization operates +
+ +
+ + + Inactive organizations cannot be accessed by users +
+ +
+ + +
+
+
+
+
+ +
+
+
+
Organization Info
+
+
+
+
Created On:
+
@organization.CreatedOn.ToShortDateString()
+ +
Created By:
+
@organization.CreatedBy
+ + @if (organization.LastModifiedOn.HasValue) + { +
Last Modified:
+
@organization.LastModifiedOn.Value.ToShortDateString()
+ +
Modified By:
+
@(organization.LastModifiedBy ?? "-")
+ } +
+
+
+
+
+ } +
+ +@code { + [Parameter] + public Guid Id { get; set; } = Guid.Empty; + + private bool isLoading = true; + private bool isSubmitting = false; + private Organization? organization; + private EditOrganizationModel model = new(); + private string errorMessage = string.Empty; + private string successMessage = string.Empty; + + protected override async Task OnInitializedAsync() + { + await LoadOrganization(); + } + + private async Task LoadOrganization() + { + isLoading = true; + try + { + var userId = await UserContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + errorMessage = "User not found. Please log in again."; + return; + } + + // Check if user is owner of this organization + var isOwner = await OrganizationService.IsOwnerAsync(userId, Id); + if (!isOwner) + { + organization = null; + return; + } + + organization = await OrganizationService.GetOrganizationByIdAsync(Id); + if (organization != null) + { + model = new EditOrganizationModel + { + Name = organization.Name, + DisplayName = organization.DisplayName, + State = organization.State, + IsActive = organization.IsActive + }; + } + } + catch (Exception ex) + { + errorMessage = $"Error loading organization: {ex.Message}"; + } + finally + { + isLoading = false; + } + } + + private async Task HandleSubmit() + { + isSubmitting = true; + errorMessage = string.Empty; + successMessage = string.Empty; + + try + { + if (organization == null) + { + errorMessage = "Organization not found."; + return; + } + + var userId = await UserContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + errorMessage = "User not found. Please log in again."; + return; + } + + // Update organization properties + organization.Name = model.Name!; + organization.DisplayName = model.DisplayName; + organization.State = model.State; + organization.IsActive = model.IsActive; + + var success = await OrganizationService.UpdateOrganizationAsync(organization); + + if (success) + { + successMessage = "Organization updated successfully!"; + ToastService.ShowSuccess(successMessage); + } + else + { + errorMessage = "Failed to update organization. Please try again."; + } + } + catch (Exception ex) + { + errorMessage = $"Error updating organization: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private void Cancel() + { + Navigation.NavigateTo("/administration/organizations"); + } + + private List<(string Code, string Name)> GetUsStates() + { + return new List<(string, string)> + { + ("AL", "Alabama"), ("AK", "Alaska"), ("AZ", "Arizona"), ("AR", "Arkansas"), + ("CA", "California"), ("CO", "Colorado"), ("CT", "Connecticut"), ("DE", "Delaware"), + ("FL", "Florida"), ("GA", "Georgia"), ("HI", "Hawaii"), ("ID", "Idaho"), + ("IL", "Illinois"), ("IN", "Indiana"), ("IA", "Iowa"), ("KS", "Kansas"), + ("KY", "Kentucky"), ("LA", "Louisiana"), ("ME", "Maine"), ("MD", "Maryland"), + ("MA", "Massachusetts"), ("MI", "Michigan"), ("MN", "Minnesota"), ("MS", "Mississippi"), + ("MO", "Missouri"), ("MT", "Montana"), ("NE", "Nebraska"), ("NV", "Nevada"), + ("NH", "New Hampshire"), ("NJ", "New Jersey"), ("NM", "New Mexico"), ("NY", "New York"), + ("NC", "North Carolina"), ("ND", "North Dakota"), ("OH", "Ohio"), ("OK", "Oklahoma"), + ("OR", "Oregon"), ("PA", "Pennsylvania"), ("RI", "Rhode Island"), ("SC", "South Carolina"), + ("SD", "South Dakota"), ("TN", "Tennessee"), ("TX", "Texas"), ("UT", "Utah"), + ("VT", "Vermont"), ("VA", "Virginia"), ("WA", "Washington"), ("WV", "West Virginia"), + ("WI", "Wisconsin"), ("WY", "Wyoming") + }; + } + + public class EditOrganizationModel + { + [Required(ErrorMessage = "Organization name is required")] + [StringLength(200, ErrorMessage = "Organization name cannot exceed 200 characters")] + public string? Name { get; set; } + + [StringLength(200, ErrorMessage = "Display name cannot exceed 200 characters")] + public string? DisplayName { get; set; } + + [StringLength(2, ErrorMessage = "State code must be 2 characters")] + public string? State { get; set; } + + public bool IsActive { get; set; } = true; + } +} diff --git a/Aquiis.Professional/Features/Administration/Organizations/Pages/ManageUsers.razor b/Aquiis.Professional/Features/Administration/Organizations/Pages/ManageUsers.razor new file mode 100644 index 0000000..d8dfb33 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Organizations/Pages/ManageUsers.razor @@ -0,0 +1,454 @@ +@page "/administration/organizations/{Id:guid}/users" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Identity +@using Microsoft.EntityFrameworkCore + +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject UserManager UserManager +@inject NavigationManager Navigation +@inject ToastService ToastService +@inject IJSRuntime JSRuntime + +@attribute [OrganizationAuthorize("Owner", "Administrator")] +@rendermode InteractiveServer + +Manage Organization Users - Administration + +
+
+
+

Manage Users

+ @if (organization != null) + { +

@organization.Name

+ } +
+ +
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (organization == null) + { +
+ Organization not found or you don't have permission to manage users. +
+ } + else + { +
+
+
+
+
Users with Access
+ +
+
+ @if (!organizationUsers.Any()) + { +
+ No users assigned to this organization yet. +
+ } + else + { +
+ + + + + + + + + + + + + @foreach (var userOrg in organizationUsers) + { + + + + + + + + + } + +
UserRoleGranted ByGranted OnStatusActions
+ @GetUserEmail(userOrg.UserId) + + @if (userOrg.Role == ApplicationConstants.OrganizationRoles.Owner && userOrg.UserId == organization.OwnerId) + { + + @userOrg.Role + + } + else + { + + } + @GetUserEmail(userOrg.GrantedBy)@userOrg.GrantedOn.ToString("MMM dd, yyyy") + @if (userOrg.IsActive && userOrg.RevokedOn == null) + { + Active + } + else + { + Revoked + } + + @if (userOrg.UserId != organization.OwnerId && userOrg.IsActive) + { + + } +
+
+ } +
+
+
+ +
+
+
+
Information
+
+
+

Organization Roles:

+
    +
  • Owner - Full control, cannot be changed or revoked
  • +
  • Administrator - Delegated admin access
  • +
  • PropertyManager - Property operations only
  • +
  • User - Limited/view-only access
  • +
+
+

Note: The organization owner cannot be changed or have their access revoked. To transfer ownership, contact support.

+
+
+
+
+ } +
+ + +@if (showAddUserModal) +{ + +} + +@code { + [Parameter] + public Guid Id { get; set; } = Guid.Empty; + + private bool isLoading = true; + private Organization? organization; + private List organizationUsers = new(); + private Dictionary userEmails = new(); + private bool showAddUserModal = false; + private List availableUsers = new(); + private string selectedUserId = string.Empty; + private string selectedRole = ApplicationConstants.OrganizationRoles.User; + private string addUserError = string.Empty; + + protected override async Task OnInitializedAsync() + { + await LoadOrganization(); + } + + private async Task LoadOrganization() + { + isLoading = true; + try + { + string? userId = await UserContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + return; + } + + // Check if user can manage this organization (Owner or Administrator) + var userRole = await OrganizationService.GetUserRoleForOrganizationAsync(userId, Id); + if (userRole != ApplicationConstants.OrganizationRoles.Owner && + userRole != ApplicationConstants.OrganizationRoles.Administrator) + { + organization = null; + return; + } + + organization = await OrganizationService.GetOrganizationByIdAsync(Id); + if (organization != null) + { + await LoadOrganizationUsers(); + await LoadAvailableUsers(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading organization: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task LoadOrganizationUsers() + { + if (organization == null) return; + + organizationUsers = await OrganizationService.GetOrganizationUsersAsync(Id); + + // Load user emails + foreach (var userOrg in organizationUsers) + { + var user = await UserManager.FindByIdAsync(userOrg.UserId); + if (user != null) + { + userEmails[userOrg.UserId] = user.Email ?? "Unknown"; + } + + var grantedByUser = await UserManager.FindByIdAsync(userOrg.GrantedBy); + if (grantedByUser != null) + { + userEmails[userOrg.GrantedBy] = grantedByUser.Email ?? "Unknown"; + } + } + } + + private async Task LoadAvailableUsers() + { + if (organization == null) return; + + // Get all users who are NOT already assigned to this organization + var allUsers = await UserManager.Users.ToListAsync(); + var nonSystemUsers = allUsers.Where(u => u.Id != ApplicationConstants.SystemUser.Id).ToList(); + var assignedUserIds = organizationUsers.Select(u => u.UserId).ToHashSet(); + + availableUsers = nonSystemUsers.Where(u => !assignedUserIds.Contains(u.Id)).ToList(); + } + + private string GetUserEmail(string userId) + { + return userEmails.ContainsKey(userId) ? userEmails[userId] : "Unknown"; + } + + private string GetRoleBadgeClass(string role) + { + return role switch + { + ApplicationConstants.OrganizationRoles.Owner => "bg-primary", + ApplicationConstants.OrganizationRoles.Administrator => "bg-info", + ApplicationConstants.OrganizationRoles.PropertyManager => "bg-success", + ApplicationConstants.OrganizationRoles.User => "bg-secondary", + _ => "bg-secondary" + }; + } + + private async Task ChangeUserRole(UserOrganization userOrg, string newRole) + { + try + { + if (newRole == userOrg.Role) return; + + var currentUserId = await UserContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(currentUserId)) + { + ToastService.ShowError("User not found"); + return; + } + + var success = await OrganizationService.UpdateUserRoleAsync(userOrg.UserId, Id, newRole, currentUserId); + + if (success) + { + ToastService.ShowSuccess($"User role updated to {newRole}"); + await LoadOrganizationUsers(); + } + else + { + ToastService.ShowError("Failed to update user role"); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error updating role: {ex.Message}"); + } + } + + private async Task RevokeUserAccess(UserOrganization userOrg) + { + if (!await JSRuntime.InvokeAsync("confirm", $"Are you sure you want to revoke {GetUserEmail(userOrg.UserId)}'s access to this organization?")) + { + return; + } + + try + { + var currentUserId = await UserContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(currentUserId)) + { + ToastService.ShowError("User not found"); + return; + } + + var success = await OrganizationService.RevokeOrganizationAccessAsync(userOrg.UserId, Id, currentUserId); + + if (success) + { + ToastService.ShowSuccess("User access revoked"); + await LoadOrganizationUsers(); + } + else + { + ToastService.ShowError("Failed to revoke user access"); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error revoking access: {ex.Message}"); + } + } + + private void ShowAddUserModal() + { + addUserError = string.Empty; + selectedUserId = string.Empty; + selectedRole = ApplicationConstants.OrganizationRoles.User; + showAddUserModal = true; + } + + private void HideAddUserModal() + { + showAddUserModal = false; + } + + private void AddApplicationUser() + { + Navigation.NavigateTo("/administration/users/create?returnUrl=" + Uri.EscapeDataString($"/administration/organizations/{Id}/users")); + } + private async Task AddUser() + { + addUserError = string.Empty; + + if (string.IsNullOrEmpty(selectedUserId)) + { + addUserError = "Please select a user"; + return; + } + + try + { + var currentUserId = await UserContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(currentUserId)) + { + addUserError = "Current user not found"; + return; + } + + var success = await OrganizationService.GrantOrganizationAccessAsync( + selectedUserId, + Id, + selectedRole, + currentUserId + ); + + if (success) + { + ToastService.ShowSuccess($"User added with {selectedRole} role"); + showAddUserModal = false; + await LoadOrganizationUsers(); + await LoadAvailableUsers(); + } + else + { + addUserError = "Failed to grant organization access"; + } + } + catch (Exception ex) + { + addUserError = $"Error adding user: {ex.Message}"; + } + } + + private void Cancel() + { + Navigation.NavigateTo($"/administration/organizations/view/{Id}"); + } +} diff --git a/Aquiis.Professional/Features/Administration/Organizations/Pages/Organizations.razor b/Aquiis.Professional/Features/Administration/Organizations/Pages/Organizations.razor new file mode 100644 index 0000000..5e3659e --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Organizations/Pages/Organizations.razor @@ -0,0 +1,214 @@ +@page "/administration/organizations" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Components.Shared +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization + +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService + +@attribute [OrganizationAuthorize("Owner", "Administrator")] +@rendermode InteractiveServer + +Organizations - Administration + +
+
+
+

Organizations

+

Manage your organizations and access

+
+ + + +
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (!organizations.Any()) + { +
+ No organizations found. + + Create your first organization + +
+ } + else + { +
+
+
+ + +
+
+
+ +
+
+
+ + + + + + + + + + + + + @foreach (var userOrg in filteredOrganizations) + { + + + + + + + + + } + +
Organization NameDisplay NameStateYour RoleStatusActions
+ @userOrg.Organization.Name + @(userOrg.Organization.DisplayName ?? "-")@(userOrg.Organization.State ?? "-") + + @userOrg.Role + + + @if (userOrg.Organization.IsActive) + { + Active + } + else + { + Inactive + } + +
+ + @if (userOrg.Role == ApplicationConstants.OrganizationRoles.Owner) + { + + } + @if (userOrg.Role == ApplicationConstants.OrganizationRoles.Owner || userOrg.Role == ApplicationConstants.OrganizationRoles.Administrator) + { + + } +
+
+
+
+
+ +
+ Showing @filteredOrganizations.Count of @organizations.Count organization(s) +
+ } +
+ +@code { + private bool isLoading = true; + private List organizations = new(); + private List filteredOrganizations = new(); + private string searchTerm = string.Empty; + + protected override async Task OnInitializedAsync() + { + await LoadOrganizations(); + } + + private async Task LoadOrganizations() + { + isLoading = true; + try + { + var userId = await UserContext.GetUserIdAsync(); + if (!string.IsNullOrEmpty(userId)) + { + organizations = await OrganizationService.GetActiveUserAssignmentsAsync(); + filteredOrganizations = organizations; + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading organizations: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private void ApplyFilters() + { + if (string.IsNullOrWhiteSpace(searchTerm)) + { + filteredOrganizations = organizations; + } + else + { + var search = searchTerm.ToLower(); + filteredOrganizations = organizations.Where(o => + (o.Organization.Name?.ToLower().Contains(search) ?? false) || + (o.Organization.DisplayName?.ToLower().Contains(search) ?? false) || + (o.Organization.State?.ToLower().Contains(search) ?? false) || + (o.Role?.ToLower().Contains(search) ?? false) + ).ToList(); + } + } + + private string GetRoleBadgeClass(string role) + { + return role switch + { + ApplicationConstants.OrganizationRoles.Owner => "bg-primary", + ApplicationConstants.OrganizationRoles.Administrator => "bg-info", + ApplicationConstants.OrganizationRoles.PropertyManager => "bg-success", + ApplicationConstants.OrganizationRoles.User => "bg-secondary", + _ => "bg-secondary" + }; + } + + private void NavigateToCreate() + { + Navigation.NavigateTo("/administration/organizations/create"); + } + + private void NavigateToView(Guid organizationId) + { + Navigation.NavigateTo($"/administration/organizations/view/{organizationId}"); + } + + private void NavigateToEdit(Guid organizationId) + { + Navigation.NavigateTo($"/administration/organizations/edit/{organizationId}"); + } + + private void NavigateToManageUsers(Guid organizationId) + { + Navigation.NavigateTo($"/administration/organizations/{organizationId}/users"); + } +} diff --git a/Aquiis.Professional/Features/Administration/Organizations/Pages/ViewOrganization.razor b/Aquiis.Professional/Features/Administration/Organizations/Pages/ViewOrganization.razor new file mode 100644 index 0000000..e75e0b9 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Organizations/Pages/ViewOrganization.razor @@ -0,0 +1,344 @@ +@page "/administration/organizations/view/{Id:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Identity + +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject UserManager UserManager +@inject NavigationManager Navigation +@inject ToastService ToastService + +@attribute [OrganizationAuthorize("Owner", "Administrator")] +@rendermode InteractiveServer + +View Organization - Administration + +
+
+

Organization Details

+
+ @if (isOwner) + { + + } + +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (organization == null) + { +
+ Organization not found or you don't have permission to view it. +
+ } + else + { +
+
+ +
+
+
Organization Information
+
+
+
+
Organization Name:
+
@organization.Name
+ +
Display Name:
+
@(organization.DisplayName ?? "-")
+ +
State:
+
@(organization.State ?? "-")
+ +
Status:
+
+ @if (organization.IsActive) + { + Active + } + else + { + Inactive + } +
+ +
Owner:
+
@ownerEmail
+ +
Created On:
+
@organization.CreatedOn.ToString("MMMM dd, yyyy")
+ + @if (organization.LastModifiedOn.HasValue) + { +
Last Modified:
+
@organization.LastModifiedOn.Value.ToString("MMMM dd, yyyy")
+ } +
+
+
+ + +
+
+
Users with Access
+ @if (isOwner || isAdministrator && isCurrentOrganization) + { + + } +
+
+ @if (!organizationUsers.Any()) + { +
+ No users assigned to this organization. +
+ } + else + { +
+ + + + + + + + + + + + @foreach (var userOrg in organizationUsers) + { + + + + + + + + } + +
UserRoleGranted ByGranted OnStatus
+
+ @GetUserEmail(userOrg.UserId) +
+
+ + @userOrg.Role + + @GetUserEmail(userOrg.GrantedBy)@userOrg.GrantedOn.ToString("MMM dd, yyyy") + @if (userOrg.IsActive && userOrg.RevokedOn == null) + { + Active + } + else + { + Revoked + } +
+
+ } +
+
+
+ +
+ +
+
+
Quick Stats
+
+
+
+
Total Users:
+
@organizationUsers.Count
+ +
Active Users:
+
@organizationUsers.Count(u => u.IsActive)
+ +
Owners:
+
@organizationUsers.Count(u => u.Role == ApplicationConstants.OrganizationRoles.Owner)
+ +
Administrators:
+
@organizationUsers.Count(u => u.Role == ApplicationConstants.OrganizationRoles.Administrator)
+ +
Property Managers:
+
@organizationUsers.Count(u => u.Role == ApplicationConstants.OrganizationRoles.PropertyManager)
+
+
+
+ + +
+
+
Your Access
+
+
+

Your Role:

+

+ + @currentUserRole + +

+
+

+ @if (isOwner) + { + You have full control over this organization as the Owner. + } + else if (isAdministrator) + { + You have administrative access to this organization. + } + else + { + You have limited access to this organization. + } +

+
+
+
+
+ } +
+ +@code { + [Parameter] + public Guid Id { get; set; } = Guid.Empty; + + private bool isLoading = true; + private Organization? organization; + private List organizationUsers = new(); + private Dictionary userEmails = new(); + private string ownerEmail = string.Empty; + private string currentUserRole = string.Empty; + private bool isOwner = false; + private bool isAdministrator = false; + + private bool isCurrentOrganization = false; + + protected override async Task OnInitializedAsync() + { + await LoadOrganization(); + } + + private async Task LoadOrganization() + { + isLoading = true; + try + { + var userId = await UserContext.GetUserIdAsync(); + var currentOrganizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + return; + } + + // if the organization being viewed is the current organization + // allow user management for Owner/Administrator roles + isCurrentOrganization = Id == currentOrganizationId; + + // Check if user has access to this organization + var canAccess = await OrganizationService.CanAccessOrganizationAsync(userId, Id); + if (!canAccess) + { + organization = null; + return; + } + + organization = await OrganizationService.GetOrganizationByIdAsync(Id); + if (organization != null) + { + // Get owner email + var owner = await UserManager.FindByIdAsync(organization.OwnerId); + ownerEmail = owner?.Email ?? "Unknown"; + + // Get users with access to this organization + organizationUsers = await OrganizationService.GetOrganizationUsersAsync(Id); + + // Load user emails + foreach (var userOrg in organizationUsers) + { + var user = await UserManager.FindByIdAsync(userOrg.UserId); + if (user != null) + { + userEmails[userOrg.UserId] = user.Email ?? "Unknown"; + } + + var grantedByUser = await UserManager.FindByIdAsync(userOrg.GrantedBy); + if (grantedByUser != null) + { + userEmails[userOrg.GrantedBy] = grantedByUser.Email ?? "Unknown"; + } + } + + // Get current user's role + var currentUserOrg = organizationUsers.FirstOrDefault(u => u.UserId == userId); + currentUserRole = currentUserOrg?.Role ?? "Unknown"; + isOwner = currentUserRole == ApplicationConstants.OrganizationRoles.Owner; + isAdministrator = currentUserRole == ApplicationConstants.OrganizationRoles.Administrator; + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading organization: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private string GetUserEmail(string userId) + { + return userEmails.ContainsKey(userId) ? userEmails[userId] : "Unknown"; + } + + private string GetRoleBadgeClass(string role) + { + return role switch + { + ApplicationConstants.OrganizationRoles.Owner => "bg-primary", + ApplicationConstants.OrganizationRoles.Administrator => "bg-info", + ApplicationConstants.OrganizationRoles.PropertyManager => "bg-success", + ApplicationConstants.OrganizationRoles.User => "bg-secondary", + _ => "bg-secondary" + }; + } + + private void NavigateToEdit() + { + Navigation.NavigateTo($"/administration/organizations/edit/{Id}"); + } + + private void NavigateToManageUsers() + { + Navigation.NavigateTo($"/administration/organizations/{Id}/users"); + } + + private void Cancel() + { + Navigation.NavigateTo("/administration/organizations"); + } +} diff --git a/Aquiis.Professional/Features/Administration/Settings/Pages/CalendarSettings.razor b/Aquiis.Professional/Features/Administration/Settings/Pages/CalendarSettings.razor new file mode 100644 index 0000000..e950749 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Settings/Pages/CalendarSettings.razor @@ -0,0 +1,378 @@ +@page "/administration/settings/calendar" +@using Aquiis.Professional.Core.Entities +@using CalendarSettingsEntity = Aquiis.Professional.Core.Entities.CalendarSettings +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Utilities +@using Microsoft.AspNetCore.Authorization + +@inject CalendarSettingsService SettingsService +@inject UserContextService UserContext +@inject ToastService ToastService +@inject NavigationManager Navigation +@rendermode InteractiveServer + +@attribute [OrganizationAuthorize("Owner", "Administrator")] + +Calendar Settings + +
+
+

Calendar Settings

+ @if (!string.IsNullOrEmpty(organizationName)) + { +

+ Settings for @organizationName + @if (!string.IsNullOrEmpty(userRole)) + { + @userRole + } +

+ } +

Configure which events are automatically added to the calendar

+
+ +
+ +@if (loading) +{ +
+
+ Loading... +
+
+} +else +{ +
+
+
+
+
Auto-Create Calendar Events
+
+
+
+ + How it works: When enabled, events are automatically created on the calendar when you create tours, inspections, maintenance requests, etc. + Disable a type if you prefer to manage those events manually. +
+ + @if (settings.Any()) + { +
+ @foreach (var setting in settings) + { +
+
+
+
+ +
+
@CalendarEventTypes.GetDisplayName(setting.EntityType)
+ @setting.EntityType events +
+
+
+
+
+ + +
+
+
+ +
+
+
+ } +
+ +
+ + @if (!canEdit) + { +
+ You have read-only access to these settings. +
+ } +
+ } + else + { +
+ + No schedulable entity types found. Make sure your entities implement ISchedulableEntity. +
+ } +
+
+ +
+
+
Default Calendar View Filters
+
+
+

Select which event types should be visible by default when opening the calendar.

+ + @if (settings.Any()) + { +
+ @foreach (var setting in settings) + { +
+
+
+ + @CalendarEventTypes.GetDisplayName(setting.EntityType) +
+
+ + +
+
+
+ } +
+ +
+ +
+ } +
+
+
+ +
+
+
+
Tips
+
+
+
Auto-Create Events
+

When enabled, calendar events are automatically created when you create or update the source entity (tour, inspection, etc.).

+ +
Default View Filters
+

These settings control which event types are shown by default when opening the calendar. Users can still toggle filters on/off.

+ +
Colors & Icons
+

Click the palette icon to customize the color and icon for each event type.

+ +
+ + Note: Disabling auto-create will prevent new events from being created, but won't delete existing calendar events. +
+
+
+
+
+} + + +@if (selectedSetting != null) +{ + +} + +@code { + private List settings = new(); + private CalendarSettingsEntity? selectedSetting; + private bool loading = true; + private bool saving = false; + private string organizationName = string.Empty; + private string userRole = string.Empty; + private bool canEdit = true; + + protected override async Task OnInitializedAsync() + { + // Get organization and role context + var organization = await UserContext.GetActiveOrganizationAsync(); + organizationName = organization?.Name ?? "Unknown Organization"; + userRole = await UserContext.GetCurrentOrganizationRoleAsync() ?? "User"; + canEdit = userRole != "User"; // User role is read-only + + await LoadSettings(); + } + + private async Task LoadSettings() + { + loading = true; + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + settings = await SettingsService.GetSettingsAsync(organizationId.Value); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading settings: {ex.Message}"); + } + finally + { + loading = false; + } + } + + private void BackToDashboard() + { + Navigation.NavigateTo("/administration/dashboard"); + } + + private string GetRoleBadgeClass() + { + return userRole switch + { + "Owner" => "bg-primary", + "Administrator" => "bg-success", + "PropertyManager" => "bg-info", + "User" => "bg-secondary", + _ => "bg-secondary" + }; + } + + private void ToggleAutoCreate(CalendarSettingsEntity setting, bool enabled) + { + setting.AutoCreateEvents = enabled; + } + + private void ToggleShowOnCalendar(CalendarSettingsEntity setting, bool show) + { + setting.ShowOnCalendar = show; + } + + private async Task SaveAllSettings() + { + saving = true; + try + { + await SettingsService.UpdateMultipleSettingsAsync(settings); + ToastService.ShowSuccess("Calendar settings saved successfully"); + } + catch (Exception ex) + { + ToastService.ShowError($"Error saving settings: {ex.Message}"); + } + finally + { + saving = false; + } + } + + private void ShowColorPicker(CalendarSettingsEntity setting) + { + selectedSetting = setting; + } + + private void CloseColorPicker() + { + selectedSetting = null; + } + + private async Task SaveColorSettings() + { + if (selectedSetting != null) + { + try + { + await SettingsService.UpdateSettingAsync(selectedSetting); + ToastService.ShowSuccess("Color and icon updated"); + CloseColorPicker(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error updating settings: {ex.Message}"); + } + } + } +} diff --git a/Aquiis.Professional/Features/Administration/Settings/Pages/EmailSettings.razor b/Aquiis.Professional/Features/Administration/Settings/Pages/EmailSettings.razor new file mode 100644 index 0000000..722e812 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Settings/Pages/EmailSettings.razor @@ -0,0 +1,454 @@ +@page "/administration/settings/email" +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Infrastructure.Services +@using SocketIOClient.Messages +@using System.ComponentModel.DataAnnotations +@inject EmailSettingsService EmailSettingsService +@inject SendGridEmailService EmailService +@inject ToastService ToastService +@inject IJSRuntime JSRuntime + +@inject NavigationManager Navigation + +@inject UserContextService _userContext + +@rendermode InteractiveServer + +Email Settings - Aquiis + +
+
+

+ Email Configuration +

+

+ Configure SendGrid integration for automated email notifications +

+
+ +
+ + @if (settings == null) + { +
+
+ Loading... +
+
+ } + else if (!settings.IsEmailEnabled) + { +
+
+
+

Email Integration Not Configured

+

Enable automated email notifications by connecting your SendGrid account.

+ +
Why Use SendGrid?
+
    +
  • Free tier: 100 emails per day forever (perfect for getting started)
  • +
  • Reliable delivery: Industry-leading email infrastructure
  • +
  • Analytics: Track opens, clicks, and bounces
  • +
  • Your account: You manage billing and usage directly
  • +
+ +
Setup Steps:
+
    +
  1. + + Create a free SendGrid account + +
  2. +
  3. Generate an API key with "Mail Send" permissions
  4. +
  5. Click the button below to configure your API key
  6. +
+ + +
+
+
+
+
+
Need Help?
+
+
+
Common Questions
+

+ Do I need a paid account?
+ No! The free tier (100 emails/day) is usually sufficient. +

+

+ What happens without email?
+ The app works fine. Notifications appear in-app only. +

+

+ Is my API key secure?
+ Yes, it's encrypted and never shared. +

+
+ + API Key Guide + +
+
+
+
+ } + else + { +
+
+
+
+ Email Integration Active +
+
+
+
+
Configuration
+

+ From Email:
+ @settings.FromEmail +

+

+ From Name:
+ @settings.FromName +

+

+ + Verified @settings.LastVerifiedOn?.ToString("g") +

+
+
+ @if (stats != null && stats.IsConfigured) + { +
Usage Statistics
+
+
+ Today: + @stats.EmailsSentToday / @stats.DailyLimit +
+
+
+ @(stats.DailyPercentUsed)% +
+
+
+
+
+ This Month: + @stats.EmailsSentThisMonth / @stats.MonthlyLimit +
+
+
+ @(stats.MonthlyPercentUsed)% +
+
+
+

+ + Plan: @stats.PlanType + @if (stats.LastEmailSentOn.HasValue) + { +
Last sent: @stats.LastEmailSentOn?.ToString("g") + } +

+ } +
+
+ + @if (!string.IsNullOrEmpty(settings.LastError)) + { +
+ + Recent Error: @settings.LastError +
+ Try updating your API key or contact SendGrid support +
+ } + +
+ + + + +
+
+
+ +
+
+ Email Activity +
+
+

+ View detailed email statistics in your + + SendGrid Dashboard + +

+
+
+
+ +
+
+
+
Tips
+
+
+
Optimize Email Usage
+
    +
  • Enable daily/weekly digest mode to batch notifications
  • +
  • Let users configure their notification preferences
  • +
  • Monitor your usage to avoid hitting limits
  • +
  • Consider upgrading if you consistently hit daily limits
  • +
+ +
SendGrid Features
+
    +
  • Templates: Create branded email templates
  • +
  • Analytics: Track opens and clicks
  • +
  • Webhooks: Get delivery notifications
  • +
  • Lists: Manage recipient groups
  • +
+
+
+
+
+ } + +@* Configuration Modal *@ +@if (showConfigModal) +{ + +} + +@code { + private OrganizationEmailSettings? settings; + private SendGridStats? stats; + private bool showConfigModal; + private bool isSaving; + private bool isRefreshing; + private ConfigurationModel configModel = new(); + + protected override async Task OnInitializedAsync() + { + await LoadSettings(); + } + + private async Task LoadSettings() + { + settings = await EmailSettingsService.GetOrCreateSettingsAsync(); + // TODO Phase 2.5: Uncomment when GetSendGridStatsAsync is implemented + // if (settings.IsEmailEnabled) + // { + // stats = await EmailService.GetSendGridStatsAsync(); + // } + } + + private async Task SaveConfiguration() + { + isSaving = true; + + var result = await EmailSettingsService.UpdateSendGridConfigAsync( + configModel.ApiKey!, + configModel.FromEmail!, + configModel.FromName!); + + if (result.Success) + { + ToastService.ShowSuccess(result.Message); + showConfigModal = false; + configModel = new(); // Clear sensitive data + await LoadSettings(); + } + else + { + ToastService.ShowError(result.Message); + } + + isSaving = false; + } + + private async Task SendTestEmail() + { + var userEmail = await _userContext.GetUserEmailAsync(); + var testEmail = await JSRuntime.InvokeAsync("prompt", + "Enter email address to send test email:", + userEmail ?? settings?.FromEmail ?? ""); + + if (!string.IsNullOrEmpty(testEmail)) + { + var result = await EmailSettingsService.TestEmailConfigurationAsync(testEmail); + if (result.Success) + ToastService.ShowSuccess(result.Message); + else + ToastService.ShowError(result.Message); + } + } + + private async Task DisableEmail() + { + var confirmed = await JSRuntime.InvokeAsync("confirm", + "Are you sure you want to disable email notifications?\n\n" + + "Notifications will only appear in-app until you re-enable email."); + + if (confirmed) + { + var result = await EmailSettingsService.DisableEmailAsync(); + ToastService.ShowInfo(result.Message); + await LoadSettings(); + } + } + + private async Task RefreshStats() + { + isRefreshing = true; + await LoadSettings(); + isRefreshing = false; + ToastService.ShowSuccess("Statistics refreshed"); + } + + private void CloseModal() + { + showConfigModal = false; + configModel = new(); // Clear sensitive data + } + + private string GetProgressBarClass(int percent) + { + return percent switch + { + ( < 50) => "bg-success", + ( < 75) => "bg-info", + ( < 90) => "bg-warning", + _ => "bg-danger" + }; + } + + private void BackToDashboard() + { + Navigation.NavigateTo("/administration/dashboard"); + } + + public class ConfigurationModel + { + [Required(ErrorMessage = "SendGrid API key is required")] + [StringLength(100, MinimumLength = 32, ErrorMessage = "API key must be at least 32 characters")] + public string? ApiKey { get; set; } + + [Required(ErrorMessage = "From email address is required")] + [EmailAddress(ErrorMessage = "Invalid email address")] + public string? FromEmail { get; set; } + + [Required(ErrorMessage = "From name is required")] + [StringLength(200, MinimumLength = 2, ErrorMessage = "From name must be 2-200 characters")] + public string? FromName { get; set; } + } +} + + \ No newline at end of file diff --git a/Aquiis.Professional/Features/Administration/Settings/Pages/LateFeeSettings.razor b/Aquiis.Professional/Features/Administration/Settings/Pages/LateFeeSettings.razor new file mode 100644 index 0000000..fe2f1e5 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Settings/Pages/LateFeeSettings.razor @@ -0,0 +1,439 @@ +@page "/administration/settings/latefees" +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations + +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject ToastService ToastService +@inject NavigationManager Navigation +@rendermode InteractiveServer + +@attribute [OrganizationAuthorize("Owner", "Administrator")] + +Late Fee Settings + +
+
+

Late Fee Settings

+ @if (!string.IsNullOrEmpty(organizationName)) + { +

+ Settings for @organizationName + @if (!string.IsNullOrEmpty(userRole)) + { + @userRole + } +

+ } +
+ +
+ +
+
+
+
+
Automated Late Fee Configuration
+
+
+
+ + How it works: The system automatically checks for overdue invoices daily at 2 AM. + After the grace period expires, a late fee will be automatically applied to unpaid invoices. +
+ + + + +
+ + + Your organization's name for reports and documents + +
+ +
+ +
Late Fee Configuration
+ +
+
+ + + Master switch for the entire late fee feature +
+
+ +
+
+ + + Automatically apply late fees to overdue invoices (if disabled, invoices will only be marked as overdue) +
+
+ +
+ +
+ + + Number of days after due date before late fees apply + +
+ +
+ +
+ + % +
+ Percentage of invoice amount to charge as late fee + +
+ +
+ +
+ $ + +
+ Cap on the maximum late fee that can be charged + +
+ +
+ +
+
+ + + Send payment reminders before invoices are due +
+
+ +
+ + + Send payment reminder this many days before due date + +
+ +
+ +
Tour Settings
+ +
+ + + Hours after scheduled time before tour is automatically marked as "No Show" + +
+ +
+ + How it works: If a tour remains in "Scheduled" status @viewModel.TourNoShowGracePeriodHours hours after its scheduled time, + it will automatically be marked as "No Show". This gives property managers time to complete documentation while ensuring accurate tracking. +
+ +
+ Example:
+ Tour Scheduled: Today at 2:00 PM
+ Grace Period: @viewModel.TourNoShowGracePeriodHours hours
+ Auto Mark as No-Show After: @DateTime.Now.Date.AddHours(14 + viewModel.TourNoShowGracePeriodHours).ToString("MMM dd, yyyy h:mm tt") +
+ +
+ Example Calculation:
+ Invoice Amount: $1,000
+ Grace Period: @viewModel.LateFeeGracePeriodDays days
+ Late Fee: @viewModel.LateFeePercentage% = $@((1000 * (viewModel.LateFeePercentage / 100)).ToString("F2"))
+ Capped at: $@viewModel.MaxLateFeeAmount
+ @{ + var calculatedFee = 1000 * (viewModel.LateFeePercentage / 100); + var actualFee = Math.Min(calculatedFee, viewModel.MaxLateFeeAmount); + } + Actual Late Fee: $@actualFee.ToString("F2") +
+ +
+ + + @if (!canEdit) + { +
+ You have read-only access to these settings. +
+ } +
+
+
+
+
+ +
+
+
+
Current Configuration
+
+
+
+
Late Fees
+
+ @if (viewModel.LateFeeEnabled) + { + Enabled + } + else + { + Disabled + } +
+ +
Auto-Apply
+
+ @if (viewModel.LateFeeAutoApply && viewModel.LateFeeEnabled) + { + Enabled + } + else + { + Disabled + } +
+ +
Grace Period
+
@viewModel.LateFeeGracePeriodDays days
+ +
Late Fee Rate
+
@viewModel.LateFeePercentage%
+ +
Maximum Late Fee
+
$@viewModel.MaxLateFeeAmount
+ +
Payment Reminders
+
+ @if (viewModel.PaymentReminderEnabled) + { + Enabled + } + else + { + Disabled + } +
+ +
Reminder Timing
+
@viewModel.PaymentReminderDaysBefore days before due
+ +
Tour No-Show Grace Period
+
@viewModel.TourNoShowGracePeriodHours hours
+
+
+
+ +
+
+
Information
+
+
+

Scheduled Task: Daily at 2:00 AM

+

Next Run: @GetNextRunTime()

+
+ + Late fees are automatically applied by the system background service. + Changes to these settings will take effect on the next scheduled run. + +
+
+
+
+ +@code { + private LateFeeSettingsViewModel viewModel = new(); + private bool isSaving = false; + private string organizationName = string.Empty; + private string userRole = string.Empty; + private bool canEdit = true; + + protected override async Task OnInitializedAsync() + { + try + { + // Get organization and role context + var org = await UserContext.GetActiveOrganizationAsync(); + organizationName = org?.Name ?? "Unknown Organization"; + userRole = await UserContext.GetCurrentOrganizationRoleAsync() ?? "User"; + canEdit = userRole != "User"; // User role is read-only + + var settings = await OrganizationService.GetOrganizationSettingsAsync(); + + if (settings != null) + { + // Map entity to view model + viewModel = new LateFeeSettingsViewModel + { + Name = settings.Name, + LateFeeEnabled = settings.LateFeeEnabled, + LateFeeAutoApply = settings.LateFeeAutoApply, + LateFeeGracePeriodDays = settings.LateFeeGracePeriodDays, + LateFeePercentage = settings.LateFeePercentage * 100, // Convert to percentage display + MaxLateFeeAmount = settings.MaxLateFeeAmount, + PaymentReminderEnabled = settings.PaymentReminderEnabled, + PaymentReminderDaysBefore = settings.PaymentReminderDaysBefore, + TourNoShowGracePeriodHours = settings.TourNoShowGracePeriodHours + }; + } + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to load settings: {ex.Message}"); + } + } + + private void BackToDashboard() + { + Navigation.NavigateTo("/administration/dashboard"); + } + + private string GetRoleBadgeClass() + { + return userRole switch + { + "Owner" => "bg-primary", + "Administrator" => "bg-success", + "PropertyManager" => "bg-info", + "User" => "bg-secondary", + _ => "bg-secondary" + }; + } + + private async Task SaveSettings() + { + try + { + isSaving = true; + + // Get the existing entity + var settings = await OrganizationService.GetOrganizationSettingsAsync(); + + if (settings == null) + { + ToastService.ShowError("Failed to load organization settings"); + return; + } + + // Map view model back to entity + settings.Name = viewModel.Name; + settings.LateFeeEnabled = viewModel.LateFeeEnabled; + settings.LateFeeAutoApply = viewModel.LateFeeAutoApply; + settings.LateFeeGracePeriodDays = viewModel.LateFeeGracePeriodDays; + settings.LateFeePercentage = viewModel.LateFeePercentage / 100; // Convert from percentage display + settings.MaxLateFeeAmount = viewModel.MaxLateFeeAmount; + settings.PaymentReminderEnabled = viewModel.PaymentReminderEnabled; + settings.PaymentReminderDaysBefore = viewModel.PaymentReminderDaysBefore; + settings.TourNoShowGracePeriodHours = viewModel.TourNoShowGracePeriodHours; + + await OrganizationService.UpdateOrganizationSettingsAsync(settings); + + ToastService.ShowSuccess("Late fee settings saved successfully!"); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to save settings: {ex.Message}"); + } + finally + { + isSaving = false; + } + } + + private void ResetToDefaults() + { + viewModel.LateFeeEnabled = true; + viewModel.LateFeeAutoApply = true; + viewModel.LateFeeGracePeriodDays = 3; + viewModel.LateFeePercentage = 5.0m; + viewModel.MaxLateFeeAmount = 50.00m; + viewModel.PaymentReminderEnabled = true; + viewModel.PaymentReminderDaysBefore = 3; + viewModel.TourNoShowGracePeriodHours = 24; + + ToastService.ShowInfo("Settings reset to defaults"); + } + + private string GetNextRunTime() + { + var now = DateTime.Now; + var next2AM = DateTime.Today.AddDays(1).AddHours(2); + + if (now.Hour < 2) + { + next2AM = DateTime.Today.AddHours(2); + } + + return next2AM.ToString("MMM dd, yyyy h:mm tt"); + } + + public class LateFeeSettingsViewModel + { + [MaxLength(200)] + [Display(Name = "Organization Name")] + public string? Name { get; set; } + + [Display(Name = "Enable Late Fees")] + public bool LateFeeEnabled { get; set; } = true; + + [Display(Name = "Auto-Apply Late Fees")] + public bool LateFeeAutoApply { get; set; } = true; + + [Required] + [Range(0, 30, ErrorMessage = "Grace period must be between 0 and 30 days")] + [Display(Name = "Grace Period (Days)")] + public int LateFeeGracePeriodDays { get; set; } = 3; + + [Required] + [Range(0.0, 100.0, ErrorMessage = "Late fee percentage must be between 0% and 100%")] + [Display(Name = "Late Fee Percentage")] + public decimal LateFeePercentage { get; set; } = 5.0m; + + [Required] + [Range(0.0, 10000.0, ErrorMessage = "Maximum late fee must be between $0 and $10,000")] + [Display(Name = "Maximum Late Fee Amount")] + public decimal MaxLateFeeAmount { get; set; } = 50.00m; + + [Display(Name = "Enable Payment Reminders")] + public bool PaymentReminderEnabled { get; set; } = true; + + [Required] + [Range(1, 30, ErrorMessage = "Reminder days must be between 1 and 30")] + [Display(Name = "Send Reminder (Days Before Due)")] + public int PaymentReminderDaysBefore { get; set; } = 3; + + [Required] + [Range(1, 168, ErrorMessage = "Grace period must be between 1 and 168 hours")] + [Display(Name = "Tour No-Show Grace Period (Hours)")] + public int TourNoShowGracePeriodHours { get; set; } = 24; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/Administration/Settings/Pages/OrganizationSettings.razor b/Aquiis.Professional/Features/Administration/Settings/Pages/OrganizationSettings.razor new file mode 100644 index 0000000..3a22d90 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Settings/Pages/OrganizationSettings.razor @@ -0,0 +1,566 @@ +@page "/administration/settings/organization" + +@using Aquiis.Professional.Core.Entities +@using OrganizationSettingsEntity = Aquiis.Professional.Core.Entities.OrganizationSettings +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations + +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject ToastService ToastService +@inject NavigationManager Navigation + +@attribute [OrganizationAuthorize("Owner", "Administrator")] +@rendermode InteractiveServer + +Organization Settings + +
+ @*
+
+ +
+
*@ + +
+
+
+
+

Organization Settings

+ @if (!string.IsNullOrEmpty(organizationName)) + { +

+ Settings for @organizationName + @if (!string.IsNullOrEmpty(userRole)) + { + @userRole + } +

+ } +
+ @if (canManageOrganizations) + { + + Manage Organizations + + } + +
+
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (settings == null) + { +
+

No Settings Found

+

Organization settings have not been configured yet. Default values will be used.

+
+ +
+ } + else + { + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + + + + +
+
+ +
+
+
General Settings
+
+
+
+ + + This name appears on documents and reports +
+
+
+ + +
+
+
Application Fee Settings
+
+
+
+ + +
+ + @if (settingsModel.ApplicationFeeEnabled) + { +
+
+ +
+ $ + +
+ + Standard fee charged per application (non-refundable) +
+
+ +
+ + days +
+ + Applications expire if not processed within this period +
+
+ } +
+
+ + +
+
+
Late Fee Settings
+
+
+
+ + +
+ + @if (settingsModel.LateFeeEnabled) + { +
+ + +
+ +
+
+ +
+ + days +
+ + Days after due date before late fee applies +
+
+ +
+ + % +
+ + Percentage of rent amount (e.g., 0.05 = 5%) +
+
+ +
+ $ + +
+ + Cap on late fee amount +
+
+ + @if (settingsModel.LateFeePercentage > 0) + { +
+ Example: For a $1,000 rent payment: +
    +
  • Calculated late fee: $@((1000 * settingsModel.LateFeePercentage).ToString("F2"))
  • +
  • Actual late fee (with cap): $@(Math.Min(1000 * settingsModel.LateFeePercentage, settingsModel.MaxLateFeeAmount).ToString("F2"))
  • +
+
+ } + } +
+
+ + +
+
+
Payment Reminder Settings
+
+
+
+ + +
+ + @if (settingsModel.PaymentReminderEnabled) + { +
+ +
+ + days +
+ + Tenants receive reminder this many days before rent is due +
+ } +
+
+ + +
+
+
Tour Settings
+
+
+
+ +
+ + hours +
+ + Time after scheduled tour before marking as no-show +
+
+
+ + +
+ +
+ + @if (!canEdit) + { +
+ You have read-only access to these settings. +
+ } +
+
+
+ +
+ +
+
+
Settings Summary
+
+
+
Application Fees
+
    +
  • Status: @(settingsModel.ApplicationFeeEnabled ? "✅ Enabled" : "❌ Disabled")
  • + @if (settingsModel.ApplicationFeeEnabled) + { +
  • Fee: $@settingsModel.DefaultApplicationFee.ToString("F2")
  • +
  • Expires: @settingsModel.ApplicationExpirationDays days
  • + } +
+ +
Late Fees
+
    +
  • Status: @(settingsModel.LateFeeEnabled ? "✅ Enabled" : "❌ Disabled")
  • + @if (settingsModel.LateFeeEnabled) + { +
  • Grace Period: @settingsModel.LateFeeGracePeriodDays days
  • +
  • Percentage: @(settingsModel.LateFeePercentage * 100)%
  • +
  • Max Fee: $@settingsModel.MaxLateFeeAmount.ToString("F2")
  • +
  • Auto-Apply: @(settingsModel.LateFeeAutoApply ? "Yes" : "No")
  • + } +
+ +
Payment Reminders
+
    +
  • Status: @(settingsModel.PaymentReminderEnabled ? "✅ Enabled" : "❌ Disabled")
  • + @if (settingsModel.PaymentReminderEnabled) + { +
  • Reminder: @settingsModel.PaymentReminderDaysBefore days before due
  • + } +
+ +
Tour Settings
+
    +
  • No-Show Grace: @settingsModel.TourNoShowGracePeriodHours hours
  • +
+
+
+ + +
+
+
About Settings
+
+
+

+ Organization Settings apply to all properties and tenants within your organization. +

+

+ Changes take effect immediately but do not retroactively affect existing invoices or applications. +

+

+ + Tip: Review settings periodically to ensure they align with your current policies. +

+
+
+
+
+
+ } +
+ +@code { + private OrganizationSettingsEntity? settings; + private OrganizationSettingsModel settingsModel = new(); + private bool isLoading = true; + private bool isSubmitting = false; + private string errorMessage = string.Empty; + private string successMessage = string.Empty; + private Guid organizationId = Guid.Empty; + private string organizationName = string.Empty; + private string userRole = string.Empty; + private bool canEdit = true; + private bool canManageOrganizations = false; + + protected override async Task OnInitializedAsync() + { + try + { + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + await LoadSettings(); + } + catch (Exception ex) + { + errorMessage = $"Error loading settings: {ex.Message}"; + } + finally + { + isLoading = false; + } + } + + private async Task LoadSettings() + { + settings = await OrganizationService.GetOrganizationSettingsAsync(); + + if (settings != null) + { + // Map to model + settingsModel = new OrganizationSettingsModel + { + Name = settings.Name, + ApplicationFeeEnabled = settings.ApplicationFeeEnabled, + DefaultApplicationFee = settings.DefaultApplicationFee, + ApplicationExpirationDays = settings.ApplicationExpirationDays, + LateFeeEnabled = settings.LateFeeEnabled, + LateFeeAutoApply = settings.LateFeeAutoApply, + LateFeeGracePeriodDays = settings.LateFeeGracePeriodDays, + LateFeePercentage = settings.LateFeePercentage, + MaxLateFeeAmount = settings.MaxLateFeeAmount, + PaymentReminderEnabled = settings.PaymentReminderEnabled, + PaymentReminderDaysBefore = settings.PaymentReminderDaysBefore, + TourNoShowGracePeriodHours = settings.TourNoShowGracePeriodHours + }; + } + } + + private async Task CreateDefaultSettings() + { + isSubmitting = true; + errorMessage = string.Empty; + + try + { + settings = new OrganizationSettingsEntity + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Name = "My Organization", + ApplicationFeeEnabled = true, + DefaultApplicationFee = 50.00m, + ApplicationExpirationDays = 30, + LateFeeEnabled = true, + LateFeeAutoApply = true, + LateFeeGracePeriodDays = 3, + LateFeePercentage = 0.05m, + MaxLateFeeAmount = 50.00m, + PaymentReminderEnabled = true, + PaymentReminderDaysBefore = 3, + TourNoShowGracePeriodHours = 24, + CreatedOn = DateTime.UtcNow, + CreatedBy = "System" + }; + + await OrganizationService.UpdateOrganizationSettingsAsync(settings); + successMessage = "Default settings created successfully!"; + await LoadSettings(); + } + catch (Exception ex) + { + errorMessage = $"Error creating default settings: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private async Task HandleSaveSettings() + { + if (settings == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + successMessage = string.Empty; + + try + { + // Update settings from model + settings.Name = settingsModel.Name; + settings.ApplicationFeeEnabled = settingsModel.ApplicationFeeEnabled; + settings.DefaultApplicationFee = settingsModel.DefaultApplicationFee; + settings.ApplicationExpirationDays = settingsModel.ApplicationExpirationDays; + settings.LateFeeEnabled = settingsModel.LateFeeEnabled; + settings.LateFeeAutoApply = settingsModel.LateFeeAutoApply; + settings.LateFeeGracePeriodDays = settingsModel.LateFeeGracePeriodDays; + settings.LateFeePercentage = settingsModel.LateFeePercentage; + settings.MaxLateFeeAmount = settingsModel.MaxLateFeeAmount; + settings.PaymentReminderEnabled = settingsModel.PaymentReminderEnabled; + settings.PaymentReminderDaysBefore = settingsModel.PaymentReminderDaysBefore; + settings.TourNoShowGracePeriodHours = settingsModel.TourNoShowGracePeriodHours; + settings.LastModifiedOn = DateTime.UtcNow; + + await OrganizationService.UpdateOrganizationSettingsAsync(settings); + + successMessage = "Settings saved successfully!"; + ToastService.ShowSuccess("Organization settings updated successfully."); + } + catch (Exception ex) + { + errorMessage = $"Error saving settings: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private void BackToDashboard(){ + Navigation.NavigateTo("/administration/dashboard"); + } + + private void Cancel() + { + Navigation.NavigateTo("/administration/dashboard"); + } + + private string GetRoleBadgeClass() + { + return userRole switch + { + "Owner" => "bg-primary", + "Administrator" => "bg-success", + "PropertyManager" => "bg-info", + "User" => "bg-secondary", + _ => "bg-secondary" + }; + } + + public class OrganizationSettingsModel + { + [StringLength(200)] + public string? Name { get; set; } + + public bool ApplicationFeeEnabled { get; set; } = true; + + [Required] + [Range(0, 1000, ErrorMessage = "Application fee must be between $0 and $1,000")] + public decimal DefaultApplicationFee { get; set; } = 50.00m; + + [Required] + [Range(1, 90, ErrorMessage = "Expiration period must be between 1 and 90 days")] + public int ApplicationExpirationDays { get; set; } = 30; + + public bool LateFeeEnabled { get; set; } = true; + + public bool LateFeeAutoApply { get; set; } = true; + + [Required] + [Range(0, 30, ErrorMessage = "Grace period must be between 0 and 30 days")] + public int LateFeeGracePeriodDays { get; set; } = 3; + + [Required] + [Range(0, 1, ErrorMessage = "Late fee percentage must be between 0% and 100%")] + public decimal LateFeePercentage { get; set; } = 0.05m; + + [Required] + [Range(0, 10000, ErrorMessage = "Maximum late fee must be between $0 and $10,000")] + public decimal MaxLateFeeAmount { get; set; } = 50.00m; + + public bool PaymentReminderEnabled { get; set; } = true; + + [Required] + [Range(1, 30, ErrorMessage = "Reminder period must be between 1 and 30 days")] + public int PaymentReminderDaysBefore { get; set; } = 3; + + [Required] + [Range(1, 168, ErrorMessage = "Grace period must be between 1 and 168 hours (1 week)")] + public int TourNoShowGracePeriodHours { get; set; } = 24; + } +} diff --git a/Aquiis.Professional/Features/Administration/Settings/Pages/SMSSettings.razor b/Aquiis.Professional/Features/Administration/Settings/Pages/SMSSettings.razor new file mode 100644 index 0000000..5eb4858 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Settings/Pages/SMSSettings.razor @@ -0,0 +1,418 @@ +@page "/administration/settings/sms" +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Infrastructure.Services +@using SocketIOClient.Messages +@using System.ComponentModel.DataAnnotations +@inject SMSSettingsService SMSSettingsService +@inject TwilioSMSService TwilioSMSService + +@inject ToastService ToastService +@inject IJSRuntime JSRuntime + +@inject UserContextService _userContext + +@inject NavigationManager Navigation + +@rendermode InteractiveServer + +SMS Settings - Aquiis +
+
+

+ SMS Configuration +

+

+ Configure Twilio integration for automated SMS notifications +

+
+ +
+ + @if (settings == null) + { +
+
+ Loading... +
+
+ } + else if (!settings.IsSMSEnabled) + { +
+
+
+

SMS Integration Not Configured

+

Enable automated SMS notifications by connecting your Twilio account.

+ +
Why Use Twilio?
+
    +
  • Free trial: $15 credit for testing (perfect for getting started)
  • +
  • Reliable delivery: Industry-leading SMS infrastructure
  • +
  • Analytics: Track message delivery and status
  • +
  • Your account: You manage billing and usage directly
  • +
+ +
Setup Steps:
+
    +
  1. + + Create a free Twilio account + +
  2. +
  3. Generate an API key with "Messaging" permissions
  4. +
  5. Click the button below to configure your API key
  6. +
+ + +
+
+
+
+
+
Need Help?
+
+
+
Common Questions
+

+ Do I need a paid account?
+ You get $15 free trial credit. Pay-as-you-go after that. +

+

+ What happens without SMS?
+ The app works fine. Notifications appear in-app only. +

+

+ Is my API key secure?
+ Yes, it's encrypted and never shared. +

+
+ + API Key Guide + +
+
+
+
+ } + else + { +
+
+
+
+ SMS Integration Active +
+
+
+
+
Configuration
+

+ Twilio Phone Number:
+ @settings.TwilioPhoneNumber +

+

+ + Verified @settings.LastVerifiedOn?.ToString("g") +

+
+
+ @* TODO Phase 2.5: Implement SMS usage statistics display *@ +
Usage Statistics
+

+ + SMS usage statistics will be available after Twilio integration (Phase 2.5) +

+
+
+ + @if (!string.IsNullOrEmpty(settings.LastError)) + { +
+ + Recent Error: @settings.LastError +
+ Try updating your API key or contact Twilio support +
+ } + +
+ + + + +
+
+
+ +
+
+ SMS Activity +
+
+

+ View detailed SMS statistics in your + + Twilio Dashboard + +

+
+
+
+ +
+
+
+
Tips
+
+
+
Optimize SMS Usage
+
    +
  • Enable daily/weekly digest mode to batch notifications
  • +
  • Let users configure their notification preferences
  • +
  • Monitor your usage to avoid hitting limits
  • +
  • Consider upgrading if you consistently hit daily limits
  • +
+ +
Twilio Features
+
    +
  • Templates: Use message templates and variables
  • +
  • Analytics: Track delivery and status
  • +
  • Webhooks: Get delivery notifications
  • +
  • Phone Numbers: Purchase dedicated numbers
  • +
+
+
+
+
+ } + +@* Configuration Modal *@ +@if (showConfigModal) +{ + +} + +@code { + private OrganizationSMSSettings? settings; + private TwilioStats? stats; + private bool showConfigModal; + private bool isSaving; + private bool isRefreshing; + private ConfigurationModel configModel = new(); + + protected override async Task OnInitializedAsync() + { + await LoadSettings(); + } + + private async Task LoadSettings() + { + settings = await SMSSettingsService.GetOrCreateSettingsAsync(); + // TODO Phase 2.5: Uncomment when GetTwilioStatsAsync is implemented + // if (settings.IsSMSEnabled) + // { + // stats = await SMSService.GetTwilioStatsAsync(); + // } + } + + private async Task SaveConfiguration() + { + isSaving = true; + + var result = await SMSSettingsService.UpdateTwilioConfigAsync( + configModel.AccountSid!, + configModel.AuthToken!, + configModel.PhoneNumber!); + + if (result.Success) + { + ToastService.ShowSuccess(result.Message); + showConfigModal = false; + configModel = new(); // Clear sensitive data + await LoadSettings(); + } + else + { + ToastService.ShowError(result.Message); + } + + isSaving = false; + } + + private async Task SendTestSMS() + { + var testPhone = await JSRuntime.InvokeAsync("prompt", + "Enter phone number to send test SMS (E.164 format, e.g., +1234567890):", + ""); + + if (!string.IsNullOrEmpty(testPhone)) + { + var result = await SMSSettingsService.TestSMSConfigurationAsync(testPhone); + if (result.Success) + ToastService.ShowSuccess(result.Message); + else + ToastService.ShowError(result.Message); + } + } + + private async Task DisableSMS() + { + var confirmed = await JSRuntime.InvokeAsync("confirm", + "Are you sure you want to disable SMS notifications?\n\n" + + "Notifications will only appear in-app until you re-enable SMS."); + + if (confirmed) + { + var result = await SMSSettingsService.DisableSMSAsync(); + ToastService.ShowInfo(result.Message); + await LoadSettings(); + } + } + + private async Task RefreshStats() + { + isRefreshing = true; + await LoadSettings(); + isRefreshing = false; + ToastService.ShowSuccess("Statistics refreshed"); + } + + private void CloseModal() + { + showConfigModal = false; + configModel = new(); // Clear sensitive data + } + + private string GetProgressBarClass(int percent) + { + return percent switch + { + ( < 50) => "bg-success", + ( < 75) => "bg-info", + ( < 90) => "bg-warning", + _ => "bg-danger" + }; + } + + private void BackToDashboard() + { + Navigation.NavigateTo("/administration/dashboard"); + } + + public class ConfigurationModel + { + [Required(ErrorMessage = "Twilio Account SID is required")] + [StringLength(100, MinimumLength = 34, ErrorMessage = "Account SID must be at least 34 characters")] + public string? AccountSid { get; set; } + + [Required(ErrorMessage = "Twilio Auth Token is required")] + [StringLength(100, MinimumLength = 32, ErrorMessage = "Auth Token must be at least 32 characters")] + public string? AuthToken { get; set; } + + [Required(ErrorMessage = "Twilio phone number is required")] + [Phone(ErrorMessage = "Invalid phone number format")] + public string? PhoneNumber { get; set; } + } +} + + \ No newline at end of file diff --git a/Aquiis.Professional/Features/Administration/Settings/Pages/ServiceSettings.razor b/Aquiis.Professional/Features/Administration/Settings/Pages/ServiceSettings.razor new file mode 100644 index 0000000..4aff432 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Settings/Pages/ServiceSettings.razor @@ -0,0 +1,464 @@ +@page "/administration/settings/services" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Infrastructure.Data +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@using Microsoft.EntityFrameworkCore + +@inject ApplicationDbContext DbContext +@inject PropertyManagementService PropertyService +@inject UserContextService UserContext +@inject ToastService ToastService +@inject ILogger Logger +@inject NavigationManager Navigation + +@rendermode InteractiveServer + +@attribute [OrganizationAuthorize("Owner", "Administrator")] + +Service Settings + +
+

Background Service Settings

+ +
+ +
+
+
+
+
Run Scheduled Tasks Manually
+
+
+
+ + Note: These tasks normally run automatically on a schedule. Use these buttons to run them immediately for testing or administrative purposes. +
+ +
+
+
+
+
Apply Late Fees
+ Process overdue invoices and apply late fees based on organization settings +
+ +
+ @if (taskResults.ContainsKey(TaskType.ApplyLateFees)) + { +
+ @taskResults[TaskType.ApplyLateFees] +
+ } +
+ +
+
+
+
Update Invoice Statuses
+ Mark pending invoices as overdue based on due dates +
+ +
+ @if (taskResults.ContainsKey(TaskType.UpdateInvoiceStatuses)) + { +
+ @taskResults[TaskType.UpdateInvoiceStatuses] +
+ } +
+ +
+
+
+
Send Payment Reminders
+ Mark invoices for payment reminders based on reminder settings +
+ +
+ @if (taskResults.ContainsKey(TaskType.SendPaymentReminders)) + { +
+ @taskResults[TaskType.SendPaymentReminders] +
+ } +
+ +
+
+
+
Check Lease Renewals
+ Process lease expiration notifications and update expired leases +
+ +
+ @if (taskResults.ContainsKey(TaskType.CheckLeaseRenewals)) + { +
+ @taskResults[TaskType.CheckLeaseRenewals] +
+ } +
+
+ +
+ +
+
+
+
+ +
+
+
+
Schedule Information
+
+
+
+
Scheduled Run Time
+
Daily at 2:00 AM
+ +
Next Scheduled Run
+
@GetNextRunTime()
+ +
Last Manual Run
+
@(lastRunTime?.ToString("MMM dd, yyyy h:mm tt") ?? "Never")
+
+
+
+ +
+
+
Task Details
+
+
+ +

Apply Late Fees: Applies late fees to invoices that are past the grace period based on organization-specific settings.

+

Update Invoice Statuses: Changes invoice status from "Pending" to "Overdue" for invoices past their due date.

+

Send Payment Reminders: Marks invoices for payment reminders when they're approaching their due date.

+

Check Lease Renewals: Processes lease expiration notifications at 90, 60, and 30 days, and marks expired leases.

+
+
+
+
+
+ +@code { + private bool isRunning = false; + private TaskType? runningTask = null; + private Dictionary taskResults = new(); + private DateTime? lastRunTime = null; + + private enum TaskType + { + ApplyLateFees, + UpdateInvoiceStatuses, + SendPaymentReminders, + CheckLeaseRenewals, + All + } + + private void BackToDashboard() + { + Navigation.NavigateTo("/administration/dashboard"); + } + + private async Task RunTask(TaskType taskType) + { + try + { + isRunning = true; + runningTask = taskType; + taskResults.Clear(); + + Guid? organizationId = await GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue) + { + ToastService.ShowError("Could not determine organization ID"); + return; + } + + switch (taskType) + { + case TaskType.ApplyLateFees: + await ApplyLateFees(organizationId.Value); + break; + case TaskType.UpdateInvoiceStatuses: + await UpdateInvoiceStatuses(organizationId.Value); + break; + case TaskType.SendPaymentReminders: + await SendPaymentReminders(organizationId.Value); + break; + case TaskType.CheckLeaseRenewals: + await CheckLeaseRenewals(organizationId.Value); + break; + } + + lastRunTime = DateTime.Now; + ToastService.ShowSuccess("Task completed successfully"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error running task {TaskType}", taskType); + ToastService.ShowError($"Error running task: {ex.Message}"); + } + finally + { + isRunning = false; + runningTask = null; + } + } + + private async Task RunAllTasks() + { + try + { + isRunning = true; + runningTask = TaskType.All; + taskResults.Clear(); + + var organizationId = await GetActiveOrganizationIdAsync(); + if (organizationId == null) + { + ToastService.ShowError("Could not determine organization ID"); + return; + } + + await ApplyLateFees(organizationId.Value); + await UpdateInvoiceStatuses(organizationId.Value); + await SendPaymentReminders(organizationId.Value); + await CheckLeaseRenewals(organizationId.Value); + + lastRunTime = DateTime.Now; + ToastService.ShowSuccess("All tasks completed successfully"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error running all tasks"); + ToastService.ShowError($"Error running tasks: {ex.Message}"); + } + finally + { + isRunning = false; + runningTask = null; + } + } + + private async Task GetActiveOrganizationIdAsync() + { + // Get organization ID from UserContext + return await UserContext.GetActiveOrganizationIdAsync(); + } + + private async Task ApplyLateFees(Guid organizationId) + { + var settings = await PropertyService.GetOrganizationSettingsByOrgIdAsync(organizationId); + + if (settings == null || !settings.LateFeeEnabled || !settings.LateFeeAutoApply) + { + var reason = settings == null ? "Settings not found" + : !settings.LateFeeEnabled ? "Late fees disabled" + : "Auto-apply disabled"; + taskResults[TaskType.ApplyLateFees] = $"No late fees applied: {reason} (OrgId: {organizationId})"; + return; + } + + var today = DateTime.Today; + var overdueInvoices = await DbContext.Invoices + .Include(i => i.Lease) + .Where(i => !i.IsDeleted && + i.OrganizationId == organizationId && + i.Status == "Pending" && + i.DueOn < today.AddDays(-settings.LateFeeGracePeriodDays) && + (i.LateFeeApplied == null || !i.LateFeeApplied.Value)) + .ToListAsync(); + + foreach (var invoice in overdueInvoices) + { + var lateFee = Math.Min(invoice.Amount * settings.LateFeePercentage, settings.MaxLateFeeAmount); + invoice.LateFeeAmount = lateFee; + invoice.LateFeeApplied = true; + invoice.LateFeeAppliedOn = DateTime.UtcNow; + invoice.Amount += lateFee; + invoice.Status = "Overdue"; + invoice.LastModifiedOn = DateTime.UtcNow; + invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id; + invoice.Notes = string.IsNullOrEmpty(invoice.Notes) + ? $"Late fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}" + : $"{invoice.Notes}\nLate fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}"; + } + + if (overdueInvoices.Any()) + { + await DbContext.SaveChangesAsync(); + } + + taskResults[TaskType.ApplyLateFees] = $"Applied late fees to {overdueInvoices.Count} invoice(s)"; + } + + private async Task UpdateInvoiceStatuses(Guid organizationId) + { + var today = DateTime.Today; + var newlyOverdueInvoices = await DbContext.Invoices + .Where(i => !i.IsDeleted && + i.OrganizationId == organizationId && + i.Status == "Pending" && + i.DueOn < today && + (i.LateFeeApplied == null || !i.LateFeeApplied.Value)) + .ToListAsync(); + + foreach (var invoice in newlyOverdueInvoices) + { + invoice.Status = "Overdue"; + invoice.LastModifiedOn = DateTime.UtcNow; + invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id; + } + + if (newlyOverdueInvoices.Any()) + { + await DbContext.SaveChangesAsync(); + } + + taskResults[TaskType.UpdateInvoiceStatuses] = $"Updated {newlyOverdueInvoices.Count} invoice(s) to Overdue status"; + } + + private async Task SendPaymentReminders(Guid organizationId) + { + var settings = await PropertyService.GetOrganizationSettingsByOrgIdAsync(organizationId); + if (settings == null || !settings.PaymentReminderEnabled) + { + var reason = settings == null ? "Settings not found" : "Payment reminders disabled"; + taskResults[TaskType.SendPaymentReminders] = $"No reminders sent: {reason}"; + return; + } + + var today = DateTime.Today; + var upcomingInvoices = await DbContext.Invoices + .Include(i => i.Lease) + .ThenInclude(l => l.Tenant) + .Include(i => i.Lease) + .ThenInclude(l => l.Property) + .Where(i => !i.IsDeleted && + i.OrganizationId == organizationId && + i.Status == "Pending" && + i.DueOn >= today && + i.DueOn <= today.AddDays(settings.PaymentReminderDaysBefore) && + (i.ReminderSent == null || !i.ReminderSent.Value)) + .ToListAsync(); + + foreach (var invoice in upcomingInvoices) + { + invoice.ReminderSent = true; + invoice.ReminderSentOn = DateTime.UtcNow; + invoice.LastModifiedOn = DateTime.UtcNow; + invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id; + } + + if (upcomingInvoices.Any()) + { + await DbContext.SaveChangesAsync(); + } + + taskResults[TaskType.SendPaymentReminders] = $"Marked {upcomingInvoices.Count} invoice(s) for payment reminders"; + } + + private async Task CheckLeaseRenewals(Guid organizationId) + { + var today = DateTime.Today; + int totalProcessed = 0; + + // 90-day notifications + var leasesExpiring90Days = await DbContext.Leases + .Include(l => l.Tenant) + .Include(l => l.Property) + .Where(l => !l.IsDeleted && + l.OrganizationId == organizationId && + l.Status == "Active" && + l.EndDate >= today.AddDays(85) && + l.EndDate <= today.AddDays(95) && + (l.RenewalNotificationSent == null || !l.RenewalNotificationSent.Value)) + .ToListAsync(); + + foreach (var lease in leasesExpiring90Days) + { + lease.RenewalNotificationSent = true; + lease.RenewalNotificationSentOn = DateTime.UtcNow; + lease.RenewalStatus = "Pending"; + lease.LastModifiedOn = DateTime.UtcNow; + lease.LastModifiedBy = ApplicationConstants.SystemUser.Id; + totalProcessed++; + } + + // Expired leases + var expiredLeases = await DbContext.Leases + .Where(l => !l.IsDeleted && + l.OrganizationId == organizationId && + l.Status == "Active" && + l.EndDate < today && + (l.RenewalStatus == null || l.RenewalStatus == "Pending")) + .ToListAsync(); + + foreach (var lease in expiredLeases) + { + lease.Status = "Expired"; + lease.RenewalStatus = "Expired"; + lease.LastModifiedOn = DateTime.UtcNow; + lease.LastModifiedBy = ApplicationConstants.SystemUser.Id; + totalProcessed++; + } + + if (totalProcessed > 0) + { + await DbContext.SaveChangesAsync(); + } + + taskResults[TaskType.CheckLeaseRenewals] = $"Processed {totalProcessed} lease renewal(s)"; + } + + private string GetNextRunTime() + { + var now = DateTime.Now; + var next2AM = DateTime.Today.AddDays(1).AddHours(2); + + if (now.Hour < 2) + { + next2AM = DateTime.Today.AddHours(2); + } + + return next2AM.ToString("MMM dd, yyyy h:mm tt"); + } +} diff --git a/Aquiis.Professional/Features/Administration/Users/Manage.razor b/Aquiis.Professional/Features/Administration/Users/Manage.razor new file mode 100644 index 0000000..d712bd3 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Users/Manage.razor @@ -0,0 +1,612 @@ +@page "/administration/users/manage" + +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Components.Shared +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Identity +@using Microsoft.EntityFrameworkCore +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.Professional.Shared.Authorization + +@inject UserManager UserManager +@inject UserContextService UserContext +@inject RoleManager RoleManager +@inject OrganizationService OrganizationService +@inject NavigationManager Navigation +@rendermode InteractiveServer + + + + +
+

User Management

+
+ + Add User + + +
+
+ +@if (isLoading) +{ +
+
+ Loading... +
+
+} +else +{ + +
+
+
+
+
+
+

@totalUsers

+

Total Users

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@activeUsers

+

Active Users

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@adminUsers

+

Admin Users

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@lockedUsers

+

Locked Accounts

+
+
+ +
+
+
+
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + +
+
+
User Accounts (@filteredUsers.Count users)
+
+
+ @if (filteredUsers.Any()) + { +
+ + + + + + + + + + + + + + @foreach (var userInfo in filteredUsers) + { + + + + + + + + + + } + +
UserEmailPhoneRoleStatusLast LoginActions
+
+
+ @GetUserInitials(userInfo.User.Email) +
+
+ + @(userInfo.User.UserName ?? userInfo.User.Email) + + @if (userInfo.User.EmailConfirmed) + { + + } +
+
+
@userInfo.User.Email + @if (!string.IsNullOrEmpty(userInfo.User.PhoneNumber)) + { + @userInfo.User.PhoneNumber + } + else + { + Not provided + } + + @if (userInfo.Roles.Any()) + { + @foreach (var role in userInfo.Roles) + { + @FormatRoleName(role) + } + } + else + { + No Role + } + + @if (userInfo.IsLockedOut) + { + + Locked Out + + } + else + { + + Active + + } + + @if (userInfo.User.LastLoginDate.HasValue) + { +
+ @userInfo.User.LastLoginDate.Value.ToString("MMM dd, yyyy") +
+ @userInfo.User.LastLoginDate.Value.ToString("h:mm tt") +
+ @if (userInfo.User.LoginCount > 0) + { + @userInfo.User.LoginCount logins + } + } + else + { + Never + } +
+
+ @if (userInfo.IsLockedOut) + { + + } + else + { + + } + +
+
+
+ } + else + { +
+ +

No users found

+

Try adjusting your search filters.

+
+ } +
+
+} + + +@if (showRoleModal && selectedUserForEdit != null) +{ + +} + + +
+ + @{ + Navigation.NavigateTo("/Account/Login", forceLoad: true); + } + +
+ +@code { + private bool isLoading = true; + private List allUsers = new(); + private List filteredUsers = new(); + private List availableRoles = new(); + + private string searchTerm = string.Empty; + private string selectedRole = string.Empty; + private string selectedStatus = string.Empty; + + private int totalUsers = 0; + private int activeUsers = 0; + private int lockedUsers = 0; + private int adminUsers = 0; + + private string? successMessage; + private string? errorMessage; + + // Role editing + private bool showRoleModal = false; + private UserInfo? selectedUserForEdit; + private RoleEditModel roleEditModel = new(); + + private Guid organizationId = Guid.Empty; + + private string organizationName = string.Empty; + + protected override async Task OnInitializedAsync() + { + try + { + // One line instead of 10+! + organizationId = await UserContext!.GetActiveOrganizationIdAsync() ?? Guid.Empty; + organizationName = (await UserContext!.GetOrganizationByIdAsync(organizationId))?.Name ?? string.Empty; + await LoadData(); + } + catch (InvalidOperationException) + { + // User is not authenticated or doesn't have an active organization + // The OrganizationAuthorizeView will handle the redirect + } + finally + { + isLoading = false; + } + } + + private async Task LoadData() + { + try + { + // Load all users with their roles + List? users = await UserManager.Users.Where(u => u.ActiveOrganizationId == organizationId).ToListAsync(); + allUsers.Clear(); + + foreach (var user in users) + { + // Get user's organization role from UserOrganizations table + var userOrgRole = await OrganizationService.GetUserRoleForOrganizationAsync(user.Id, organizationId); + var roles = userOrgRole != null ? new List { userOrgRole } : new List(); + + var isLockedOut = await UserManager.IsLockedOutAsync(user); + + allUsers.Add(new UserInfo + { + User = (ApplicationUser)user, + Roles = roles, + IsLockedOut = isLockedOut + }); + } + + // Load available roles from OrganizationRoles + availableRoles = ApplicationConstants.OrganizationRoles.AllRoles.ToList(); + + // Calculate statistics + CalculateStatistics(); + + // Filter users + FilterUsers(); + } + catch (Exception ex) + { + errorMessage = "Error loading user data: " + ex.Message; + } + } + + private void BackToDashboard() + { + Navigation.NavigateTo("/administration/dashboard"); + } + + private void CalculateStatistics() + { + totalUsers = allUsers.Count; + activeUsers = allUsers.Count(u => !u.IsLockedOut); + lockedUsers = allUsers.Count(u => u.IsLockedOut); + adminUsers = allUsers.Count(u => u.Roles.Contains(ApplicationConstants.OrganizationRoles.Administrator) || u.Roles.Contains(ApplicationConstants.OrganizationRoles.Owner)); + } + + private void FilterUsers() + { + filteredUsers = allUsers.Where(u => + (string.IsNullOrEmpty(searchTerm) || + u.User.Email!.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + (u.User.UserName != null && u.User.UserName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) || + (!string.IsNullOrEmpty(u.User.PhoneNumber) && u.User.PhoneNumber.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))) && + (string.IsNullOrEmpty(selectedRole) || u.Roles.Contains(selectedRole)) && + (string.IsNullOrEmpty(selectedStatus) || + (selectedStatus == "Active" && !u.IsLockedOut) || + (selectedStatus == "Locked" && u.IsLockedOut)) + ).ToList(); + } + + private async Task LockUser(ApplicationUser user) + { + try + { + var result = await UserManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)); + if (result.Succeeded) + { + successMessage = $"User {user.Email} has been locked out."; + await LoadData(); + } + else + { + errorMessage = "Failed to lock user: " + string.Join(", ", result.Errors.Select(e => e.Description)); + } + } + catch (Exception ex) + { + errorMessage = "Error locking user: " + ex.Message; + } + } + + private async Task UnlockUser(ApplicationUser user) + { + try + { + var result = await UserManager.SetLockoutEndDateAsync(user, null); + if (result.Succeeded) + { + successMessage = $"User {user.Email} has been unlocked."; + await LoadData(); + } + else + { + errorMessage = "Failed to unlock user: " + string.Join(", ", result.Errors.Select(e => e.Description)); + } + } + catch (Exception ex) + { + errorMessage = "Error unlocking user: " + ex.Message; + } + } + + private void EditUserRoles(UserInfo userInfo) + { + selectedUserForEdit = userInfo; + roleEditModel = new RoleEditModel(); + + // Set the current role (user should have exactly one role) + roleEditModel.SelectedRole = userInfo.Roles.FirstOrDefault() ?? ApplicationConstants.OrganizationRoles.User; + + showRoleModal = true; + } + + private async Task SaveUserRoles() + { + if (selectedUserForEdit == null) return; + + try + { + var user = selectedUserForEdit.User; + var newRole = roleEditModel.SelectedRole; + + // Validate role selection + if (string.IsNullOrEmpty(newRole)) + { + errorMessage = "Please select a role for the user."; + return; + } + + // Update the user's role in the organization using OrganizationService + var updateResult = await OrganizationService.UpdateUserRoleAsync(user.Id, organizationId, newRole, await UserContext.GetUserIdAsync() ?? string.Empty); + + if (updateResult) + { + successMessage = $"Role updated for {user.Email}."; + CloseRoleModal(); + await LoadData(); + } + else + { + errorMessage = "Failed to update user role."; + } + } + catch (Exception ex) + { + errorMessage = "Error updating role: " + ex.Message; + } + } + + private void CloseRoleModal() + { + showRoleModal = false; + selectedUserForEdit = null; + roleEditModel = new(); + } + + private void ClearFilters() + { + searchTerm = string.Empty; + selectedRole = string.Empty; + selectedStatus = string.Empty; + FilterUsers(); + } + + private string GetUserInitials(string? email) + { + if (string.IsNullOrEmpty(email)) return "?"; + var parts = email.Split('@')[0].Split('.'); + if (parts.Length >= 2) + return (parts[0][0].ToString() + parts[1][0].ToString()).ToUpper(); + return email[0].ToString().ToUpper(); + } + + private string GetRoleBadgeClass(string role) + { + return role switch + { + ApplicationConstants.OrganizationRoles.Owner => "bg-primary", + ApplicationConstants.OrganizationRoles.Administrator => "bg-info", + ApplicationConstants.OrganizationRoles.PropertyManager => "bg-success", + ApplicationConstants.OrganizationRoles.User => "bg-secondary", + _ => "bg-secondary" + }; + } + + private string FormatRoleName(string role) + { + // Add spaces before capital letters (e.g., PropertyManager -> Property Manager) + return System.Text.RegularExpressions.Regex.Replace(role, "([a-z])([A-Z])", "$1 $2"); + } + + private class UserInfo + { + public ApplicationUser User { get; set; } = default!; + public List Roles { get; set; } = new(); + public bool IsLockedOut { get; set; } + } + + private class RoleEditModel + { + public string SelectedRole { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/Administration/Users/Pages/Create.razor b/Aquiis.Professional/Features/Administration/Users/Pages/Create.razor new file mode 100644 index 0000000..d8bed9d --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Users/Pages/Create.razor @@ -0,0 +1,311 @@ +@page "/Administration/Users/Create" + +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Identity +@inject NavigationManager Navigation +@inject UserManager UserManager +@inject RoleManager RoleManager +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject AuthenticationStateProvider AuthenticationStateProvider +@rendermode InteractiveServer + +@attribute [OrganizationAuthorize("Owner", "Administrator")] + +Create User - Administration + +
+

Create User

+ +
+ +
+
+
+
+
New User Account
+
+
+ + + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + + This will be used as the username +
+
+ + + +
+
+ +
+
+ + + + Min 6 characters, 1 uppercase, 1 lowercase, 1 digit +
+
+ + + +
+
+ +
+
+ + +
+
+ +
+ + @foreach (var role in ApplicationConstants.OrganizationRoles.AllRoles) + { +
+ + +
+ } + @if (string.IsNullOrEmpty(userModel.SelectedRole)) + { + Role selection is required + } +
+ +
+ + +
+
+
+
+
+ +
+
+
+
Password Requirements
+
+
+
    +
  • Minimum 6 characters
  • +
  • At least 1 uppercase letter
  • +
  • At least 1 lowercase letter
  • +
  • At least 1 digit (0-9)
  • +
+
+
+ +
+
+
Organization Roles
+
+
+
    +
  • Owner: Full control including organization management
  • +
  • Administrator: Manage users and all features except organization settings
  • +
  • Property Manager: Manage properties, tenants, and leases
  • +
  • User: Limited access to view data
  • +
+
+
+
+
+ +@code { + private UserModel userModel = new UserModel(); + private bool isSubmitting = false; + private string errorMessage = string.Empty; + private string successMessage = string.Empty; + + protected override async Task OnInitializedAsync() + { + await Task.CompletedTask; + } + + private void OnRoleChanged(string roleName) + { + userModel.SelectedRole = roleName; + } + + private async Task CreateUser() + { + try + { + isSubmitting = true; + errorMessage = string.Empty; + successMessage = string.Empty; + + // Validate role - user must have exactly one role + if (string.IsNullOrEmpty(userModel.SelectedRole)) + { + errorMessage = "Please select a role for the user."; + return; + } + + // Get current user's context + var currentUserId = await UserContext.GetUserIdAsync(); + var currentOrganizationId = await UserContext.GetActiveOrganizationIdAsync(); + + if (string.IsNullOrEmpty(currentUserId) || !currentOrganizationId.HasValue) + { + errorMessage = "User not authenticated or no active organization."; + return; + } + + // Check if user already exists + var existingUser = await UserManager.FindByEmailAsync(userModel.Email); + if (existingUser != null) + { + errorMessage = "A user with this email already exists."; + return; + } + + // Create new user + var newUser = new ApplicationUser + { + UserName = userModel.Email, + Email = userModel.Email, + EmailConfirmed = userModel.EmailConfirmed, + PhoneNumber = userModel.PhoneNumber, + FirstName = userModel.FirstName, + LastName = userModel.LastName, + OrganizationId = currentOrganizationId.Value, + ActiveOrganizationId = currentOrganizationId.Value + }; + + var createResult = await UserManager.CreateAsync(newUser, userModel.Password); + + if (!createResult.Succeeded) + { + errorMessage = $"Error creating user: {string.Join(", ", createResult.Errors.Select(e => e.Description))}"; + return; + } + + // Grant organization access with the selected role + var grantResult = await OrganizationService.GrantOrganizationAccessAsync( + newUser.Id, + currentOrganizationId.Value, + userModel.SelectedRole, + currentUserId); + + if (!grantResult) + { + errorMessage = "User created but failed to assign organization role."; + // Consider whether to delete the user here or leave it + return; + } + + successMessage = $"User account created successfully! Username: {userModel.Email}, Role: {userModel.SelectedRole}"; + + // Reset form + userModel = new UserModel(); + + // Redirect after a brief delay + await Task.Delay(2000); + Navigation.NavigateTo("/administration/users/manage"); + } + catch (Exception ex) + { + errorMessage = $"Error creating user: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private void Cancel() + { + Navigation.NavigateTo("/administration/users/manage"); + } + + private string FormatRoleName(string role) + { + // Add spaces before capital letters (e.g., PropertyManager -> Property Manager) + return System.Text.RegularExpressions.Regex.Replace(role, "([a-z])([A-Z])", "$1 $2"); + } + + public class UserModel + { + [Required(ErrorMessage = "First name is required")] + [StringLength(100, ErrorMessage = "First name cannot exceed 100 characters")] + public string FirstName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Last name is required")] + [StringLength(100, ErrorMessage = "Last name cannot exceed 100 characters")] + public string LastName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Please enter a valid email address")] + public string Email { get; set; } = string.Empty; + + [Phone(ErrorMessage = "Please enter a valid phone number")] + public string PhoneNumber { get; set; } = string.Empty; + + [Required(ErrorMessage = "Password is required")] + [StringLength(100, MinimumLength = 6, ErrorMessage = "Password must be at least 6 characters")] + public string Password { get; set; } = string.Empty; + + [Required(ErrorMessage = "Please confirm the password")] + [Compare(nameof(Password), ErrorMessage = "Passwords do not match")] + public string ConfirmPassword { get; set; } = string.Empty; + + public bool EmailConfirmed { get; set; } = true; + + public string SelectedRole { get; set; } = ApplicationConstants.OrganizationRoles.User; + } +} diff --git a/Aquiis.Professional/Features/Administration/Users/View.razor b/Aquiis.Professional/Features/Administration/Users/View.razor new file mode 100644 index 0000000..3d6e005 --- /dev/null +++ b/Aquiis.Professional/Features/Administration/Users/View.razor @@ -0,0 +1,507 @@ +@page "/administration/users/view/{UserId}" + +@using Aquiis.Professional.Shared.Components.Shared +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.EntityFrameworkCore +@using System.ComponentModel.DataAnnotations +@using Aquiis.Professional.Shared.Components.Account + +@rendermode InteractiveServer +@inject UserManager UserManager +@inject RoleManager RoleManager +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject NavigationManager NavigationManager + + +

View User Details

+ + @if (isLoading) +{ +
+
+ Loading... +
+
+} +else if (viewedUser == null) +{ +
+

User Not Found

+

The requested user could not be found.

+ Back to User Management +
+} +else if (!canViewUser) +{ +
+

Access Denied

+

You don't have permission to view this user's account.

+ Back to User Management +
+} +else +{ +
+
+

+
+
+ @GetUserInitials(viewedUser.Email) +
+
+ @(viewedUser.UserName ?? viewedUser.Email) + @if (viewedUser.EmailConfirmed) + { + + } +
+
+

+ @if (isViewingOwnAccount) + { +

Your Account

+ } + else if (isCurrentUserAdmin) + { +

User Account (Admin View)

+ } +
+
+ @if (isCurrentUserAdmin) + { + + Back to Users + + } + @if (isViewingOwnAccount) + { + + Edit Account + + } +
+
+ +
+
+
+
+
Account Information
+
+
+ @if (!string.IsNullOrEmpty(successMessage)) + { + + } + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + + + +
+ + +
+ +
+ + +
+ +
+ @if (canEditAccount) + { + + + + } + else + { + + + } +
+ +
+ @if (isCurrentUserAdmin && !isViewingOwnAccount) + { + + + } + else + { + + + } +
+ + @if (canEditAccount) + { +
+ +
+ } +
+
+
+
+ +
+
+
+
Account Status
+
+
+
+ +
+ @if (viewedUser.EmailConfirmed) + { + + Confirmed + + } + else + { + + Not Confirmed + + } +
+
+ +
+ +
+ @if (isLockedOut) + { + + Locked Out + + @if (viewedUser.LockoutEnd.HasValue) + { + + Until: @viewedUser.LockoutEnd.Value.ToString("MMM dd, yyyy HH:mm") + + } + } + else + { + + Active + + } +
+
+ +
+ +
+ @if (viewedUser.TwoFactorEnabled) + { + + Enabled + + } + else + { + + Disabled + + } +
+
+ + @if (isCurrentUserAdmin && !isViewingOwnAccount) + { +
+
Admin Actions
+
+ @if (isLockedOut) + { + + } + else + { + + } +
+ } +
+
+
+
+} + + +
+ +@code { + [Parameter] public string UserId { get; set; } = string.Empty; + + private ApplicationUser? viewedUser; + private ApplicationUser? currentUser; + private bool isLoading = true; + private bool canViewUser = false; + private bool canEditAccount = false; + private bool isViewingOwnAccount = false; + private bool isCurrentUserAdmin = false; + private bool isLockedOut = false; + + private List userRoles = new(); + private List availableRoles = new(); + private string currentUserRole = "User"; + private string selectedRole = string.Empty; + + private string? successMessage; + private string? errorMessage; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + await LoadUserData(); + isLoading = false; + } + + private async Task LoadUserData() + { + try + { + var authState = await AuthenticationStateTask; + currentUser = await UserManager.GetUserAsync(authState.User); + + if (currentUser == null) + { + NavigationManager.NavigateTo("/Account/Login"); + return; + } + + // Load the viewed user + viewedUser = await UserManager.FindByIdAsync(UserId); + if (viewedUser == null) return; + + // Check permissions + var currentOrgId = await UserContext.GetActiveOrganizationIdAsync(); + var currentUserRole = await OrganizationService.GetUserRoleForOrganizationAsync(currentUser.Id, currentOrgId.Value); + isCurrentUserAdmin = currentUserRole == ApplicationConstants.OrganizationRoles.Owner || + currentUserRole == ApplicationConstants.OrganizationRoles.Administrator; + isViewingOwnAccount = currentUser.Id == viewedUser.Id; + + // Users can view their own account, admins can view any account in the same org + canViewUser = isViewingOwnAccount || isCurrentUserAdmin; + if (!canViewUser) return; + + // Only allow editing own account + canEditAccount = isViewingOwnAccount; + + // Load user's organization role + var viewedUserRole = await OrganizationService.GetUserRoleForOrganizationAsync(viewedUser.Id, currentOrgId.Value); + userRoles = viewedUserRole != null ? new List { viewedUserRole } : new List(); + currentUserRole = viewedUserRole ?? "No Role"; + selectedRole = currentUserRole; + isLockedOut = await UserManager.IsLockedOutAsync(viewedUser); + + // Load available roles for admins + if (isCurrentUserAdmin) + { + availableRoles = ApplicationConstants.OrganizationRoles.AllRoles.ToList(); + } + + // Initialize form + Input.PhoneNumber = viewedUser.PhoneNumber ?? string.Empty; + } + catch (Exception ex) + { + errorMessage = "Error loading user data: " + ex.Message; + } + } + + private async Task OnValidSubmitAsync() + { + if (!canEditAccount || viewedUser == null) return; + + try + { + if (Input.PhoneNumber != viewedUser.PhoneNumber) + { + var result = await UserManager.SetPhoneNumberAsync(viewedUser, Input.PhoneNumber); + if (!result.Succeeded) + { + errorMessage = "Failed to update phone number: " + string.Join(", ", result.Errors.Select(e => e.Description)); + return; + } + } + + successMessage = "Account updated successfully."; + errorMessage = null; + } + catch (Exception ex) + { + errorMessage = "Error updating account: " + ex.Message; + } + } + + private async Task UpdateUserRole() + { + if (!isCurrentUserAdmin || isViewingOwnAccount || viewedUser == null || string.IsNullOrEmpty(selectedRole)) return; + + try + { + var currentOrgId = await UserContext.GetActiveOrganizationIdAsync(); + var currentUserId = await UserContext.GetUserIdAsync(); + + if (!currentOrgId.HasValue || string.IsNullOrEmpty(currentUserId)) + { + errorMessage = "Unable to determine current organization context."; + return; + } + + // Check if user already has an organization assignment + var existingRole = await OrganizationService.GetUserRoleForOrganizationAsync(viewedUser.Id, currentOrgId.Value); + + bool updateResult; + if (existingRole != null) + { + // Update existing role + updateResult = await OrganizationService.UpdateUserRoleAsync( + viewedUser.Id, + currentOrgId.Value, + selectedRole, + currentUserId); + } + else + { + // Grant new organization access with the selected role + updateResult = await OrganizationService.GrantOrganizationAccessAsync( + viewedUser.Id, + currentOrgId.Value, + selectedRole, + currentUserId); + } + + if (updateResult) + { + userRoles = new List { selectedRole }; + currentUserRole = selectedRole; + successMessage = $"Role updated to {selectedRole}."; + errorMessage = null; + } + else + { + errorMessage = "Failed to update user role. The user may not have access to this organization."; + } + } + catch (Exception ex) + { + errorMessage = "Error updating role: " + ex.Message; + } + } + + private async Task LockUser() + { + if (!isCurrentUserAdmin || isViewingOwnAccount || viewedUser == null) return; + + try + { + var result = await UserManager.SetLockoutEndDateAsync(viewedUser, DateTimeOffset.UtcNow.AddYears(100)); + if (result.Succeeded) + { + isLockedOut = true; + successMessage = "User account has been locked."; + errorMessage = null; + } + else + { + errorMessage = "Failed to lock user account."; + } + } + catch (Exception ex) + { + errorMessage = "Error locking user: " + ex.Message; + } + } + + private async Task UnlockUser() + { + if (!isCurrentUserAdmin || isViewingOwnAccount || viewedUser == null) return; + + try + { + var result = await UserManager.SetLockoutEndDateAsync(viewedUser, null); + if (result.Succeeded) + { + isLockedOut = false; + successMessage = "User account has been unlocked."; + errorMessage = null; + } + else + { + errorMessage = "Failed to unlock user account."; + } + } + catch (Exception ex) + { + errorMessage = "Error unlocking user: " + ex.Message; + } + } + + private string GetUserInitials(string? email) + { + if (string.IsNullOrEmpty(email)) return "?"; + var parts = email.Split('@')[0].Split('.'); + if (parts.Length >= 2) + return (parts[0][0].ToString() + parts[1][0].ToString()).ToUpper(); + return email[0].ToString().ToUpper(); + } + + private sealed class InputModel + { + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/Notifications/Pages/NotificationCenter.razor b/Aquiis.Professional/Features/Notifications/Pages/NotificationCenter.razor new file mode 100644 index 0000000..4d6c8f3 --- /dev/null +++ b/Aquiis.Professional/Features/Notifications/Pages/NotificationCenter.razor @@ -0,0 +1,653 @@ +@page "/notifications" +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Infrastructure.Services +@inject NotificationService NotificationService +@inject NavigationManager NavigationManager +@rendermode InteractiveServer +@attribute [OrganizationAuthorize] +@namespace Aquiis.Professional.Features.Notifications.Pages + +Notification Center + +
+
+

+ Notification Center +

+

+ Here you can manage your notifications. +

+
+
+ + +
+
+ + +
+
+
+ +
+
+ + + + + @if (!string.IsNullOrEmpty(searchText)) + { + + } +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + @if (HasActiveFilters()) + { +
+ + + Showing @filteredNotifications.Count of @notifications.Count notifications + +
+ } +
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + @foreach (var notification in pagedNotifications) + { + + @if(!notification.IsRead){ + + } else { + + } + + + + + + } + +
+ Title + @if (sortColumn == nameof(Notification.Title)) + { + + } + + Category + @if (sortColumn == nameof(Notification.Category)) + { + + } + + Message + @if (sortColumn == nameof(Notification.Message)) + { + + } + + Date + @if (sortColumn == nameof(Notification.CreatedOn)) + { + + } + Actions
+ @notification.Title + + @notification.Title + @notification.Category@notification.Message@notification.CreatedOn.ToString("g") +
+ + + +
+
+
+
+ @if (totalPages > 1) + { + + } +
+
+ +@* Message Detail Modal *@ +@if (showMessageModal && selectedNotification != null) +{ + +} + +@code { + private List notifications = new List(); + private List filteredNotifications = new List(); + private List sortedNotifications = new List(); + private List pagedNotifications = new List(); + + private Notification? selectedNotification; + private bool showMessageModal = false; + + private string sortColumn = nameof(Notification.CreatedOn); + private bool sortAscending = false; + + // Filter and search properties + private string searchText = ""; + private string filterCategory = ""; + private string filterType = ""; + private string filterStatus = ""; + + private int currentPage = 1; + private int pageSize = 25; + private int totalPages = 1; + private int totalRecords = 0; + + protected override async Task OnInitializedAsync() + { + await LoadNotificationsAsync(); + } + + private async Task LoadNotificationsAsync() + { + // Simulate loading notifications + await Task.Delay(1000); + notifications = await NotificationService.GetUnreadNotificationsAsync(); + + notifications = new List{ + new Notification { Id= Guid.NewGuid(), Title = "New message from John", Category = "Messages", Type = "Info", Message = "Hey, can we meet tomorrow?", CreatedOn = DateTime.Now, IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "Your report is ready", Category = "Reports", Type = "Success", Message = "Your monthly report is now available.", CreatedOn = DateTime.Now.AddDays(-1), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "System maintenance scheduled", Category = "System", Type = "Warning", Message = "System maintenance is scheduled for tonight at 11 PM.", CreatedOn = DateTime.Now.AddDays(-5), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "New comment on your post", Category = "Comments", Type = "Info", Message = "Alice commented on your post.", CreatedOn = DateTime.Now.AddDays(-2), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "Password will expire soon", Category = "Security", Type = "Warning", Message = "Your password will expire in 3 days.", CreatedOn = DateTime.Now.AddDays(-3), IsRead = false } + }; + + filteredNotifications = notifications; + SortAndPaginateNotifications(); + } + + private void SortTable(string column) + { + if (sortColumn == column) + { + sortAscending = !sortAscending; + } + else + { + sortColumn = column; + sortAscending = true; + } + SortAndPaginateNotifications(); + } + + private void ApplyFilters() + { + filteredNotifications = notifications.Where(n => + { + // Search filter + if (!string.IsNullOrEmpty(searchText)) + { + var search = searchText.ToLower(); + if (!n.Title.ToLower().Contains(search) && + !n.Message.ToLower().Contains(search)) + { + return false; + } + } + + // Category filter + if (!string.IsNullOrEmpty(filterCategory) && n.Category != filterCategory) + { + return false; + } + + // Type filter + if (!string.IsNullOrEmpty(filterType) && n.Type != filterType) + { + return false; + } + + // Status filter + if (!string.IsNullOrEmpty(filterStatus)) + { + if (filterStatus == "read" && !n.IsRead) + return false; + if (filterStatus == "unread" && n.IsRead) + return false; + } + + return true; + }).ToList(); + + currentPage = 1; + SortAndPaginateNotifications(); + } + + private void ClearSearch() + { + searchText = ""; + ApplyFilters(); + } + + private void ClearAllFilters() + { + searchText = ""; + filterCategory = ""; + filterType = ""; + filterStatus = ""; + ApplyFilters(); + } + + private bool HasActiveFilters() + { + return !string.IsNullOrEmpty(searchText) || + !string.IsNullOrEmpty(filterCategory) || + !string.IsNullOrEmpty(filterType) || + !string.IsNullOrEmpty(filterStatus); + } + + private async Task MarkAllAsRead() + { + foreach (var notification in notifications.Where(n => !n.IsRead)) + { + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + } + await Task.CompletedTask; + SortAndPaginateNotifications(); + } + + private void SortAndPaginateNotifications() + { + // Use filtered notifications if filters are active + var sourceList = HasActiveFilters() ? filteredNotifications : notifications; + + // Sort + sortedNotifications = sortColumn switch + { + nameof(Notification.Title) => sortAscending + ? sourceList.OrderBy(n => n.Title).ToList() + : sourceList.OrderByDescending(n => n.Title).ToList(), + nameof(Notification.Category) => sortAscending + ? sourceList.OrderBy(n => n.Category).ToList() + : sourceList.OrderByDescending(n => n.Category).ToList(), + nameof(Notification.Message) => sortAscending + ? sourceList.OrderBy(n => n.Message).ToList() + : sourceList.OrderByDescending(n => n.Message).ToList(), + nameof(Notification.CreatedOn) => sortAscending + ? sourceList.OrderBy(n => n.CreatedOn).ToList() + : sourceList.OrderByDescending(n => n.CreatedOn).ToList(), + _ => sourceList.OrderByDescending(n => n.CreatedOn).ToList() + }; + + // Paginate + totalRecords = sortedNotifications.Count; + totalPages = (int)Math.Ceiling(totalRecords / (double)pageSize); + currentPage = Math.Max(1, Math.Min(currentPage, totalPages)); + + pagedNotifications = sortedNotifications + .Skip((currentPage - 1) * pageSize) + .Take(pageSize) + .ToList(); + } + + private void UpdatePagination() + { + currentPage = 1; + SortAndPaginateNotifications(); + } + + private void FirstPage() => GoToPage(1); + private void LastPage() => GoToPage(totalPages); + private void NextPage() => GoToPage(currentPage + 1); + private void PreviousPage() => GoToPage(currentPage - 1); + + private void GoToPage(int page) + { + currentPage = Math.Max(1, Math.Min(page, totalPages)); + SortAndPaginateNotifications(); + } + + private void ViewNotification(Guid id){ + var notification = notifications.FirstOrDefault(n => n.Id == id); + if (notification != null) + { + // Implement the logic to view the notification details + } + } + + private void ToggleReadStatus(Guid id){ + var notification = notifications.FirstOrDefault(n => n.Id == id); + if (notification != null) + { + notification.IsRead = !notification.IsRead; + SortAndPaginateNotifications(); + } + } + + private void DeleteNotification(Guid id) + { + var notification = notifications.FirstOrDefault(n => n.Id == id); + if (notification != null) + { + notifications.Remove(notification); + SortAndPaginateNotifications(); + } + } + + private void BackToDashboard() + { + NavigationManager.NavigateTo("/"); + } + + private void GoToPreferences() + { + NavigationManager.NavigateTo("/notifications/preferences"); + } + + // Modal Methods + private void OpenMessageModal(Guid id) + { + selectedNotification = notifications.FirstOrDefault(n => n.Id == id); + if (selectedNotification != null) + { + // Mark as read when opened + if (!selectedNotification.IsRead) + { + selectedNotification.IsRead = true; + selectedNotification.ReadOn = DateTime.UtcNow; + } + showMessageModal = true; + } + } + + private void CloseMessageModal() + { + showMessageModal = false; + selectedNotification = null; + SortAndPaginateNotifications(); + } + + private void DeleteCurrentNotification() + { + if (selectedNotification != null) + { + notifications.Remove(selectedNotification); + CloseMessageModal(); + } + } + + private void ViewRelatedEntity() + { + if (selectedNotification?.RelatedEntityId.HasValue == true) + { + var route = EntityRouteHelper.GetEntityRoute( + selectedNotification.RelatedEntityType, + selectedNotification.RelatedEntityId.Value); + NavigationManager.NavigateTo(route); + } + } + + // TODO: Implement when SenderId is added to Notification entity + // private void ReplyToMessage() + // { + // // Create new notification to sender + // } + + // TODO: Implement when SenderId is added to Notification entity + // private void ForwardMessage() + // { + // // Show user selection modal, then send to selected users + // } + + // Helper methods for badge colors + private string GetCategoryBadgeColor(string category) => category switch + { + "Lease" => "primary", + "Payment" => "success", + "Maintenance" => "warning", + "Application" => "info", + "Security" => "danger", + _ => "secondary" + }; + + private string GetTypeBadgeColor(string type) => type switch + { + "Info" => "info", + "Warning" => "warning", + "Error" => "danger", + "Success" => "success", + _ => "secondary" + }; +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/Notifications/Pages/NotificationPreferences.razor b/Aquiis.Professional/Features/Notifications/Pages/NotificationPreferences.razor new file mode 100644 index 0000000..d32d593 --- /dev/null +++ b/Aquiis.Professional/Features/Notifications/Pages/NotificationPreferences.razor @@ -0,0 +1,404 @@ +@page "/notifications/preferences" +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Microsoft.JSInterop +@using PreferencesEntity = Aquiis.Professional.Core.Entities.NotificationPreferences +@inject NotificationService NotificationService +@inject ToastService ToastService +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@attribute [OrganizationAuthorize] +@rendermode InteractiveServer + +Notification Preferences - Aquiis + +
+ +
+
+

+ Notification Preferences +

+

Configure how you receive notifications

+
+ +
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (preferences != null) + { + + + + +
+
+
+ In-App Notifications +
+
+
+
+ + +
+
+ + In-app notifications appear in the notification bell at the top of the page. They are always stored in your notification history. +
+
+
+ + +
+
+
+ Email Notifications +
+
+
+
+ + +
+ + @if (preferences.EnableEmailNotifications) + { +
+ + + + Notifications will be sent to this email address +
+ +
+ +
Email Categories
+

Choose which types of notifications you want to receive via email

+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ } +
+
+ + +
+
+
+ SMS Notifications +
+
+
+
+ + +
+ + @if (preferences.EnableSMSNotifications) + { +
+ + + + Include country code (e.g., +1 for US) +
+ +
+ + Note: SMS notifications are for urgent matters only to minimize costs and avoid message fatigue. +
+ +
+ +
SMS Categories
+

Choose which urgent notifications you want to receive via SMS

+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ } +
+
+ + +
+
+
+ Digest Preferences +
+
+
+

+ + Digests consolidate multiple notifications into a single summary email, helping reduce email volume. +

+ +
+
+
Daily Digest
+
+ + +
+ + @if (preferences.EnableDailyDigest) + { +
+ + + Time of day to receive the digest +
+ } +
+ +
+
Weekly Digest
+
+ + +
+ + @if (preferences.EnableWeeklyDigest) + { +
+ + + + + + + + + + + Day of the week to receive the digest +
+ } +
+
+
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+ } +
+ +@code { + private PreferencesEntity? preferences { get; set; } + + private bool isLoading = true; + private bool isSaving = false; + + // Helper property for time binding (InputDate doesn't bind TimeSpan directly) + private DateTime DailyDigestTimeValue + { + get => DateTime.Today.Add(preferences?.DailyDigestTime ?? new TimeSpan(9, 0, 0)); + set => preferences!.DailyDigestTime = value.TimeOfDay; + } + + protected override async Task OnInitializedAsync() + { + try + { + preferences = await NotificationService.GetUserPreferencesAsync(); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to load preferences: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task SavePreferences() + { + if (preferences == null) return; + + isSaving = true; + StateHasChanged(); + + try + { + Console.WriteLine($"Saving preferences - EnableInApp: {preferences.EnableInAppNotifications}, EnableEmail: {preferences.EnableEmailNotifications}"); + await NotificationService.UpdateUserPreferencesAsync(preferences); + Console.WriteLine("Preferences saved successfully"); + ToastService.ShowSuccess("Notification preferences saved successfully!"); + } + catch (Exception ex) + { + Console.WriteLine($"Error saving preferences: {ex.Message}"); + ToastService.ShowError($"Failed to save preferences: {ex.Message}"); + } + finally + { + isSaving = false; + StateHasChanged(); + } + } + + private async Task ResetToDefaults() + { + if (preferences == null) return; + + var confirmed = await JSRuntime.InvokeAsync("confirm", "Are you sure you want to reset all preferences to defaults? This cannot be undone."); + if (!confirmed) return; + + // Reset to defaults + preferences.EnableInAppNotifications = true; + preferences.EnableEmailNotifications = true; + preferences.EnableSMSNotifications = false; + preferences.EmailLeaseExpiring = true; + preferences.EmailPaymentDue = true; + preferences.EmailPaymentReceived = true; + preferences.EmailApplicationStatusChange = true; + preferences.EmailMaintenanceUpdate = true; + preferences.EmailInspectionScheduled = true; + preferences.SMSPaymentDue = false; + preferences.SMSMaintenanceEmergency = true; + preferences.SMSLeaseExpiringUrgent = false; + preferences.EnableDailyDigest = false; + preferences.DailyDigestTime = new TimeSpan(9, 0, 0); + preferences.EnableWeeklyDigest = false; + preferences.WeeklyDigestDay = DayOfWeek.Monday; + + await SavePreferences(); + } + + private void Cancel() + { + Navigation.NavigateTo("/notifications"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Applications.razor b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Applications.razor new file mode 100644 index 0000000..920bc6d --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Applications.razor @@ -0,0 +1,250 @@ +@page "/propertymanagement/applications" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization + +@inject NavigationManager Navigation +@inject UserContextService UserContext +@inject RentalApplicationService RentalApplicationService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Rental Applications + +
+
+
+
+

Rental Applications

+
+
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else + { + + + + @if (!filteredApplications.Any()) + { +
+ + No applications found. +
+ } + else + { +
+ + + + + + + + + + + + + + + + @foreach (var app in filteredApplications.OrderByDescending(a => a.AppliedOn)) + { + var rentRatio = app.Property != null ? (app.Property.MonthlyRent / app.MonthlyIncome * 100) : 0; + var daysUntilExpiration = (app.ExpiresOn - DateTime.UtcNow)?.TotalDays ?? 0; + + + + + + + + + + + + + } + +
ApplicantPropertyApplied OnMonthly IncomeRent RatioStatusFee PaidExpiresActions
+ @app.ProspectiveTenant?.FullName
+ @app.ProspectiveTenant?.Email +
+ @app.Property?.Address
+ @app.Property?.MonthlyRent.ToString("C")/mo +
+ @app.AppliedOn.ToString("MMM dd, yyyy") + + @app.MonthlyIncome.ToString("C") + + + @rentRatio.ToString("F1")% + + + @if (app.Status == ApplicationConstants.ApplicationStatuses.Submitted) + { + @app.Status + } + else if (app.Status == ApplicationConstants.ApplicationStatuses.UnderReview || app.Status == ApplicationConstants.ApplicationStatuses.Screening) + { + @app.Status + } + else if (app.Status == ApplicationConstants.ApplicationStatuses.Approved) + { + @app.Status + } + else if (app.Status == ApplicationConstants.ApplicationStatuses.Denied) + { + @app.Status + } + else if (app.Status == ApplicationConstants.ApplicationStatuses.Expired) + { + @app.Status + } + else + { + @app.Status + } + + @if (app.ApplicationFeePaid) + { + + } + else + { + + } + + @if (app.ExpiresOn < DateTime.UtcNow) + { + Expired + } + else if (daysUntilExpiration < 7) + { + @((int)daysUntilExpiration)d + } + else + { + @app.ExpiresOn?.ToString("MMM dd") + } + + +
+
+ } + } +
+ +@code { + private List applications = new(); + private List filteredApplications = new(); + private List pendingApplications = new(); + private List screeningApplications = new(); + private List approvedApplications = new(); + private List deniedApplications = new(); + private string currentFilter = "Pending"; + private bool isLoading = true; + private Guid organizationId = Guid.Empty; + + protected override async Task OnInitializedAsync() + { + try + { + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + await LoadApplications(); + } + catch (Exception ex) + { + Console.WriteLine($"Error loading applications: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task LoadApplications() + { + applications = await RentalApplicationService.GetAllAsync(); + + pendingApplications = applications.Where(a => + a.Status == ApplicationConstants.ApplicationStatuses.Submitted || + a.Status == ApplicationConstants.ApplicationStatuses.UnderReview).ToList(); + + screeningApplications = applications.Where(a => + a.Status == ApplicationConstants.ApplicationStatuses.Screening).ToList(); + + approvedApplications = applications.Where(a => + a.Status == ApplicationConstants.ApplicationStatuses.Approved || + a.Status == ApplicationConstants.ApplicationStatuses.LeaseOffered || + a.Status == ApplicationConstants.ApplicationStatuses.LeaseAccepted).ToList(); + + deniedApplications = applications.Where(a => + a.Status == ApplicationConstants.ApplicationStatuses.Denied || + a.Status == ApplicationConstants.ApplicationStatuses.Withdrawn).ToList(); + + SetFilter(currentFilter); + } + + private void SetFilter(string filter) + { + currentFilter = filter; + + filteredApplications = filter switch + { + "Pending" => pendingApplications, + "Screening" => screeningApplications, + "Approved" => approvedApplications, + "Denied" => deniedApplications, + _ => applications + }; + } + + private void ViewApplication(Guid applicationId) + { + Navigation.NavigateTo($"/propertymanagement/applications/{applicationId}/review"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor new file mode 100644 index 0000000..ae18d26 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor @@ -0,0 +1,449 @@ +@page "/propertymanagement/applications/{ApplicationId:guid}/generate-lease-offer" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Application.Services.Workflows +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations + +@inject RentalApplicationService RentalApplicationService +@inject ApplicationWorkflowService WorkflowService +@inject NavigationManager Navigation +@inject AuthenticationStateProvider AuthStateProvider +@inject UserContextService UserContext +@inject ToastService ToastService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Generate Lease Offer + +
+
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (application == null) + { +
+

Application Not Found

+

The application you are trying to view does not exist or you do not have permission to access it.

+
+ Return to Applications +
+ } + else + { +
+
+
+
+

Generate Lease Offer

+
+
+ @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + + + +
+
Application Details
+
+
+ Applicant: @application.ProspectiveTenant?.FullName +
+
+ Property: @application.Property?.Address +
+
+ Applied On: @application.AppliedOn.ToString("MMM dd, yyyy") +
+
+ Status: + @application.Status +
+
+
+ +
+
Lease Terms
+ +
+
+ + + +
+
+ + + + Duration: @CalculateDuration() months +
+
+ +
+
+ +
+ $ + +
+ +
+
+ +
+ $ + +
+ +
+
+ +
+
+ + + +
+
+ +
+
+ + +
+
+
+ +
+ + Note: This lease offer will expire in 30 days. The prospective tenant must accept before @DateTime.UtcNow.AddDays(30).ToString("MMM dd, yyyy"). +
+ +
+ + +
+
+
+
+
+ +
+
+
+
Workflow Status
+
+
+
+
+ + Application Submitted +
+
+ + Application Approved +
+
+ + Generating Lease Offer +
+
+ + Awaiting Acceptance +
+
+ + Lease Signed +
+
+
+
+ + @if (application.Property != null) + { +
+
+
Property Info
+
+
+

Address:
@application.Property.Address

+

Type: @application.Property.PropertyType

+

Beds/Baths: @application.Property.Bedrooms / @application.Property.Bathrooms

+

Current Rent: @application.Property.MonthlyRent.ToString("C")

+
+
+ } +
+
+ } +
+ + + +@code { + [Parameter] + public Guid ApplicationId { get; set; } + + private RentalApplication? application; + private LeaseOfferModel leaseModel = new(); + private bool isLoading = true; + private bool isSubmitting = false; + private string errorMessage = string.Empty; + private string userId = string.Empty; + private Guid organizationId = Guid.Empty; + + protected override async Task OnInitializedAsync() + { + try + { + userId = await UserContext.GetUserIdAsync() ?? string.Empty; + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + + await LoadApplication(); + } + catch (Exception ex) + { + errorMessage = $"Error loading application: {ex.Message}"; + } + finally + { + isLoading = false; + } + } + + private async Task LoadApplication() + { + application = await RentalApplicationService.GetRentalApplicationWithRelationsAsync(ApplicationId); + + if (application != null) + { + // Verify application is approved + if (application.Status != ApplicationConstants.ApplicationStatuses.Approved) + { + errorMessage = "Only approved applications can generate lease offers."; + return; + } + + // Pre-fill lease data from property and application + leaseModel.StartDate = DateTime.Today.AddDays(14); // Default 2 weeks from now + leaseModel.EndDate = leaseModel.StartDate.AddYears(1); // Default 1 year lease + leaseModel.MonthlyRent = application.Property?.MonthlyRent ?? 0; + leaseModel.SecurityDeposit = application.Property?.MonthlyRent ?? 0; // Default to 1 month rent + leaseModel.Terms = GetDefaultLeaseTerms(); + } + } + + private async Task HandleGenerateLeaseOffer() + { + if (application == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + // Validate dates + if (leaseModel.EndDate <= leaseModel.StartDate) + { + errorMessage = "End date must be after start date."; + return; + } + + // Use workflow service to generate lease offer + var offerModel = new Aquiis.Professional.Application.Services.Workflows.LeaseOfferModel + { + StartDate = leaseModel.StartDate, + EndDate = leaseModel.EndDate, + MonthlyRent = leaseModel.MonthlyRent, + SecurityDeposit = leaseModel.SecurityDeposit, + Terms = leaseModel.Terms, + Notes = leaseModel.Notes + }; + + var result = await WorkflowService.GenerateLeaseOfferAsync(application.Id, offerModel); + + if (result.Success) + { + ToastService.ShowSuccess("Lease offer generated successfully!"); + Navigation.NavigateTo($"/propertymanagement/leaseoffers/view/{result.Data!.Id}"); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } + } + catch (Exception ex) + { + errorMessage = $"Error generating lease offer: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private async Task DenyCompetingApplications(int propertyId, int currentApplicationId) + { + // This method is no longer needed - workflow service handles it automatically + await Task.CompletedTask; + } + + private string GetDefaultLeaseTerms() + { + return @"STANDARD LEASE TERMS AND CONDITIONS + +1. RENT PAYMENT +- Rent is due on the 1st of each month +- Late fee of $50 applies after the 5th of the month +- Payment methods: Check, ACH, Online Portal + +2. SECURITY DEPOSIT +- Refundable upon lease termination +- Subject to deductions for damages beyond normal wear and tear +- Will be returned within 30 days of move-out + +3. UTILITIES +- Tenant responsible for: Electric, Gas, Water, Internet +- Landlord responsible for: Trash collection + +4. MAINTENANCE +- Tenant must report maintenance issues within 24 hours +- Emergency repairs available 24/7 +- Routine maintenance requests processed within 48 hours + +5. OCCUPANCY +- Only approved occupants may reside in the property +- No subletting without written permission + +6. PETS +- Pet policy as agreed upon separately +- Pet deposit may apply + +7. TERMINATION +- 60-day notice required for non-renewal +- Early termination subject to fees + +8. OTHER +- No smoking inside the property +- Tenant must maintain renter's insurance +- Property inspections conducted quarterly"; + } + + private int CalculateDuration() + { + if (leaseModel.EndDate <= leaseModel.StartDate) + return 0; + + var months = ((leaseModel.EndDate.Year - leaseModel.StartDate.Year) * 12) + + leaseModel.EndDate.Month - leaseModel.StartDate.Month; + return months; + } + + private void Cancel() + { + Navigation.NavigateTo($"/propertymanagement/applications/{ApplicationId}"); + } + + public class LeaseOfferModel + { + [Required] + public DateTime StartDate { get; set; } = DateTime.Today.AddDays(14); + + [Required] + public DateTime EndDate { get; set; } = DateTime.Today.AddYears(1).AddDays(14); + + [Required] + [Range(0.01, double.MaxValue, ErrorMessage = "Monthly rent must be greater than 0")] + public decimal MonthlyRent { get; set; } + + [Required] + [Range(0, double.MaxValue, ErrorMessage = "Security deposit cannot be negative")] + public decimal SecurityDeposit { get; set; } + + [Required] + [StringLength(5000)] + public string Terms { get; set; } = string.Empty; + + [StringLength(1000)] + public string? Notes { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor new file mode 100644 index 0000000..fb8204a --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor @@ -0,0 +1,475 @@ +@page "/PropertyManagement/ProspectiveTenants" +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Utilities +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService +@inject ProspectiveTenantService ProspectiveTenantService +@inject PropertyService PropertyService + +@rendermode InteractiveServer + +Prospective Tenants + +
+
+
+

Prospective Tenants

+

Manage leads and track the application pipeline

+
+
+ +
+
+ + @if (showAddForm) + { +
+
+
Add New Prospective Tenant
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + + + @foreach (var state in States.StatesArray()) + { + + } + +
+
+ +
+
+ + + + @foreach (var source in ApplicationConstants.ProspectiveSources.AllProspectiveSources) + { + + } + +
+
+ + + + @foreach (var property in properties) + { + + } + +
+
+ +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+
+
+
+ } + + @if (loading) + { +
+
+ Loading... +
+
+ } + else + { +
+ +
+ @if (!filteredProspects.Any()) + { +
+ +

No prospective tenants found

+
+ } + else + { +
+ + + + + + + + + + + + + + @foreach (var prospect in filteredProspects) + { + + + + + + + + + + } + +
NameContactInterested PropertyStatusSourceFirst ContactActions
+ @prospect.FullName + +
@prospect.Email
+ @prospect.Phone +
+ @if (prospect.InterestedProperty != null) + { + @prospect.InterestedProperty.Address + } + else + { + Not specified + } + + + @GetStatusDisplay(prospect.Status) + + @(prospect.Source ?? "N/A")@prospect.FirstContactedOn?.ToString("MM/dd/yyyy") +
+ @if (prospect.Status == ApplicationConstants.ProspectiveStatuses.Lead) + { + + } + @if (prospect.Status == ApplicationConstants.ProspectiveStatuses.TourScheduled || + prospect.Status == ApplicationConstants.ProspectiveStatuses.Lead) + { + + } + + +
+
+
+ } +
+
+ } +
+ +@code { + private List prospects = new(); + private List properties = new(); + private bool loading = true; + private bool showAddForm = false; + private ProspectViewModel newProspect = new(); + private string filterStatus = "All"; + + private List filteredProspects => + filterStatus == "All" + ? prospects + : prospects.Where(p => p.Status == filterStatus).ToList(); + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + loading = true; + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue && organizationId != Guid.Empty) + { + prospects = await ProspectiveTenantService.GetAllAsync(); + + // Load properties for dropdown + var dbContextFactory = Navigation.ToAbsoluteUri("/").ToString(); // Get service + // For now, we'll need to inject PropertyManagementService + var allProperties = await PropertyService.GetAllAsync(); + properties = allProperties.Where(p => p.IsAvailable).ToList(); + } + else + { + ToastService.ShowError("Organization context not available"); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading prospects: {ex.Message}"); + } + finally + { + loading = false; + } + } + + private void ShowAddProspect() + { + newProspect = new ProspectViewModel(); + showAddForm = true; + } + + private void CancelAdd() + { + showAddForm = false; + newProspect = new(); + } + + private async Task HandleAddProspect() + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (!organizationId.HasValue || organizationId == Guid.Empty || string.IsNullOrEmpty(userId)) + { + ToastService.ShowError("User context not available"); + return; + } + + // Map ViewModel to Entity + var prospect = new ProspectiveTenant + { + FirstName = newProspect.FirstName, + LastName = newProspect.LastName, + Email = newProspect.Email, + Phone = newProspect.Phone, + DateOfBirth = newProspect.DateOfBirth, + IdentificationNumber = newProspect.IdentificationNumber, + IdentificationState = newProspect.IdentificationState, + Source = newProspect.Source, + Notes = newProspect.Notes, + InterestedPropertyId = newProspect.InterestedPropertyId, + DesiredMoveInDate = newProspect.DesiredMoveInDate, + OrganizationId = organizationId.Value, + }; + + await ProspectiveTenantService.CreateAsync(prospect); + + ToastService.ShowSuccess("Prospective tenant added successfully"); + showAddForm = false; + await LoadData(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error adding prospect: {ex.Message}"); + } + } + + private void SetFilter(string status) + { + filterStatus = status; + } + + private void ScheduleTour(Guid prospectId) + { + Navigation.NavigateTo($"/PropertyManagement/Tours/Schedule/{prospectId}"); + } + + private void BeginApplication(Guid prospectId) + { + Navigation.NavigateTo($"/propertymanagement/prospects/{prospectId}/submit-application"); + } + + private void ViewDetails(Guid prospectId) + { + Navigation.NavigateTo($"/PropertyManagement/ProspectiveTenants/{prospectId}"); + } + + private async Task DeleteProspect(Guid prospectId) + { + // TODO: Add confirmation dialog in future sprint + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && organizationId != Guid.Empty && !string.IsNullOrEmpty(userId)) + { + await ProspectiveTenantService.DeleteAsync(prospectId); + ToastService.ShowSuccess("Prospect deleted successfully"); + await LoadData(); + } + else + { + ToastService.ShowError("User context not available"); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error deleting prospect: {ex.Message}"); + } + } + + private string GetStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.ProspectiveStatuses.Lead => "bg-secondary", + var s when s == ApplicationConstants.ProspectiveStatuses.TourScheduled => "bg-info", + var s when s == ApplicationConstants.ProspectiveStatuses.Applied => "bg-primary", + var s when s == ApplicationConstants.ProspectiveStatuses.Screening => "bg-warning", + var s when s == ApplicationConstants.ProspectiveStatuses.Approved => "bg-success", + var s when s == ApplicationConstants.ProspectiveStatuses.Denied => "bg-danger", + _ => "bg-secondary" + }; + + private string GetStatusDisplay(string status) => status switch + { + var s when s == ApplicationConstants.ProspectiveStatuses.TourScheduled => "Tour Scheduled", + var s when s == ApplicationConstants.ProspectiveStatuses.ConvertedToTenant => "Converted", + _ => status + }; + + public class ProspectViewModel + { + [Required(ErrorMessage = "First name is required")] + [StringLength(100)] + public string FirstName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Last name is required")] + [StringLength(100)] + public string LastName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Invalid email address")] + [StringLength(200)] + public string Email { get; set; } = string.Empty; + + [Required(ErrorMessage = "Phone is required")] + [Phone(ErrorMessage = "Invalid phone number")] + [StringLength(20)] + public string Phone { get; set; } = string.Empty; + + public DateTime? DateOfBirth { get; set; } + + [StringLength(100)] + public string? IdentificationNumber { get; set; } + + [StringLength(2)] + public string? IdentificationState { get; set; } + + [StringLength(100)] + public string? Source { get; set; } + + [StringLength(2000)] + public string? Notes { get; set; } + + public Guid? InterestedPropertyId { get; set; } + + public DateTime? DesiredMoveInDate { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor new file mode 100644 index 0000000..dbf4046 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor @@ -0,0 +1,1177 @@ +@page "/propertymanagement/applications/{ApplicationId:guid}/review" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Application.Services.Workflows +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations + +@inject RentalApplicationService RentalApplicationService +@inject ScreeningService ScreeningService +@inject LeaseOfferService LeaseOfferService +@inject ApplicationWorkflowService WorkflowService +@inject NavigationManager Navigation +@inject AuthenticationStateProvider AuthStateProvider +@inject UserContextService UserContext +@inject ToastService ToastService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Review Application + +
+
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (application == null) + { +
+

Application Not Found

+

The application you are trying to view does not exist or you do not have permission to access it.

+
+ Return to Applications +
+ } + else + { +
+
+
+
+
+

Application Review

+ @application.Status +
+
+
+ @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + + +
+
Applicant Information
+
+
+ Name: @application.ProspectiveTenant?.FullName +
+
+ Email: @application.ProspectiveTenant?.Email +
+
+ Phone: @application.ProspectiveTenant?.Phone +
+
+ Date of Birth: @application.ProspectiveTenant?.DateOfBirth?.ToString("MMM dd, yyyy") +
+
+ Applied On: @application.AppliedOn.ToString("MMM dd, yyyy") +
+
+ Expires On: @application.ExpiresOn?.ToString("MMM dd, yyyy") + @if (application.ExpiresOn < DateTime.UtcNow) + { + Expired + } + else if ((application.ExpiresOn - DateTime.UtcNow)?.TotalDays < 7) + { + Expires Soon + } +
+
+
+ + +
+
Property
+
+
+ Address: @application.Property?.Address +
+
+ Monthly Rent: @application.Property?.MonthlyRent.ToString("C") +
+
+ Type: @application.Property?.PropertyType +
+
+ Beds/Baths: @application.Property?.Bedrooms / @application.Property?.Bathrooms +
+
+
+ + +
+
Current Address
+
+
+ @application.CurrentAddress
+ @application.CurrentCity, @application.CurrentState @application.CurrentZipCode +
+
+ Current Rent: @application.CurrentRent.ToString("C") +
+
+ Landlord: @application.LandlordName +
+
+ Landlord Phone: @application.LandlordPhone +
+
+
+ + +
+
Employment Information
+
+
+ Employer: @application.EmployerName +
+
+ Job Title: @application.JobTitle +
+
+ Monthly Income: @application.MonthlyIncome.ToString("C") +
+
+ Employment Length: @application.EmploymentLengthMonths months +
+ @if (application.Property != null) + { + var incomeRatio = application.Property.MonthlyRent / application.MonthlyIncome; + var percentOfIncome = incomeRatio * 100; +
+
+ Rent-to-Income Ratio: @percentOfIncome.ToString("F1")% + @if (incomeRatio <= 0.30m) + { + (Excellent - meets 30% guideline) + } + else if (incomeRatio <= 0.35m) + { + (Acceptable - slightly above guideline) + } + else + { + (High risk - significantly above 30% guideline) + } +
+
+ } +
+
+ + +
+
References
+
+
Reference 1
+
+ Name: @application.Reference1Name +
+
+ Phone: @application.Reference1Phone +
+
+ Relationship: @application.Reference1Relationship +
+ + @if (!string.IsNullOrEmpty(application.Reference2Name)) + { +
Reference 2
+
+ Name: @application.Reference2Name +
+
+ Phone: @application.Reference2Phone +
+
+ Relationship: @application.Reference2Relationship +
+ } +
+
+ + +
+
Application Fee
+
+
+ Fee Amount: @application.ApplicationFee.ToString("C") +
+
+ Status: + @if (application.ApplicationFeePaid) + { + Paid + } + else + { + Unpaid + } +
+ @if (application.ApplicationFeePaid && application.ApplicationFeePaidOn.HasValue) + { +
+ Paid On: @application.ApplicationFeePaidOn?.ToString("MMM dd, yyyy") +
+ @if (!string.IsNullOrEmpty(application.ApplicationFeePaymentMethod)) + { +
+ Payment Method: @application.ApplicationFeePaymentMethod +
+ } + } +
+ @if (!application.ApplicationFeePaid) + { +
+ +
+ } +
+ + + @if (screening != null) + { +
+
Screening Results
+
+
+
+
+ Background Check +
+
+ @if (screening.BackgroundCheckRequested) + { +

+ Status: + @if (screening.BackgroundCheckPassed == true) + { + Passed + } + else if (screening.BackgroundCheckPassed == false) + { + Failed + } + else + { + Pending + } +

+ @if (screening.BackgroundCheckRequestedOn.HasValue) + { +

Requested: @screening.BackgroundCheckRequestedOn?.ToString("MMM dd, yyyy")

+ } + @if (screening.BackgroundCheckCompletedOn.HasValue) + { +

Completed: @screening.BackgroundCheckCompletedOn?.ToString("MMM dd, yyyy")

+ } + @if (!string.IsNullOrEmpty(screening.BackgroundCheckNotes)) + { +

Notes: @screening.BackgroundCheckNotes

+ } + @if (!screening.BackgroundCheckPassed.HasValue) + { +
+ + +
+ } + } + else + { +

Not requested

+ } +
+
+
+
+
+
+ Credit Check +
+
+ @if (screening.CreditCheckRequested) + { +

+ Status: + @if (screening.CreditCheckPassed == true) + { + Passed + } + else if (screening.CreditCheckPassed == false) + { + Failed + } + else + { + Pending + } +

+ @if (screening.CreditScore.HasValue) + { +

Credit Score: @screening.CreditScore

+ } + @if (screening.CreditCheckRequestedOn.HasValue) + { +

Requested: @screening.CreditCheckRequestedOn?.ToString("MMM dd, yyyy")

+ } + @if (screening.CreditCheckCompletedOn.HasValue) + { +

Completed: @screening.CreditCheckCompletedOn?.ToString("MMM dd, yyyy")

+ } + @if (!string.IsNullOrEmpty(screening.CreditCheckNotes)) + { +

Notes: @screening.CreditCheckNotes

+ } + @if (!screening.CreditCheckPassed.HasValue) + { +
+ + +
+ } + } + else + { +

Not requested

+ } +
+
+
+
+
+ Overall Result: @screening.OverallResult + @if (!string.IsNullOrEmpty(screening.ResultNotes)) + { +
@screening.ResultNotes + } +
+
+
+
+ } + + +
+ +
+ @if ((application.Status == ApplicationConstants.ApplicationStatuses.Submitted || + application.Status == ApplicationConstants.ApplicationStatuses.UnderReview) && + screening == null && application.ApplicationFeePaid) + { + + } + @if (screening != null && screening.OverallResult == ApplicationConstants.ScreeningResults.Passed && + application.Status != ApplicationConstants.ApplicationStatuses.Approved && + application.Status != ApplicationConstants.ApplicationStatuses.LeaseAccepted && + application.Status != ApplicationConstants.ApplicationStatuses.LeaseOffered && + application.Status != ApplicationConstants.ApplicationStatuses.Denied) + { + + } + @if (application.Status == ApplicationConstants.ApplicationStatuses.Approved && !hasLeaseOffer) + { + + } + @if (application.Status == ApplicationConstants.ApplicationStatuses.LeaseOffered && leaseOffer != null) + { + + } + @if (application.Status == ApplicationConstants.ApplicationStatuses.Approved || + application.Status == ApplicationConstants.ApplicationStatuses.LeaseOffered) + { + + } + else if (application.Status != ApplicationConstants.ApplicationStatuses.Denied && + application.Status != ApplicationConstants.ApplicationStatuses.Withdrawn && + application.Status != ApplicationConstants.ApplicationStatuses.LeaseAccepted) + { + + } +
+
+
+
+
+ +
+
+
+
Workflow Status
+
+
+
+
+ + Application Submitted +
+
+ + Fee Paid +
+
+ + Screening Initiated +
+
+ + Background Check +
+
+ + Credit Check +
+
+ + Approved +
+ @if (application.Status == ApplicationConstants.ApplicationStatuses.Withdrawn) + { +
+ + Withdrawn +
+ } +
+
+
+
+
+ } +
+ + +@if (showDenyModal) +{ + +} + + +@if (showWithdrawModal) +{ + +} + + +@if (showCollectFeeModal) +{ + +} + + +@if (showBackgroundCheckModal) +{ + +} + + +@if (showCreditCheckModal) +{ + +} + + + +@code { + [Parameter] + public Guid ApplicationId { get; set; } + + private RentalApplication? application; + private ApplicationScreening? screening; + private LeaseOffer? leaseOffer; + private bool hasLeaseOffer = false; + private bool isLoading = true; + private bool isSubmitting = false; + private bool showDenyModal = false; + private bool showWithdrawModal = false; + private bool showCollectFeeModal = false; + private bool showBackgroundCheckModal = false; + private bool showCreditCheckModal = false; + private bool backgroundCheckDisposition = false; + private bool creditCheckDisposition = false; + private string errorMessage = string.Empty; + private string successMessage = string.Empty; + private string denyReason = string.Empty; + private string withdrawReason = string.Empty; + private string userId = string.Empty; + private Guid organizationId = Guid.Empty; + private FeePaymentModel feePaymentModel = new(); + private ScreeningDispositionModel backgroundCheckModel = new(); + private ScreeningDispositionModel creditCheckModel = new(); + + protected override async Task OnInitializedAsync() + { + try + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? string.Empty; + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + + await LoadApplication(); + } + catch (Exception ex) + { + errorMessage = $"Error loading application: {ex.Message}"; + } + finally + { + isLoading = false; + } + } + + private async Task LoadApplication() + { + application = await RentalApplicationService.GetRentalApplicationWithRelationsAsync(ApplicationId); + + if (application != null) + { + screening = await ScreeningService.GetScreeningByApplicationIdAsync(ApplicationId); + + // Check if a lease offer already exists for this application + leaseOffer = await LeaseOfferService.GetLeaseOfferByApplicationIdAsync(application.Id); + hasLeaseOffer = leaseOffer != null && !leaseOffer.IsDeleted; + } + } + + private async Task InitiateScreening() + { + if (application == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + // Request both background and credit checks by default + var result = await WorkflowService.InitiateScreeningAsync(ApplicationId, true, true); + + if (result.Success) + { + screening = result.Data; + successMessage = "Screening initiated successfully! Background and credit checks have been requested."; + await LoadApplication(); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } + } + catch (Exception ex) + { + errorMessage = $"Error initiating screening: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private async Task ApproveApplication() + { + if (application == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + var result = await WorkflowService.ApproveApplicationAsync(ApplicationId); + + if (result.Success) + { + ToastService.ShowSuccess("Application approved! You can now generate a lease offer."); + Navigation.NavigateTo($"/propertymanagement/applications/{ApplicationId}/generate-lease-offer"); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } + } + catch (Exception ex) + { + errorMessage = $"Error approving application: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private async Task DenyApplication() + { + if (application == null || string.IsNullOrWhiteSpace(denyReason)) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + var result = await WorkflowService.DenyApplicationAsync(ApplicationId, denyReason); + + if (result.Success) + { + ToastService.ShowInfo("Application denied."); + showDenyModal = false; + await LoadApplication(); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } + } + catch (Exception ex) + { + errorMessage = $"Error denying application: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private async Task WithdrawApplication() + { + if (application == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + var result = await WorkflowService.WithdrawApplicationAsync(ApplicationId, withdrawReason ?? "Withdrawn by applicant"); + + if (result.Success) + { + ToastService.ShowInfo("Application withdrawn."); + showWithdrawModal = false; + await LoadApplication(); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } + } + catch (Exception ex) + { + errorMessage = $"Error withdrawing application: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private async Task HandleCollectFee() + { + if (application == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + // Update application with fee payment details + application.ApplicationFeePaid = true; + application.ApplicationFeePaidOn = feePaymentModel.PaymentDate; + application.ApplicationFeePaymentMethod = feePaymentModel.PaymentMethod; + + // Transition to UnderReview status once fee is paid + if (application.Status == ApplicationConstants.ApplicationStatuses.Submitted) + { + application.Status = ApplicationConstants.ApplicationStatuses.UnderReview; + } + + await RentalApplicationService.UpdateAsync(application); + + var successMsg = $"Application fee of {application.ApplicationFee:C} collected via {feePaymentModel.PaymentMethod}"; + if (!string.IsNullOrEmpty(feePaymentModel.ReferenceNumber)) + { + successMsg += $" (Ref: {feePaymentModel.ReferenceNumber})"; + } + + ToastService.ShowSuccess(successMsg); + showCollectFeeModal = false; + feePaymentModel = new(); + await LoadApplication(); + } + catch (Exception ex) + { + errorMessage = $"Error recording fee payment: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private void ShowBackgroundCheckDisposition(bool passed) + { + backgroundCheckDisposition = passed; + backgroundCheckModel = new(); + showBackgroundCheckModal = true; + } + + private void ShowCreditCheckDisposition(bool passed) + { + creditCheckDisposition = passed; + creditCheckModel = new(); + showCreditCheckModal = true; + } + + private async Task HandleBackgroundCheckDisposition() + { + if (screening == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + screening.BackgroundCheckPassed = backgroundCheckDisposition; + screening.BackgroundCheckCompletedOn = DateTime.UtcNow; + screening.BackgroundCheckNotes = backgroundCheckModel.Notes; + + // Update overall result + await UpdateOverallScreeningResult(screening); + + await ScreeningService.UpdateAsync(screening); + + ToastService.ShowSuccess($"Background check marked as {(backgroundCheckDisposition ? "PASSED" : "FAILED")}"); + showBackgroundCheckModal = false; + await LoadApplication(); + } + catch (Exception ex) + { + errorMessage = $"Error updating background check: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private async Task HandleCreditCheckDisposition() + { + if (screening == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + screening.CreditCheckPassed = creditCheckDisposition; + screening.CreditCheckCompletedOn = DateTime.UtcNow; + screening.CreditScore = creditCheckModel.CreditScore; + screening.CreditCheckNotes = creditCheckModel.Notes; + + // Update overall result + await UpdateOverallScreeningResult(screening); + + await ScreeningService.UpdateAsync(screening); + + ToastService.ShowSuccess($"Credit check marked as {(creditCheckDisposition ? "PASSED" : "FAILED")}"); + showCreditCheckModal = false; + await LoadApplication(); + } + catch (Exception ex) + { + errorMessage = $"Error updating credit check: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private Task UpdateOverallScreeningResult(ApplicationScreening screening) + { + // If both checks are requested + if (screening.BackgroundCheckRequested && screening.CreditCheckRequested) + { + // If both have results + if (screening.BackgroundCheckPassed.HasValue && screening.CreditCheckPassed.HasValue) + { + // Both must pass + if (screening.BackgroundCheckPassed.Value && screening.CreditCheckPassed.Value) + { + screening.OverallResult = ApplicationConstants.ScreeningResults.Passed; + screening.ResultNotes = "All screening checks passed successfully."; + } + else + { + screening.OverallResult = ApplicationConstants.ScreeningResults.Failed; + var failedChecks = new List(); + if (!screening.BackgroundCheckPassed.Value) failedChecks.Add("Background Check"); + if (!screening.CreditCheckPassed.Value) failedChecks.Add("Credit Check"); + screening.ResultNotes = $"Failed: {string.Join(", ", failedChecks)}"; + } + } + else + { + screening.OverallResult = ApplicationConstants.ScreeningResults.Pending; + screening.ResultNotes = "Awaiting completion of all screening checks."; + } + } + // If only background check requested + else if (screening.BackgroundCheckRequested) + { + if (screening.BackgroundCheckPassed.HasValue) + { + screening.OverallResult = screening.BackgroundCheckPassed.Value + ? ApplicationConstants.ScreeningResults.Passed + : ApplicationConstants.ScreeningResults.Failed; + screening.ResultNotes = screening.BackgroundCheckPassed.Value + ? "Background check passed." + : "Background check failed."; + } + } + // If only credit check requested + else if (screening.CreditCheckRequested) + { + if (screening.CreditCheckPassed.HasValue) + { + screening.OverallResult = screening.CreditCheckPassed.Value + ? ApplicationConstants.ScreeningResults.Passed + : ApplicationConstants.ScreeningResults.Failed; + screening.ResultNotes = screening.CreditCheckPassed.Value + ? "Credit check passed." + : "Credit check failed."; + } + } + + return Task.CompletedTask; + } + + private void NavigateToGenerateLeaseOffer() + { + Navigation.NavigateTo($"/propertymanagement/applications/{ApplicationId}/generate-lease-offer"); + } + + private void NavigateToViewLeaseOffer() + { + if (leaseOffer != null) + { + Navigation.NavigateTo($"/propertymanagement/leaseoffers/view/{leaseOffer.Id}"); + } + } + + private void Cancel() + { + Navigation.NavigateTo("/propertymanagement/applications"); + } + + public class FeePaymentModel + { + [Required(ErrorMessage = "Payment method is required")] + public string PaymentMethod { get; set; } = string.Empty; + + public string? ReferenceNumber { get; set; } + + public DateTime PaymentDate { get; set; } = DateTime.Today; + + public string? Notes { get; set; } + } + + public class ScreeningDispositionModel + { + public int? CreditScore { get; set; } + + public string? Notes { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor new file mode 100644 index 0000000..e8a2111 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor @@ -0,0 +1,326 @@ +@page "/PropertyManagement/Tours/Schedule/{ProspectId:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject ProspectiveTenantService ProspectiveTenantService +@inject PropertyService PropertyService +@inject TourService TourService +@inject ChecklistService ChecklistService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService + +@rendermode InteractiveServer + +Schedule Tour + +
+
+
+ +

Schedule Property Tour

+
+
+ + @if (loading) + { +
+
+ Loading... +
+
+ } + else if (prospect == null) + { +
+ Prospective tenant not found. +
+ } + else + { +
+
+
+
+
Tour Details
+
+
+ + + + +
+ + +
+ +
+ + + + @foreach (var property in availableProperties) + { + + } + +
+ +
+ + + + @foreach (var template in tourTemplates) + { + + } + +
Select which checklist to use for this property tour
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ + @if (upcomingTours.Any()) + { +
+
+
Upcoming Tours for @prospect.FullName
+
+
+
+ @foreach (var tour in upcomingTours) + { +
+
+
+
@tour.Property?.Address
+

+ @tour.ScheduledOn.ToString("MMM dd, yyyy") + @tour.ScheduledOn.ToString("h:mm tt") + (@tour.DurationMinutes min) +

+ Status: @tour.Status +
+ @tour.Status +
+
+ } +
+
+
+ } +
+ +
+
+
+
Prospect Information
+
+
+
+
Name
+
@prospect.FullName
+ +
Email
+
@prospect.Email
+ +
Phone
+
@prospect.Phone
+ +
Status
+
@prospect.Status
+ + @if (prospect.InterestedProperty != null) + { +
Interested In
+
@prospect.InterestedProperty.Address
+ } + + @if (prospect.DesiredMoveInDate.HasValue) + { +
Desired Move-In
+
@prospect.DesiredMoveInDate.Value.ToString("MM/dd/yyyy")
+ } + + @if (!string.IsNullOrEmpty(prospect.Notes)) + { +
Notes
+
@prospect.Notes
+ } +
+
+
+
+
+ } +
+ +@code { + [Parameter] + public Guid ProspectId { get; set; } + + private ProspectiveTenant? prospect; + private List availableProperties = new(); + private List upcomingTours = new(); + private List tourTemplates = new(); + private TourViewModel newTour = new(); + private bool loading = true; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + loading = true; + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + prospect = await ProspectiveTenantService.GetByIdAsync(ProspectId); + + if (prospect != null) + { + // Load available properties (Available status only) + var allProperties = await PropertyService.GetAllAsync(); + availableProperties = allProperties + .Where(p => p.Status == ApplicationConstants.PropertyStatuses.Available) + .ToList(); + + // Load available Property Tour templates + var allTemplates = await ChecklistService.GetChecklistTemplatesAsync(); + tourTemplates = allTemplates + .Where(t => t.Category == "Tour" && !t.IsDeleted) + .OrderByDescending(t => t.IsSystemTemplate) // System templates first + .ThenBy(t => t.Name) + .ToList(); + + // Load existing tours for this prospect + upcomingTours = await TourService.GetByProspectiveIdAsync(ProspectId); + upcomingTours = upcomingTours + .Where(s => s.ScheduledOn >= DateTime.Now && s.Status == ApplicationConstants.TourStatuses.Scheduled) + .OrderBy(s => s.ScheduledOn) + .ToList(); + + // Initialize new tour ViewModel + newTour = new TourViewModel + { + ProspectiveTenantId = ProspectId, + PropertyId = prospect.InterestedPropertyId ?? Guid.Empty, + ScheduledOn = DateTime.Now.AddDays(1).Date.AddHours(10), // Default to tomorrow at 10 AM + DurationMinutes = 30, + ChecklistTemplateId = tourTemplates.FirstOrDefault(t => t.IsSystemTemplate && t.Name == "Property Tour")?.Id ?? tourTemplates.FirstOrDefault()?.Id + }; + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading data: {ex.Message}"); + } + finally + { + loading = false; + } + } + + private async Task HandleScheduleTour() + { + try + { + if (newTour.PropertyId == Guid.Empty) + { + ToastService.ShowError("Please select a property"); + return; + } + + if (!newTour.ChecklistTemplateId.HasValue || newTour.ChecklistTemplateId.Value == Guid.Empty) + { + ToastService.ShowError("Please select a checklist template"); + return; + } + + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (!organizationId.HasValue || string.IsNullOrEmpty(userId)) + { + ToastService.ShowError("User context not available"); + return; + } + + // Map ViewModel to Entity + var tour = new Tour + { + ProspectiveTenantId = newTour.ProspectiveTenantId, + PropertyId = newTour.PropertyId, + ScheduledOn = newTour.ScheduledOn, + DurationMinutes = newTour.DurationMinutes, + OrganizationId = organizationId.Value, + CreatedBy = userId + }; + + await TourService.CreateAsync(tour, newTour.ChecklistTemplateId); + + ToastService.ShowSuccess("Tour scheduled successfully"); + Navigation.NavigateTo("/PropertyManagement/Tours"); + } + catch (Exception ex) + { + ToastService.ShowError($"Error scheduling tour: {ex.Message}"); + } + } + + private void Cancel() + { + Navigation.NavigateTo("/PropertyManagement/ProspectiveTenants"); + } + + public class TourViewModel + { + [Required] + public Guid ProspectiveTenantId { get; set; } + + [Required(ErrorMessage = "Property is required")] + public Guid PropertyId { get; set; } + + [Required(ErrorMessage = "Date and time is required")] + public DateTime ScheduledOn { get; set; } + + [Required(ErrorMessage = "Duration is required")] + [Range(15, 180, ErrorMessage = "Duration must be between 15 and 180 minutes")] + public int DurationMinutes { get; set; } + + [Required(ErrorMessage = "Checklist template is required")] + public Guid? ChecklistTemplateId { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor new file mode 100644 index 0000000..415bbd2 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor @@ -0,0 +1,616 @@ +@page "/propertymanagement/prospects/{ProspectId:guid}/submit-application" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Validation +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Application.Services.Workflows +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations + +@inject ProspectiveTenantService ProspectiveTenantService +@inject RentalApplicationService RentalApplicationService +@inject PropertyService PropertyService +@inject OrganizationService OrganizationService +@inject ApplicationWorkflowService WorkflowService +@inject NavigationManager Navigation +@inject AuthenticationStateProvider AuthStateProvider +@inject UserContextService UserContext +@inject ToastService ToastService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager", "Tenant")] +@rendermode InteractiveServer + +Submit Rental Application + +
+
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (prospect == null) + { +
+

Prospect Not Found

+

The prospective tenant you are trying to view does not exist or you do not have permission to access it.

+
+ Return to Prospects +
+ } + else if (existingApplication != null) + { +
+

Application Already Submitted

+

This prospective tenant has already submitted an application for @existingApplication.Property?.Address.

+

Status: @existingApplication.Status

+

Applied On: @existingApplication.AppliedOn.ToString("MMM dd, yyyy")

+
+ View Prospect +
+ } + else + { +
+
+
+
+

Submit Rental Application

+
+
+ @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + + + + +
+
Applicant Information
+
+
+ Name: @prospect.FullName +
+
+ Email: @prospect.Email +
+
+ Phone: @prospect.Phone +
+
+ Date of Birth: @prospect.DateOfBirth?.ToString("MMM dd, yyyy") +
+
+
+ +
+
Property Selection
+
+ + + + @foreach (var property in availableProperties) + { + + } + + +
+ + @if (selectedProperty != null) + { +
+
+
+ Property: @selectedProperty.Address
+ Type: @selectedProperty.PropertyType
+ Beds/Baths: @selectedProperty.Bedrooms / @selectedProperty.Bathrooms +
+
+ Monthly Rent: @selectedProperty.MonthlyRent.ToString("C")
+ Sq Ft: @selectedProperty.SquareFeet
+ Status: @selectedProperty.Status +
+
+
+ } +
+ +
+
Current Address
+
+
+ + + +
+
+ + + +
+
+ + + + @foreach (var state in ApplicationConstants.USStateAbbreviations) + { + + } + + +
+
+ + + +
+
+ +
+ $ + +
+ +
+
+
+ +
+
Current Landlord
+
+
+ + + +
+
+ + + +
+
+
+ +
+
Employment Information
+
+
+ + + +
+
+ + + +
+
+ +
+ $ + +
+ + @if (selectedProperty != null && applicationModel.MonthlyIncome > 0) + { + var ratio = selectedProperty.MonthlyRent / applicationModel.MonthlyIncome; + var percentOfIncome = ratio * 100; + + Rent would be @percentOfIncome.ToString("F1")% of income + @if (ratio <= 0.30m) + { + (Good) + } + else if (ratio <= 0.35m) + { + (Acceptable) + } + else + { + (High) + } + + } +
+
+ + + +
+
+
+ +
+
References
+
+
Reference 1 (Required)
+
+ + + +
+
+ + + +
+
+ + + +
+ +
Reference 2 (Optional)
+
+ + +
+
+ + +
+
+ + +
+
+
+ + @if (applicationFeeRequired) + { +
+
Application Fee
+

Amount Due: @applicationFee.ToString("C")

+

Application fee is non-refundable and must be paid to process your application.

+
+ } + +
+ + +
+
+
+
+
+ +
+
+
+
Application Checklist
+
+
+
    +
  • + + Personal information +
  • +
  • + + Current address +
  • +
  • + + Employment details +
  • +
  • + + References +
  • + @if (applicationFeeRequired) + { +
  • + + Application fee payment +
  • + } +
+
+
+ +
+
+
What Happens Next?
+
+
+
    +
  1. Application submitted for review
  2. +
  3. Background check initiated
  4. +
  5. Credit check performed
  6. +
  7. Application reviewed (1-3 business days)
  8. +
  9. You'll be notified of decision
  10. +
  11. If approved, lease offer sent
  12. +
+
+
+
+
+ } +
+ +@code { + [Parameter] + public Guid ProspectId { get; set; } + + private ProspectiveTenant? prospect; + private RentalApplication? existingApplication; + private List availableProperties = new(); + private Property? selectedProperty; + private ApplicationSubmissionModel applicationModel = new(); + private bool isLoading = true; + private bool isSubmitting = false; + private bool applicationFeeRequired = false; + private decimal applicationFee = 50.00m; + private string errorMessage = string.Empty; + private string userId = string.Empty; + private Guid organizationId = Guid.Empty; + + protected override async Task OnInitializedAsync() + { + try + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? string.Empty; + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + + await LoadData(); + } + catch (Exception ex) + { + errorMessage = $"Error loading data: {ex.Message}"; + } + finally + { + isLoading = false; + } + } + + private async Task LoadData() + { + // Load prospect + prospect = await ProspectiveTenantService.GetByIdAsync(ProspectId); + + if (prospect == null) return; + + // Check if application already exists + existingApplication = await RentalApplicationService.GetApplicationByProspectiveIdAsync(ProspectId); + + // Load available properties + var allProperties = await PropertyService.GetAllAsync(); + availableProperties = allProperties.Where(p => + p.Status == ApplicationConstants.PropertyStatuses.Available || + p.Status == ApplicationConstants.PropertyStatuses.ApplicationPending).ToList(); + + // Pre-select interested property if set + if (prospect.InterestedPropertyId.HasValue) + { + applicationModel.PropertyId = prospect.InterestedPropertyId.Value; + selectedProperty = availableProperties.FirstOrDefault(p => p.Id == prospect.InterestedPropertyId.Value); + } + + // Load organization settings for application fee + var orgSettings = await OrganizationService.GetOrganizationSettingsByOrgIdAsync(prospect.OrganizationId); + if (orgSettings != null) + { + applicationFeeRequired = orgSettings.ApplicationFeeEnabled; + applicationFee = orgSettings.DefaultApplicationFee; + } + } + + private void UpdateSelectedProperty() + { + if (applicationModel.PropertyId != Guid.Empty) + { + selectedProperty = availableProperties.FirstOrDefault(p => p.Id == applicationModel.PropertyId); + } + else + { + selectedProperty = null; + } + } + + private async Task OnPropertyChanged(ChangeEventArgs e) + { + if (Guid.TryParse(e.Value?.ToString(), out Guid propertyId) && propertyId != Guid.Empty) + { + applicationModel.PropertyId = propertyId; + selectedProperty = availableProperties.FirstOrDefault(p => p.Id == propertyId); + } + else + { + applicationModel.PropertyId = Guid.Empty; + selectedProperty = null; + } + StateHasChanged(); + await Task.CompletedTask; + } + + private async Task HandleSubmitApplication() + { + Console.WriteLine("HandleSubmitApplication called"); + + if (prospect == null || selectedProperty == null) + { + errorMessage = prospect == null ? "Prospect not found" : "Please select a property"; + Console.WriteLine($"Validation failed: {errorMessage}"); + return; + } + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + Console.WriteLine($"Submitting application for prospect {ProspectId}, property {applicationModel.PropertyId}"); + + // Use ApplicationWorkflowService to submit application + var submissionModel = new Aquiis.Professional.Application.Services.Workflows.ApplicationSubmissionModel + { + // Current Address + CurrentAddress = applicationModel.CurrentAddress, + CurrentCity = applicationModel.CurrentCity, + CurrentState = applicationModel.CurrentState, + CurrentZipCode = applicationModel.CurrentZipCode, + CurrentRent = applicationModel.CurrentRent, + LandlordName = applicationModel.LandlordName, + LandlordPhone = applicationModel.LandlordPhone, + + // Employment + EmployerName = applicationModel.EmployerName, + JobTitle = applicationModel.JobTitle, + MonthlyIncome = applicationModel.MonthlyIncome, + EmploymentLengthMonths = applicationModel.EmploymentLengthMonths, + + // References + Reference1Name = applicationModel.Reference1Name, + Reference1Phone = applicationModel.Reference1Phone, + Reference1Relationship = applicationModel.Reference1Relationship, + Reference2Name = applicationModel.Reference2Name, + Reference2Phone = applicationModel.Reference2Phone, + Reference2Relationship = applicationModel.Reference2Relationship, + + // Fees + ApplicationFee = applicationFee, + ApplicationFeePaid = false // Will be paid separately + }; + + var result = await WorkflowService.SubmitApplicationAsync( + ProspectId, + applicationModel.PropertyId, + submissionModel); + + Console.WriteLine($"Workflow result: Success={result.Success}, Errors={string.Join(", ", result.Errors)}"); + + if (result.Success) + { + ToastService.ShowSuccess("Application submitted successfully! You will be notified once reviewed."); + Navigation.NavigateTo($"/PropertyManagement/ProspectiveTenants/{ProspectId}"); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } + } + catch (Exception ex) + { + errorMessage = $"Error submitting application: {ex.Message}"; + Console.WriteLine($"Exception in HandleSubmitApplication: {ex}"); + } + finally + { + isSubmitting = false; + } + } + + private void Cancel() + { + Navigation.NavigateTo($"/PropertyManagement/ProspectiveTenants/{ProspectId}"); + } + + public class ApplicationSubmissionModel + { + [RequiredGuid(ErrorMessage = "Please select a property")] + public Guid PropertyId { get; set; } + + [Required] + [StringLength(200)] + public string CurrentAddress { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string CurrentCity { get; set; } = string.Empty; + + [Required] + [StringLength(2)] + public string CurrentState { get; set; } = string.Empty; + + [Required] + [StringLength(10)] + public string CurrentZipCode { get; set; } = string.Empty; + + [Required] + [Range(0.01, double.MaxValue, ErrorMessage = "Current rent must be greater than 0")] + public decimal CurrentRent { get; set; } + + [Required] + [StringLength(200)] + public string LandlordName { get; set; } = string.Empty; + + [Required] + [StringLength(20)] + public string LandlordPhone { get; set; } = string.Empty; + + [Required] + [StringLength(200)] + public string EmployerName { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string JobTitle { get; set; } = string.Empty; + + [Required] + [Range(0.01, double.MaxValue, ErrorMessage = "Monthly income must be greater than 0")] + public decimal MonthlyIncome { get; set; } + + [Required] + [Range(0, int.MaxValue, ErrorMessage = "Employment length cannot be negative")] + public int EmploymentLengthMonths { get; set; } + + [Required] + [StringLength(200)] + public string Reference1Name { get; set; } = string.Empty; + + [Required] + [StringLength(20)] + public string Reference1Phone { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string Reference1Relationship { get; set; } = string.Empty; + + [StringLength(200)] + public string? Reference2Name { get; set; } + + [StringLength(20)] + public string? Reference2Phone { get; set; } + + [StringLength(100)] + public string? Reference2Relationship { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Tours.razor b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Tours.razor new file mode 100644 index 0000000..435ead4 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Tours.razor @@ -0,0 +1,397 @@ +@page "/PropertyManagement/Tours" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService +@inject TourService TourService + +@rendermode InteractiveServer + +Property Tours + +
+
+
+

Property Tours

+

Manage and track property tour appointments

+
+
+
+ +
+ +
+
+ + @if (loading) + { +
+
+ Loading... +
+
+ } + else + { + +
+
+
+ Upcoming Tours (Next 7 Days) +
+
+
+ @if (!upcomingTours.Any()) + { +
+ +

No tours scheduled for the next 7 days

+
+ } + else + { +
+ @foreach (var tour in upcomingTours.OrderBy(s => s.ScheduledOn)) + { + var daysUntil = (tour.ScheduledOn.Date - DateTime.Now.Date).Days; + var timeLabel = daysUntil == 0 ? "Today" : daysUntil == 1 ? "Tomorrow" : tour.ScheduledOn.ToString("MMM dd"); + +
+
+
+
+
+
@timeLabel
+
@tour.ScheduledOn.ToString("h:mm tt")
+ @tour.DurationMinutes min +
+
+
+
@tour.ProspectiveTenant?.FullName
+ + @tour.ProspectiveTenant?.Email
+ @tour.ProspectiveTenant?.Phone +
+
+
+
@tour.Property?.Address
+ + @tour.Property?.City, @tour.Property?.State @tour.Property?.ZipCode + + @if (tour.Checklist != null) + { +
+ + @tour.Checklist.Status + +
+ } +
+
+
+ + +
+
+
+
+
+ } +
+ } +
+
+ + +
+ +
+ @if (!filteredTours.Any()) + { +
+ +

No tours found

+
+ } + else + { +
+ + + + + + + + + + + + + + + @foreach (var tour in filteredTours.OrderByDescending(s => s.ScheduledOn)) + { + + + + + + + + + + + } + +
Date & TimeProspectPropertyDurationStatusTour ChecklistFeedbackActions
+
@tour.ScheduledOn.ToString("MMM dd, yyyy")
+ @tour.ScheduledOn.ToString("h:mm tt") +
+ @tour.ProspectiveTenant?.FullName
+ @tour.ProspectiveTenant?.Phone +
@tour.Property?.Address@tour.DurationMinutes min + + @tour.Status + + + @if (tour.Checklist != null) + { + + @tour.Checklist.Status + + } + else + { + N/A + } + + @if (!string.IsNullOrEmpty(tour.Feedback)) + { + @(tour.Feedback.Length > 50 ? tour.Feedback.Substring(0, 50) + "..." : tour.Feedback) + } + else if (!string.IsNullOrEmpty(tour.InterestLevel)) + { + + @GetInterestDisplay(tour.InterestLevel) + + } + + @if (tour.Status == ApplicationConstants.TourStatuses.Scheduled) + { +
+ + +
+ } + else if (tour.Status == ApplicationConstants.TourStatuses.Completed && tour.ChecklistId.HasValue) + { + + } + else if (tour.Status == ApplicationConstants.TourStatuses.Completed) + { + + } +
+
+ } +
+
+ } +
+ +@code { + private List allTours = new(); + private List upcomingTours = new(); + private bool loading = true; + private string filterStatus = "All"; + + private List filteredTours => + filterStatus == "All" + ? allTours + : allTours.Where(s => s.Status == filterStatus).ToList(); + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + loading = true; + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + allTours = await TourService.GetAllAsync(); + upcomingTours = await TourService.GetUpcomingToursAsync(7); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading tours: {ex.Message}"); + } + finally + { + loading = false; + } + } + + private void SetFilter(string status) + { + filterStatus = status; + } + + private void NavigateToProspects() + { + Navigation.NavigateTo("/PropertyManagement/ProspectiveTenants"); + } + + private void NavigateToCalendar() + { + Navigation.NavigateTo("/PropertyManagement/Tours/Calendar"); + } + + private async Task MarkCompleted(Guid tourId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + var tour = await TourService.GetByIdAsync(tourId); + if (tour != null) + { + // Navigate to the property tour checklist to complete it + if (tour.ChecklistId.HasValue) + { + Navigation.NavigateTo($"/PropertyManagement/Checklists/View/{tour.ChecklistId.Value}"); + } + else + { + ToastService.ShowWarning("No property tour checklist found for this tour"); + } + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error completing tour: {ex.Message}"); + } + } + + private async Task CancelTour(Guid tourId) + { + // TODO: Add confirmation dialog in future sprint + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + if (organizationId.HasValue) + { + await TourService.CancelTourAsync(tourId); + + ToastService.ShowSuccess("Tour cancelled"); + await LoadData(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error cancelling tour: {ex.Message}"); + } + } + + private void ViewFeedback(Guid showingId) + { + Navigation.NavigateTo($"/PropertyManagement/Showings/Feedback/{showingId}"); + } + + private string GetStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.TourStatuses.Scheduled => "bg-info", + var s when s == ApplicationConstants.TourStatuses.Completed => "bg-success", + var s when s == ApplicationConstants.TourStatuses.Cancelled => "bg-danger", + var s when s == ApplicationConstants.TourStatuses.NoShow => "bg-warning", + _ => "bg-secondary" + }; + + private string GetInterestBadgeClass(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "bg-success", + var l when l == ApplicationConstants.TourInterestLevels.Interested => "bg-primary", + var l when l == ApplicationConstants.TourInterestLevels.Neutral => "bg-secondary", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "bg-danger", + _ => "bg-secondary" + }; + + private string GetInterestDisplay(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "Very Interested", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "Not Interested", + _ => level ?? "N/A" + }; + + private string GetChecklistStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.ChecklistStatuses.Draft => "bg-secondary", + var s when s == ApplicationConstants.ChecklistStatuses.InProgress => "bg-info", + var s when s == ApplicationConstants.ChecklistStatuses.Completed => "bg-success", + _ => "bg-secondary" + }; + + private void ViewTourChecklist(Guid checklistId) + { + Navigation.NavigateTo($"/PropertyManagement/Checklists/View/{checklistId}"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor new file mode 100644 index 0000000..6091981 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor @@ -0,0 +1,667 @@ +@page "/PropertyManagement/Tours/Calendar" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService +@inject TourService TourService + +@rendermode InteractiveServer + +Tour Calendar + +
+
+
+

Tour Calendar

+

View and manage scheduled property tours

+
+
+
+ +
+
+ + + +
+ +
+
+ + @if (loading) + { +
+
+ Loading... +
+
+ } + else + { + +
+
+
+ + +

@GetDateRangeTitle()

+ +
+ + +
+
+
+
+ + + @if (viewMode == "day") + { +
+
+
@currentDate.ToString("dddd, MMMM dd, yyyy")
+
+
+ @RenderDayView() +
+
+ } + else if (viewMode == "week") + { + @RenderWeekView() + } + else if (viewMode == "month") + { + @RenderMonthView() + } + } +
+ + +@if (selectedTour != null) +{ + +} + +@code { + private List allTours = new(); + private Tour? selectedTour; + private bool loading = true; + private string viewMode = "week"; // day, week, month + private DateTime currentDate = DateTime.Today; + + protected override async Task OnInitializedAsync() + { + await LoadTours(); + } + + private async Task LoadTours() + { + loading = true; + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + allTours = await TourService.GetAllAsync(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading tours: {ex.Message}"); + } + finally + { + loading = false; + } + } + + private void ChangeView(string mode) + { + viewMode = mode; + } + + private void NavigatePrevious() + { + currentDate = viewMode switch + { + "day" => currentDate.AddDays(-1), + "week" => currentDate.AddDays(-7), + "month" => currentDate.AddMonths(-1), + _ => currentDate + }; + } + + private void NavigateNext() + { + currentDate = viewMode switch + { + "day" => currentDate.AddDays(1), + "week" => currentDate.AddDays(7), + "month" => currentDate.AddMonths(1), + _ => currentDate + }; + } + + private void NavigateToday() + { + currentDate = DateTime.Today; + } + + private string GetDateRangeTitle() + { + return viewMode switch + { + "day" => currentDate.ToString("dddd, MMMM dd, yyyy"), + "week" => $"{GetWeekStart().ToString("MMM dd")} - {GetWeekEnd().ToString("MMM dd, yyyy")}", + "month" => currentDate.ToString("MMMM yyyy"), + _ => "" + }; + } + + private DateTime GetWeekStart() + { + var diff = (7 + (currentDate.DayOfWeek - DayOfWeek.Sunday)) % 7; + return currentDate.AddDays(-1 * diff).Date; + } + + private DateTime GetWeekEnd() + { + return GetWeekStart().AddDays(6); + } + + private RenderFragment RenderDayView() => builder => + { + var dayTours = allTours + .Where(t => t.ScheduledOn.Date == currentDate.Date) + .OrderBy(t => t.ScheduledOn) + .ToList(); + + if (!dayTours.Any()) + { + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", "text-center text-muted p-4"); + builder.OpenElement(2, "i"); + builder.AddAttribute(3, "class", "bi bi-calendar-x"); + builder.AddAttribute(4, "style", "font-size: 3rem;"); + builder.CloseElement(); + builder.OpenElement(5, "p"); + builder.AddAttribute(6, "class", "mt-2"); + builder.AddContent(7, "No tours scheduled for this day"); + builder.CloseElement(); + builder.CloseElement(); + } + else + { + builder.OpenElement(10, "div"); + builder.AddAttribute(11, "class", "list-group"); + + foreach (var tour in dayTours) + { + builder.OpenElement(20, "div"); + builder.AddAttribute(21, "class", "list-group-item list-group-item-action"); + builder.AddAttribute(22, "onclick", EventCallback.Factory.Create(this, () => ShowTourDetail(tour))); + builder.AddAttribute(23, "style", "cursor: pointer;"); + + builder.OpenElement(30, "div"); + builder.AddAttribute(31, "class", "d-flex justify-content-between align-items-start"); + + builder.OpenElement(40, "div"); + builder.OpenElement(41, "h6"); + builder.AddAttribute(42, "class", "mb-1"); + builder.OpenElement(43, "i"); + builder.AddAttribute(44, "class", "bi bi-clock"); + builder.CloseElement(); + builder.AddContent(45, $" {tour.ScheduledOn.ToString("h:mm tt")} - {tour.ScheduledOn.AddMinutes(tour.DurationMinutes).ToString("h:mm tt")}"); + builder.CloseElement(); + + builder.OpenElement(50, "p"); + builder.AddAttribute(51, "class", "mb-1"); + builder.AddContent(52, $"{tour.ProspectiveTenant?.FullName} → {tour.Property?.Address}"); + builder.CloseElement(); + + builder.OpenElement(60, "small"); + builder.AddAttribute(61, "class", "text-muted"); + builder.AddContent(62, $"{tour.DurationMinutes} minutes"); + builder.CloseElement(); + builder.CloseElement(); + + builder.OpenElement(70, "div"); + builder.OpenElement(71, "span"); + builder.AddAttribute(72, "class", $"badge {GetStatusBadgeClass(tour.Status)}"); + builder.AddContent(73, tour.Status); + builder.CloseElement(); + builder.CloseElement(); + + builder.CloseElement(); + builder.CloseElement(); + } + + builder.CloseElement(); + } + }; + + private RenderFragment RenderWeekView() => builder => + { + var weekStart = GetWeekStart(); + var weekEnd = GetWeekEnd(); + + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", "card"); + builder.OpenElement(2, "div"); + builder.AddAttribute(3, "class", "card-body p-0"); + + builder.OpenElement(10, "div"); + builder.AddAttribute(11, "class", "table-responsive"); + builder.OpenElement(12, "table"); + builder.AddAttribute(13, "class", "table table-bordered mb-0"); + + // Header + builder.OpenElement(20, "thead"); + builder.OpenElement(21, "tr"); + + for (int i = 0; i < 7; i++) + { + var date = weekStart.AddDays(i); + var isToday = date.Date == DateTime.Today; + + builder.OpenElement(30 + i, "th"); + builder.AddAttribute(31 + i, "class", isToday ? "bg-primary text-white text-center" : "text-center"); + builder.AddAttribute(32 + i, "style", "width: 14.28%;"); + builder.OpenElement(40 + i, "div"); + builder.AddContent(41 + i, date.ToString("ddd")); + builder.CloseElement(); + builder.OpenElement(50 + i, "div"); + builder.AddAttribute(51 + i, "class", "fs-5"); + builder.AddContent(52 + i, date.Day.ToString()); + builder.CloseElement(); + builder.CloseElement(); + } + + builder.CloseElement(); + builder.CloseElement(); + + // Body + builder.OpenElement(100, "tbody"); + builder.OpenElement(101, "tr"); + + for (int i = 0; i < 7; i++) + { + var date = weekStart.AddDays(i); + var dayTours = allTours + .Where(t => t.ScheduledOn.Date == date.Date) + .OrderBy(t => t.ScheduledOn) + .ToList(); + + builder.OpenElement(110 + i, "td"); + builder.AddAttribute(111 + i, "class", "align-top"); + builder.AddAttribute(112 + i, "style", "min-height: 300px; vertical-align: top;"); + + if (dayTours.Any()) + { + builder.OpenElement(120 + i, "div"); + builder.AddAttribute(121 + i, "class", "d-flex flex-column gap-1"); + + foreach (var tour in dayTours) + { + var index = 130 + (i * 100) + dayTours.IndexOf(tour); + builder.OpenElement(index, "div"); + builder.AddAttribute(index + 1, "class", $"card border-start border-4 {GetBorderColorClass(tour.Status)} mb-1"); + builder.AddAttribute(index + 2, "onclick", EventCallback.Factory.Create(this, () => ShowTourDetail(tour))); + builder.AddAttribute(index + 3, "style", "cursor: pointer;"); + + builder.OpenElement(index + 10, "div"); + builder.AddAttribute(index + 11, "class", "card-body p-2"); + + builder.OpenElement(index + 20, "small"); + builder.AddAttribute(index + 21, "class", "fw-bold d-block"); + builder.AddContent(index + 22, tour.ScheduledOn.ToString("h:mm tt")); + builder.CloseElement(); + + builder.OpenElement(index + 30, "small"); + builder.AddAttribute(index + 31, "class", "d-block text-truncate"); + builder.AddContent(index + 32, tour.ProspectiveTenant?.FullName); + builder.CloseElement(); + + builder.OpenElement(index + 40, "small"); + builder.AddAttribute(index + 41, "class", "d-block text-truncate text-muted"); + builder.AddContent(index + 42, tour.Property?.Address); + builder.CloseElement(); + + builder.CloseElement(); + builder.CloseElement(); + } + + builder.CloseElement(); + } + + builder.CloseElement(); + } + + builder.CloseElement(); + builder.CloseElement(); + + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); + }; + + private RenderFragment RenderMonthView() => builder => + { + var firstDayOfMonth = new DateTime(currentDate.Year, currentDate.Month, 1); + var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1); + var firstDayOfWeek = (int)firstDayOfMonth.DayOfWeek; + var daysInMonth = DateTime.DaysInMonth(currentDate.Year, currentDate.Month); + + var startDate = firstDayOfMonth.AddDays(-firstDayOfWeek); + + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", "card"); + builder.OpenElement(2, "div"); + builder.AddAttribute(3, "class", "card-body p-0"); + + builder.OpenElement(10, "div"); + builder.AddAttribute(11, "class", "table-responsive"); + builder.OpenElement(12, "table"); + builder.AddAttribute(13, "class", "table table-bordered mb-0"); + + // Header - Days of week + builder.OpenElement(20, "thead"); + builder.OpenElement(21, "tr"); + var daysOfWeek = new[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + foreach (var day in daysOfWeek) + { + builder.OpenElement(30, "th"); + builder.AddAttribute(31, "class", "text-center"); + builder.AddContent(32, day); + builder.CloseElement(); + } + builder.CloseElement(); + builder.CloseElement(); + + // Body - Weeks and days + builder.OpenElement(100, "tbody"); + + var currentWeekDate = startDate; + for (int week = 0; week < 6; week++) + { + builder.OpenElement(110 + week, "tr"); + + for (int day = 0; day < 7; day++) + { + var date = currentWeekDate; + var isCurrentMonth = date.Month == currentDate.Month; + var isToday = date.Date == DateTime.Today; + var dayTours = allTours.Where(t => t.ScheduledOn.Date == date.Date).OrderBy(t => t.ScheduledOn).ToList(); + + var cellIndex = 200 + (week * 10) + day; + builder.OpenElement(cellIndex, "td"); + builder.AddAttribute(cellIndex + 1, "class", isCurrentMonth ? "align-top" : "align-top bg-light text-muted"); + builder.AddAttribute(cellIndex + 2, "style", "min-height: 100px; width: 14.28%;"); + + builder.OpenElement(cellIndex + 10, "div"); + builder.AddAttribute(cellIndex + 11, "class", isToday ? "badge bg-primary rounded-circle mb-1" : "fw-bold mb-1"); + builder.AddContent(cellIndex + 12, date.Day.ToString()); + builder.CloseElement(); + + if (dayTours.Any()) + { + builder.OpenElement(cellIndex + 20, "div"); + builder.AddAttribute(cellIndex + 21, "class", "d-flex flex-column gap-1"); + + foreach (var tour in dayTours.Take(3)) + { + var tourIndex = cellIndex + 30 + dayTours.IndexOf(tour); + builder.OpenElement(tourIndex, "div"); + builder.AddAttribute(tourIndex + 1, "class", $"badge {GetStatusBadgeClass(tour.Status)} text-start text-truncate"); + builder.AddAttribute(tourIndex + 2, "onclick", EventCallback.Factory.Create(this, () => ShowTourDetail(tour))); + builder.AddAttribute(tourIndex + 3, "style", "cursor: pointer; font-size: 0.7rem;"); + builder.AddContent(tourIndex + 4, $"{tour.ScheduledOn.ToString("h:mm tt")} - {tour.ProspectiveTenant?.FullName}"); + builder.CloseElement(); + } + + if (dayTours.Count > 3) + { + builder.OpenElement(cellIndex + 80, "small"); + builder.AddAttribute(cellIndex + 81, "class", "text-muted"); + builder.AddContent(cellIndex + 82, $"+{dayTours.Count - 3} more"); + builder.CloseElement(); + } + + builder.CloseElement(); + } + + builder.CloseElement(); + currentWeekDate = currentWeekDate.AddDays(1); + } + + builder.CloseElement(); + } + + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); + }; + + private void ShowTourDetail(Tour tour) + { + selectedTour = tour; + } + + private void CloseModal() + { + selectedTour = null; + } + + private async Task MarkCompleted(Guid tourId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + var tour = await TourService.GetByIdAsync(tourId); + if (tour != null) + { + CloseModal(); + if (tour.ChecklistId.HasValue) + { + Navigation.NavigateTo($"/PropertyManagement/Checklists/View/{tour.ChecklistId.Value}"); + } + else + { + ToastService.ShowWarning("No property tour checklist found for this tour"); + } + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error completing tour: {ex.Message}"); + } + } + + private async Task CancelTour(Guid tourId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + await TourService.CancelTourAsync(tourId); + ToastService.ShowSuccess("Tour cancelled successfully"); + CloseModal(); + await LoadTours(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error cancelling tour: {ex.Message}"); + } + } + + private void NavigateToProspects() + { + Navigation.NavigateTo("/PropertyManagement/ProspectiveTenants"); + } + + private void NavigateToListView() + { + Navigation.NavigateTo("/PropertyManagement/Tours"); + } + + private string GetStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.TourStatuses.Scheduled => "bg-info", + var s when s == ApplicationConstants.TourStatuses.Completed => "bg-success", + var s when s == ApplicationConstants.TourStatuses.Cancelled => "bg-danger", + var s when s == ApplicationConstants.TourStatuses.NoShow => "bg-warning text-dark", + _ => "bg-secondary" + }; + + private string GetBorderColorClass(string status) => status switch + { + var s when s == ApplicationConstants.TourStatuses.Scheduled => "border-info", + var s when s == ApplicationConstants.TourStatuses.Completed => "border-success", + var s when s == ApplicationConstants.TourStatuses.Cancelled => "border-danger", + var s when s == ApplicationConstants.TourStatuses.NoShow => "border-warning", + _ => "border-secondary" + }; + + private string GetChecklistStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.ChecklistStatuses.Draft => "bg-secondary", + var s when s == ApplicationConstants.ChecklistStatuses.InProgress => "bg-warning text-dark", + var s when s == ApplicationConstants.ChecklistStatuses.Completed => "bg-success", + _ => "bg-secondary" + }; + + private string GetInterestBadgeClass(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "bg-success", + var l when l == ApplicationConstants.TourInterestLevels.Interested => "bg-primary", + var l when l == ApplicationConstants.TourInterestLevels.Neutral => "bg-secondary", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "bg-danger", + _ => "bg-secondary" + }; + + private string GetInterestDisplay(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "Very Interested", + var l when l == ApplicationConstants.TourInterestLevels.Interested => "Interested", + var l when l == ApplicationConstants.TourInterestLevels.Neutral => "Neutral", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "Not Interested", + _ => "Unknown" + }; +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor new file mode 100644 index 0000000..c790f05 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor @@ -0,0 +1,813 @@ +@page "/PropertyManagement/ProspectiveTenants/{ProspectId:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Utilities +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authorization + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject ProspectiveTenantService ProspectiveTenantService +@inject TourService TourService +@inject RentalApplicationService RentalApplicationService +@inject PropertyService PropertyService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService + +@rendermode InteractiveServer + +Prospect Details + +
+
+
+ +
+
+ + @if (loading) + { +
+
+ Loading... +
+
+ } + else if (prospect == null) + { +
+ Prospective tenant not found. +
+ } + else + { +
+
+ +
+
+
+ Contact Information +
+
+ + @GetStatusDisplay(prospect.Status) + + @if (!isEditing) + { + + } +
+
+
+ @if (isEditing) + { + + + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + + + @foreach (var state in States.StatesArray()) + { + + } + +
+
+ +
+
+ + + + @foreach (var source in ApplicationConstants.ProspectiveSources.AllProspectiveSources) + { + + } + +
+
+ + + + @foreach (var property in availableProperties) + { + + } + +
+
+ +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+
+ } + else + { +
+
+
+
Name:
+
@prospect.FullName
+ +
Email:
+
+ + @prospect.Email + +
+ +
Phone:
+
+ + @prospect.Phone + +
+ + @if (prospect.DateOfBirth.HasValue) + { +
Date of Birth:
+
@prospect.DateOfBirth.Value.ToString("MMM dd, yyyy")
+ } + + @if (!string.IsNullOrEmpty(prospect.IdentificationNumber)) + { +
ID Number:
+
@prospect.IdentificationNumber @(!string.IsNullOrEmpty(prospect.IdentificationState) ? $"({prospect.IdentificationState})" : "")
+ } +
+
+
+
+
Source:
+
@(prospect.Source ?? "N/A")
+ +
First Contact:
+
@prospect.FirstContactedOn?.ToString("MMM dd, yyyy")
+ + @if (prospect.DesiredMoveInDate.HasValue) + { +
Desired Move-In:
+
@prospect.DesiredMoveInDate.Value.ToString("MMM dd, yyyy")
+ } +
+
+
+ + @if (!string.IsNullOrEmpty(prospect.Notes)) + { +
+
+ Notes: +

@prospect.Notes

+
+ } + + @if (prospect.InterestedProperty != null) + { +
+
+ Interested Property: +
+ @prospect.InterestedProperty.Address
+ + @prospect.InterestedProperty.City, @prospect.InterestedProperty.State @prospect.InterestedProperty.ZipCode +
+ $@prospect.InterestedProperty.MonthlyRent.ToString("N0")/month +
+
+ } + } +
+ +
+ + + @if (tours.Any()) + { +
+
+
Tours History
+
+
+
+ + + + + + + + + + + + + @foreach (var tour in tours.OrderByDescending(s => s.ScheduledOn)) + { + + + + + + + + + } + +
Date & TimePropertyDurationStatusTour ChecklistInterest Level
+ @tour.ScheduledOn.ToString("MMM dd, yyyy")
+ @tour.ScheduledOn.ToString("h:mm tt") +
@tour.Property?.Address@tour.DurationMinutes min + + @tour.Status + + + @if (tour.Checklist != null) + { + + + @tour.Checklist.Status + + + } + else + { + N/A + } + + @if (!string.IsNullOrEmpty(tour.InterestLevel)) + { + + @GetInterestDisplay(tour.InterestLevel) + + } +
+
+
+
+ } + + + @if (application != null) + { +
+
+
Application Status
+
+
+
+
+
+
Application Date:
+
@application.AppliedOn.ToString("MMM dd, yyyy")
+ +
Status:
+
+ + @GetApplicationStatusDisplay(application.Status) + +
+ +
Monthly Income:
+
$@application.MonthlyIncome.ToString("N2")
+
+
+
+
+
Employer:
+
@application.EmployerName
+ +
Job Title:
+
@application.JobTitle
+ +
Application Fee:
+
+ $@application.ApplicationFee.ToString("N2") + @if (application.ApplicationFeePaid) + { + Paid + @if (application.ApplicationFeePaidOn.HasValue) + { + @application.ApplicationFeePaidOn.Value.ToString("MMM dd, yyyy") + } + @if (!string.IsNullOrEmpty(application.ApplicationFeePaymentMethod)) + { + via @application.ApplicationFeePaymentMethod + } + } + else + { + Unpaid + } +
+ + @if (application.ExpiresOn.HasValue) + { +
Expires On:
+
+ @application.ExpiresOn.Value.ToString("MMM dd, yyyy") + @if (application.ExpiresOn.Value < DateTime.UtcNow && application.Status != ApplicationConstants.ApplicationStatuses.Expired) + { + Expired + } + else if (application.ExpiresOn.Value < DateTime.UtcNow.AddDays(7)) + { + Expires Soon + } +
+ } +
+
+
+ + @if (application.Screening != null) + { +
+
Screening Results
+
+
+ Background Check: + @if (application.Screening.BackgroundCheckPassed.HasValue) + { + + @(application.Screening.BackgroundCheckPassed.Value ? "Passed" : "Failed") + + } + else if (application.Screening.BackgroundCheckRequested) + { + Pending + } + else + { + Not Requested + } +
+
+ Credit Check: + @if (application.Screening.CreditCheckPassed.HasValue) + { + + @(application.Screening.CreditCheckPassed.Value ? "Passed" : "Failed") + + @if (application.Screening.CreditScore.HasValue) + { + Score: @application.Screening.CreditScore + } + } + else if (application.Screening.CreditCheckRequested) + { + Pending + } + else + { + Not Requested + } +
+
+ } +
+
+ } +
+ +
+ +
+
+
Quick Actions
+
+
+
+ @if (prospect.Status == ApplicationConstants.ProspectiveStatuses.Lead) + { + + } + else if (prospect.Status == ApplicationConstants.ProspectiveStatuses.TourScheduled) + { + + } + + @if (application == null && (prospect.Status == ApplicationConstants.ProspectiveStatuses.Lead || + prospect.Status == ApplicationConstants.ProspectiveStatuses.TourScheduled)) + { + + } + else if (application != null) + { + + } + + +
+
+
+ + +
+
+
Activity Timeline
+
+
+
+
+
+
+ @prospect.CreatedOn.ToString("MMM dd, yyyy h:mm tt") +

Lead created

+
+
+ + @foreach (var tour in tours.OrderBy(s => s.ScheduledOn)) + { +
+
+
+ @tour.ScheduledOn.ToString("MMM dd, yyyy h:mm tt") +

Property tour - @tour.Property?.Address

+
+
+ } + + @if (application != null) + { +
+
+
+ @application.AppliedOn.ToString("MMM dd, yyyy h:mm tt") +

Application submitted

+
+
+ } +
+
+
+
+
+ } +
+ + + +@code { + [Parameter] + public Guid ProspectId { get; set; } + + private ProspectiveTenant? prospect; + private List tours = new(); + private RentalApplication? application; + private List availableProperties = new(); + private bool loading = true; + private bool isEditing = false; + private ProspectEditViewModel editModel = new(); + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + loading = true; + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + prospect = await ProspectiveTenantService.GetByIdAsync(ProspectId); + + if (prospect != null) + { + tours = await TourService.GetByProspectiveIdAsync(ProspectId); + application = await RentalApplicationService.GetApplicationByProspectiveIdAsync(ProspectId); + + // Load properties for edit dropdown + availableProperties = await PropertyService.GetAllAsync(); + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading prospect details: {ex.Message}"); + } + finally + { + loading = false; + } + } + + private void StartEdit() + { + if (prospect != null) + { + editModel = new ProspectEditViewModel + { + FirstName = prospect.FirstName, + LastName = prospect.LastName, + Email = prospect.Email, + Phone = prospect.Phone, + DateOfBirth = prospect.DateOfBirth, + IdentificationNumber = prospect.IdentificationNumber, + IdentificationState = prospect.IdentificationState, + Source = prospect.Source, + Notes = prospect.Notes, + InterestedPropertyId = prospect.InterestedPropertyId?.ToString(), + DesiredMoveInDate = prospect.DesiredMoveInDate + }; + isEditing = true; + } + } + + private void CancelEdit() + { + isEditing = false; + editModel = new(); + } + + private async Task HandleSaveEdit() + { + if (prospect == null) return; + + try + { + var userId = await UserContext.GetUserIdAsync(); + + // Update prospect with edited values + prospect.FirstName = editModel.FirstName; + prospect.LastName = editModel.LastName; + prospect.Email = editModel.Email; + prospect.Phone = editModel.Phone; + prospect.DateOfBirth = editModel.DateOfBirth; + prospect.IdentificationNumber = editModel.IdentificationNumber; + prospect.IdentificationState = editModel.IdentificationState; + prospect.Source = editModel.Source; + prospect.Notes = editModel.Notes; + prospect.InterestedPropertyId = Guid.TryParse(editModel.InterestedPropertyId, out var propId) && propId != Guid.Empty ? propId : null; + prospect.DesiredMoveInDate = editModel.DesiredMoveInDate; + + await ProspectiveTenantService.UpdateAsync(prospect); + + ToastService.ShowSuccess("Prospect updated successfully"); + isEditing = false; + await LoadData(); // Reload to get updated data with navigation properties + } + catch (Exception ex) + { + ToastService.ShowError($"Error updating prospect: {ex.Message}"); + } + } + + private void ScheduleTour() + { + Navigation.NavigateTo($"/PropertyManagement/Tours/Schedule/{ProspectId}"); + } + + private void BeginApplication() + { + Navigation.NavigateTo($"/propertymanagement/prospects/{ProspectId}/submit-application"); + } + + private void ViewApplication() + { + if (application != null) + { + Navigation.NavigateTo($"/propertymanagement/applications/{application.Id}/review"); + } + } + + private void ViewTours() + { + Navigation.NavigateTo("/PropertyManagement/Tours"); + } + + private void GoBack() + { + Navigation.NavigateTo("/PropertyManagement/ProspectiveTenants"); + } + + private string GetStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.ProspectiveStatuses.Lead => "bg-secondary", + var s when s == ApplicationConstants.ProspectiveStatuses.TourScheduled => "bg-info", + var s when s == ApplicationConstants.ProspectiveStatuses.Applied => "bg-primary", + var s when s == ApplicationConstants.ProspectiveStatuses.Screening => "bg-warning", + var s when s == ApplicationConstants.ProspectiveStatuses.Approved => "bg-success", + var s when s == ApplicationConstants.ProspectiveStatuses.Denied => "bg-danger", + _ => "bg-secondary" + }; + + private string GetStatusDisplay(string status) => status switch + { + var s when s == ApplicationConstants.ProspectiveStatuses.TourScheduled => "Tour Scheduled", + var s when s == ApplicationConstants.ProspectiveStatuses.ConvertedToTenant => "Converted", + _ => status + }; + + private string GetTourStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.TourStatuses.Scheduled => "bg-info", + var s when s == ApplicationConstants.TourStatuses.Completed => "bg-success", + var s when s == ApplicationConstants.TourStatuses.Cancelled => "bg-danger", + var s when s == ApplicationConstants.TourStatuses.NoShow => "bg-warning", + _ => "bg-secondary" + }; + + private string GetInterestBadgeClass(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "bg-success", + var l when l == ApplicationConstants.TourInterestLevels.Interested => "bg-primary", + var l when l == ApplicationConstants.TourInterestLevels.Neutral => "bg-secondary", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "bg-danger", + _ => "bg-secondary" + }; + + private string GetInterestDisplay(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "Very Interested", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "Not Interested", + _ => level ?? "N/A" + }; + + private string GetApplicationStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.ApplicationStatuses.Submitted => "bg-info", + var s when s == ApplicationConstants.ApplicationStatuses.UnderReview => "bg-primary", + var s when s == ApplicationConstants.ApplicationStatuses.Screening => "bg-warning", + var s when s == ApplicationConstants.ApplicationStatuses.Approved => "bg-success", + var s when s == ApplicationConstants.ApplicationStatuses.Denied => "bg-danger", + _ => "bg-secondary" + }; + + private string GetApplicationStatusDisplay(string status) => status switch + { + var s when s == ApplicationConstants.ApplicationStatuses.UnderReview => "Under Review", + _ => status + }; + + private string GetChecklistStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.ChecklistStatuses.Draft => "bg-secondary", + var s when s == ApplicationConstants.ChecklistStatuses.InProgress => "bg-info", + var s when s == ApplicationConstants.ChecklistStatuses.Completed => "bg-success", + _ => "bg-secondary" + }; + + public class ProspectEditViewModel + { + [Required(ErrorMessage = "First name is required")] + [StringLength(100)] + public string FirstName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Last name is required")] + [StringLength(100)] + public string LastName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Invalid email address")] + [StringLength(200)] + public string Email { get; set; } = string.Empty; + + [Required(ErrorMessage = "Phone is required")] + [Phone(ErrorMessage = "Invalid phone number")] + [StringLength(20)] + public string Phone { get; set; } = string.Empty; + + public DateTime? DateOfBirth { get; set; } + + [StringLength(100)] + public string? IdentificationNumber { get; set; } + + [StringLength(2)] + public string? IdentificationState { get; set; } + + [StringLength(100)] + public string? Source { get; set; } + + [StringLength(2000)] + public string? Notes { get; set; } + + public string? InterestedPropertyId { get; set; } + + public DateTime? DesiredMoveInDate { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Calendar.razor b/Aquiis.Professional/Features/PropertyManagement/Calendar.razor new file mode 100644 index 0000000..043e396 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Calendar.razor @@ -0,0 +1,1670 @@ +@page "/PropertyManagement/Calendar" +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Utilities +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Shared.Components + + +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject CalendarEventService CalendarEventService +@inject CalendarSettingsService CalendarSettingsService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService +@inject PropertyManagementService PropertyManagementService +@inject PropertyService PropertyService +@inject TourService TourService +@inject InspectionService InspectionService +@inject MaintenanceService MaintenanceService + +@rendermode InteractiveServer + +Calendar + +
+
+
+

Calendar

+

Tours, Appointments, and other Events

+
+
+
+ +
+
+ + + +
+ + + +
+
+ + @if (showFilters) + { +
+
+
Event Types
+
+ @foreach (var eventType in CalendarEventTypes.GetAllTypes()) + { + var config = CalendarEventTypes.Config[eventType]; +
+
+ + +
+
+ } +
+
+
+ } + + @if (loading) + { +
+
+ Loading... +
+
+ } + else + { + +
+
+
+ + +
+

@GetDateRangeTitle()

+ +
+ +
+ + +
+
+
+
+ + + @if (viewMode == "day") + { +
+
+
@currentDate.ToString("dddd, MMMM dd, yyyy")
+
+
+ @RenderDayView() +
+
+ } + else if (viewMode == "week") + { + @RenderWeekView() + } + else if (viewMode == "month") + { + @RenderMonthView() + } + } +
+ + +@if (selectedEvent != null) +{ + +} + + +@if (showAddEventModal) +{ + +} + +@code { + private List allEvents = new(); + private CalendarEvent? selectedEvent; + private Tour? selectedTour; + private Inspection? selectedInspection; + private MaintenanceRequest? selectedMaintenanceRequest; + private bool loading = true; + private string viewMode = "week"; // day, week, month + private DateTime currentDate = DateTime.Today; + private List selectedEventTypes = new(); + private bool showFilters = false; + private bool showAddEventModal = false; + private CalendarEvent newEvent = new(); + private string propertySearchTerm = string.Empty; + private List propertySearchResults = new(); + private bool showPropertySearchResults = false; + private Property? selectedPropertyForEvent = null; + + protected override async Task OnInitializedAsync() + { + // Load filter defaults from settings + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + var settings = await CalendarSettingsService.GetSettingsAsync(organizationId.Value); + selectedEventTypes = settings + .Where(s => s.ShowOnCalendar) + .Select(s => s.EntityType) + .ToList(); + } + + // Fallback to all types if no settings + if (!selectedEventTypes.Any()) + { + selectedEventTypes = CalendarEventTypes.GetAllTypes().ToList(); + } + + await LoadEvents(); + } + + private async Task LoadEvents() + { + loading = true; + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + // Get date range based on current view + var (startDate, endDate) = viewMode switch + { + "day" => (currentDate.Date, currentDate.Date.AddDays(1)), + "week" => (GetWeekStart(), GetWeekEnd().AddDays(1)), + "month" => (new DateTime(currentDate.Year, currentDate.Month, 1), + new DateTime(currentDate.Year, currentDate.Month, 1).AddMonths(1)), + _ => (currentDate.Date, currentDate.Date.AddDays(1)) + }; + + // Include "Custom" event type in filters to show user-created events + var eventTypesToLoad = selectedEventTypes.Any() + ? selectedEventTypes.Union(new[] { CalendarEventTypes.Custom }).ToList() + : null; + + allEvents = await CalendarEventService.GetEventsAsync( + startDate, + endDate, + eventTypesToLoad + ); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading calendar events: {ex.Message}"); + } + finally + { + loading = false; + } + } + + private async Task OnEventTypeFilterChanged() + { + await LoadEvents(); + } + + private void ToggleFilters() + { + showFilters = !showFilters; + } + + private async Task ToggleEventType(string eventType) + { + if (selectedEventTypes.Contains(eventType)) + { + selectedEventTypes.Remove(eventType); + } + else + { + selectedEventTypes.Add(eventType); + } + await LoadEvents(); + } + + private void NavigateToDashboard() + { + Navigation.NavigateTo("/"); + } + + private async Task ShowAddEventModal() + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + newEvent = new CalendarEvent + { + StartOn = currentDate.Date.AddHours(9), // Default to 9 AM on current date + Color = "#6c757d", + Icon = "bi-calendar-event", + EventType = CalendarEventTypes.Custom, + Status = "Scheduled", + OrganizationId = organizationId.HasValue ? organizationId.Value : Guid.Empty, + CreatedBy = !string.IsNullOrEmpty(userId) ? userId : string.Empty, + CreatedOn = DateTime.UtcNow + }; + showAddEventModal = true; + } + + private void CloseAddEventModal() + { + showAddEventModal = false; + newEvent = new(); + propertySearchTerm = string.Empty; + propertySearchResults.Clear(); + showPropertySearchResults = false; + selectedPropertyForEvent = null; + } + + private async Task OnPropertySearchInput(ChangeEventArgs e) + { + propertySearchTerm = e.Value?.ToString() ?? string.Empty; + + if (string.IsNullOrWhiteSpace(propertySearchTerm)) + { + propertySearchResults.Clear(); + showPropertySearchResults = false; + return; + } + + try + { + propertySearchResults = await PropertyService.SearchPropertiesByAddressAsync(propertySearchTerm); + showPropertySearchResults = propertySearchResults.Any(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error searching properties: {ex.Message}"); + } + } + + private void SelectProperty(Property property) + { + selectedPropertyForEvent = property; + newEvent.PropertyId = property.Id; + propertySearchTerm = $"{property.Address}, {property.City}, {property.State} {property.ZipCode}"; + showPropertySearchResults = false; + propertySearchResults.Clear(); + } + + private void ClearPropertySelection() + { + selectedPropertyForEvent = null; + newEvent.PropertyId = null; + propertySearchTerm = string.Empty; + propertySearchResults.Clear(); + showPropertySearchResults = false; + } + + private async Task SaveCustomEvent() + { + try + { + // Calculate duration if end time is set + if (newEvent.EndOn.HasValue) + { + newEvent.DurationMinutes = (int)(newEvent.EndOn.Value - newEvent.StartOn).TotalMinutes; + } + + await CalendarEventService.CreateCustomEventAsync(newEvent); + + ToastService.ShowSuccess("Event created successfully"); + CloseAddEventModal(); + await LoadEvents(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error creating event: {ex.Message}"); + } + } + + private async Task ChangeView(string mode) + { + viewMode = mode; + await LoadEvents(); + } + + private async Task NavigatePrevious() + { + currentDate = viewMode switch + { + "day" => currentDate.AddDays(-1), + "week" => currentDate.AddDays(-7), + "month" => currentDate.AddMonths(-1), + _ => currentDate + }; + await LoadEvents(); + } + + private async Task NavigateNext() + { + currentDate = viewMode switch + { + "day" => currentDate.AddDays(1), + "week" => currentDate.AddDays(7), + "month" => currentDate.AddMonths(1), + _ => currentDate + }; + await LoadEvents(); + } + + private async Task NavigateToday() + { + currentDate = DateTime.Today; + await LoadEvents(); + } + + private string GetDateRangeTitle() + { + return viewMode switch + { + "day" => currentDate.ToString("dddd, MMMM dd, yyyy"), + "week" => $"{GetWeekStart().ToString("MMM dd")} - {GetWeekEnd().ToString("MMM dd, yyyy")}", + "month" => currentDate.ToString("MMMM yyyy"), + _ => "" + }; + } + + private DateTime GetWeekStart() + { + var diff = (7 + (currentDate.DayOfWeek - DayOfWeek.Sunday)) % 7; + return currentDate.AddDays(-1 * diff).Date; + } + + private DateTime GetWeekEnd() + { + return GetWeekStart().AddDays(6); + } + + private async Task OnDateSelected(ChangeEventArgs e) + { + if (e.Value != null && DateTime.TryParse(e.Value.ToString(), out var selectedDate)) + { + currentDate = selectedDate; + await LoadEvents(); + } + } + + private RenderFragment RenderDayView() => builder => + { + var dayEvents = allEvents + .Where(e => e.StartOn.Date == currentDate.Date) + .OrderBy(e => e.StartOn) + .ToList(); + + if (!dayEvents.Any()) + { + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", "text-center text-muted p-4"); + builder.OpenElement(2, "i"); + builder.AddAttribute(3, "class", "bi bi-calendar-x"); + builder.AddAttribute(4, "style", "font-size: 3rem;"); + builder.CloseElement(); + builder.OpenElement(5, "p"); + builder.AddAttribute(6, "class", "mt-2"); + builder.AddContent(7, "No events scheduled for this day"); + builder.CloseElement(); + builder.CloseElement(); + } + else + { + builder.OpenElement(10, "div"); + builder.AddAttribute(11, "class", "list-group"); + + foreach (var evt in dayEvents) + { + builder.OpenElement(20, "div"); + builder.AddAttribute(21, "class", "list-group-item list-group-item-action"); + builder.AddAttribute(22, "onclick", EventCallback.Factory.Create(this, async () => await ShowEventDetail(evt))); + builder.AddAttribute(23, "style", $"cursor: pointer; border-left: 4px solid {evt.Color};"); + + builder.OpenElement(30, "div"); + builder.AddAttribute(31, "class", "d-flex justify-content-between align-items-start"); + + builder.OpenElement(40, "div"); + builder.OpenElement(41, "h6"); + builder.AddAttribute(42, "class", "mb-1"); + builder.OpenElement(43, "i"); + builder.AddAttribute(44, "class", evt.Icon ?? "bi bi-calendar-event"); + builder.CloseElement(); + var endTime = evt.EndOn?.ToString("h:mm tt") ?? ""; + var timeDisplay = !string.IsNullOrEmpty(endTime) ? $" {evt.StartOn.ToString("h:mm tt")} - {endTime}" : $" {evt.StartOn.ToString("h:mm tt")}"; + builder.AddContent(45, timeDisplay); + builder.CloseElement(); + + builder.OpenElement(50, "p"); + builder.AddAttribute(51, "class", "mb-1"); + builder.AddContent(52, evt.Title); + builder.CloseElement(); + + builder.OpenElement(60, "small"); + builder.AddAttribute(61, "class", "text-muted"); + builder.AddContent(62, evt.Description ?? ""); + builder.CloseElement(); + builder.CloseElement(); + + builder.OpenElement(70, "div"); + builder.OpenElement(71, "span"); + builder.AddAttribute(72, "class", $"badge bg-secondary"); + builder.AddContent(73, CalendarEventTypes.GetDisplayName(evt.EventType)); + builder.CloseElement(); + if (!string.IsNullOrEmpty(evt.Status)) + { + builder.OpenElement(74, "span"); + builder.AddAttribute(75, "class", $"badge {GetEventStatusBadgeClass(evt.Status)} ms-1"); + builder.AddContent(76, evt.Status); + builder.CloseElement(); + } + builder.CloseElement(); + + builder.CloseElement(); + builder.CloseElement(); + } + + builder.CloseElement(); + } + }; + + private RenderFragment RenderWeekView() => builder => + { + var weekStart = GetWeekStart(); + var weekEnd = GetWeekEnd(); + + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", "card"); + builder.OpenElement(2, "div"); + builder.AddAttribute(3, "class", "card-body p-0"); + + builder.OpenElement(10, "div"); + builder.AddAttribute(11, "class", "table-responsive"); + builder.OpenElement(12, "table"); + builder.AddAttribute(13, "class", "table table-bordered mb-0"); + + // Header + builder.OpenElement(20, "thead"); + builder.OpenElement(21, "tr"); + + for (int i = 0; i < 7; i++) + { + var date = weekStart.AddDays(i); + var isToday = date.Date == DateTime.Today; + + builder.OpenElement(30 + i, "th"); + builder.AddAttribute(31 + i, "class", isToday ? "bg-primary text-white text-center" : "text-center"); + builder.AddAttribute(32 + i, "style", "width: 14.28%;"); + builder.OpenElement(40 + i, "div"); + builder.AddContent(41 + i, date.ToString("ddd")); + builder.CloseElement(); + builder.OpenElement(50 + i, "div"); + builder.AddAttribute(51 + i, "class", "fs-5"); + builder.AddContent(52 + i, date.Day.ToString()); + builder.CloseElement(); + builder.CloseElement(); + } + + builder.CloseElement(); + builder.CloseElement(); + + // Body + builder.OpenElement(100, "tbody"); + builder.OpenElement(101, "tr"); + + for (int i = 0; i < 7; i++) + { + var date = weekStart.AddDays(i); + var dayEvents = allEvents + .Where(e => e.StartOn.Date == date.Date) + .OrderBy(e => e.StartOn) + .ToList(); + + builder.OpenElement(110 + i, "td"); + builder.AddAttribute(111 + i, "class", "align-top"); + builder.AddAttribute(112 + i, "style", "min-height: 300px; vertical-align: top;"); + + if (dayEvents.Any()) + { + builder.OpenElement(120 + i, "div"); + builder.AddAttribute(121 + i, "class", "d-flex flex-column gap-1"); + + foreach (var evt in dayEvents) + { + var index = 130 + (i * 100) + dayEvents.IndexOf(evt); + builder.OpenElement(index, "div"); + builder.AddAttribute(index + 1, "class", "card border-start border-4 mb-1"); + builder.AddAttribute(index + 2, "onclick", EventCallback.Factory.Create(this, async () => await ShowEventDetail(evt))); + builder.AddAttribute(index + 3, "style", $"cursor: pointer; border-left-color: {evt.Color} !important;"); + + builder.OpenElement(index + 10, "div"); + builder.AddAttribute(index + 11, "class", "card-body p-2"); + + builder.OpenElement(index + 20, "small"); + builder.AddAttribute(index + 21, "class", "fw-bold d-block"); + builder.OpenElement(index + 22, "i"); + builder.AddAttribute(index + 23, "class", evt.Icon ?? "bi bi-calendar-event"); + builder.CloseElement(); + builder.AddContent(index + 24, $" {evt.StartOn.ToString("h:mm tt")}"); + builder.CloseElement(); + + builder.OpenElement(index + 30, "small"); + builder.AddAttribute(index + 31, "class", "d-block text-truncate"); + builder.AddContent(index + 32, evt.Title); + builder.CloseElement(); + + builder.OpenElement(index + 40, "small"); + builder.AddAttribute(index + 41, "class", "d-block text-truncate text-muted"); + builder.OpenElement(index + 42, "span"); + builder.AddAttribute(index + 43, "class", "badge bg-secondary" ); + builder.AddAttribute(index + 44, "style", "font-size: 0.65rem;"); + builder.AddContent(index + 45, CalendarEventTypes.GetDisplayName(evt.EventType)); + builder.CloseElement(); + if (!string.IsNullOrEmpty(evt.Status)) + { + builder.OpenElement(index + 46, "span"); + builder.AddAttribute(index + 47, "class", $"badge {GetEventStatusBadgeClass(evt.Status)} ms-1"); + builder.AddAttribute(index + 48, "style", "font-size: 0.65rem;"); + builder.AddContent(index + 49, evt.Status); + builder.CloseElement(); + } + builder.CloseElement(); + + builder.CloseElement(); + builder.CloseElement(); + } + + builder.CloseElement(); + } + + builder.CloseElement(); + } + + builder.CloseElement(); // Close tr + builder.CloseElement(); // Close tbody + builder.CloseElement(); // Close table + builder.CloseElement(); // Close table-responsive div + builder.CloseElement(); // Close card-body + builder.CloseElement(); // Close card + }; + + private RenderFragment RenderMonthView() => builder => + { + var firstDayOfMonth = new DateTime(currentDate.Year, currentDate.Month, 1); + var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1); + var firstDayOfWeek = (int)firstDayOfMonth.DayOfWeek; + var daysInMonth = DateTime.DaysInMonth(currentDate.Year, currentDate.Month); + + var startDate = firstDayOfMonth.AddDays(-firstDayOfWeek); + + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", "card"); + builder.OpenElement(2, "div"); + builder.AddAttribute(3, "class", "card-body p-0"); + + builder.OpenElement(10, "div"); + builder.AddAttribute(11, "class", "table-responsive"); + builder.OpenElement(12, "table"); + builder.AddAttribute(13, "class", "table table-bordered mb-0"); + + // Header - Days of week + builder.OpenElement(20, "thead"); + builder.OpenElement(21, "tr"); + var daysOfWeek = new[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + foreach (var day in daysOfWeek) + { + builder.OpenElement(30, "th"); + builder.AddAttribute(31, "class", "text-center"); + builder.AddContent(32, day); + builder.CloseElement(); + } + builder.CloseElement(); + builder.CloseElement(); + + // Body - Weeks and days + builder.OpenElement(100, "tbody"); + + var currentWeekDate = startDate; + for (int week = 0; week < 6; week++) + { + builder.OpenElement(110 + week, "tr"); + + for (int day = 0; day < 7; day++) + { + var date = currentWeekDate; + var isCurrentMonth = date.Month == currentDate.Month; + var isToday = date.Date == DateTime.Today; + var dayEvents = allEvents + .Where(e => e.StartOn.Date == date.Date) + .OrderBy(e => e.StartOn) + .ToList(); + + var cellIndex = 200 + (week * 10) + day; + builder.OpenElement(cellIndex, "td"); + builder.AddAttribute(cellIndex + 1, "class", isCurrentMonth ? "align-top" : "align-top bg-light text-muted"); + builder.AddAttribute(cellIndex + 2, "style", "min-height: 100px; width: 14.28%;"); + + builder.OpenElement(cellIndex + 10, "div"); + builder.AddAttribute(cellIndex + 11, "class", isToday ? "badge bg-primary rounded-circle mb-1" : "fw-bold mb-1"); + builder.AddContent(cellIndex + 12, date.Day.ToString()); + builder.CloseElement(); + + if (dayEvents.Any()) + { + builder.OpenElement(cellIndex + 20, "div"); + builder.AddAttribute(cellIndex + 21, "class", "d-flex flex-column gap-1"); + + foreach (var evt in dayEvents.Take(3)) + { + var eventIndex = cellIndex + 30 + dayEvents.IndexOf(evt); + builder.OpenElement(eventIndex, "div"); + builder.AddAttribute(eventIndex + 1, "class", $"badge {GetMonthViewEventBadgeClass(evt)} text-start text-truncate"); + builder.AddAttribute(eventIndex + 2, "onclick", EventCallback.Factory.Create(this, async () => await ShowEventDetail(evt))); + builder.AddAttribute(eventIndex + 3, "style", $"cursor: pointer; font-size: 0.7rem; border-left: 3px solid {evt.Color};"); + builder.OpenElement(eventIndex + 4, "i"); + builder.AddAttribute(eventIndex + 5, "class", evt.Icon ?? "bi bi-calendar-event"); + builder.CloseElement(); + builder.AddContent(eventIndex + 6, $" {evt.StartOn.ToString("h:mm tt")} - {evt.Title}"); + builder.CloseElement(); + } + + if (dayEvents.Count > 3) + { + builder.OpenElement(cellIndex + 80, "small"); + builder.AddAttribute(cellIndex + 81, "class", "text-muted"); + builder.AddContent(cellIndex + 82, $"+{dayEvents.Count - 3} more"); + builder.CloseElement(); + } + + builder.CloseElement(); + } + + builder.CloseElement(); + currentWeekDate = currentWeekDate.AddDays(1); + } + + builder.CloseElement(); + } + + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); + }; + + private void ShowTourDetail(Tour tour) + { + selectedTour = tour; + } + + private async Task ShowEventDetail(CalendarEvent calendarEvent) + { + // Load entity and show modal for all event types + selectedEvent = calendarEvent; + + if (!calendarEvent.SourceEntityId.HasValue) + { + // Custom event without source entity - just show basic info + return; + } + + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue) return; + + switch (calendarEvent.EventType) + { + case CalendarEventTypes.Tour: + await ShowTourDetailById(calendarEvent.SourceEntityId.Value); + break; + + case CalendarEventTypes.Inspection: + // Check if this is a property-based routine inspection or an actual inspection record + if (calendarEvent.SourceEntityType == "Property") + { + // This is a scheduled routine inspection - no Inspection record exists yet + // Just show the basic calendar event info (selectedEvent is already set) + } + else + { + // This is linked to an actual Inspection record + await ShowInspectionDetailById(calendarEvent.SourceEntityId.Value); + } + break; + + case CalendarEventTypes.Maintenance: + await ShowMaintenanceRequestDetailById(calendarEvent.SourceEntityId.Value); + break; + + // Other event types (LeaseExpiry, RentDue, Custom) just show basic info + default: + break; + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading event details: {ex.Message}"); + } + } + + private async Task ShowTourDetailById(Guid tourId) + { + var tour = await TourService.GetByIdAsync(tourId); + if (tour != null) + { + selectedTour = tour; + } + else + { + ToastService.ShowError("Tour not found"); + } + } + + private async Task ShowInspectionDetailById(Guid inspectionId) + { + var inspection = await InspectionService.GetByIdAsync(inspectionId); + if (inspection != null) + { + selectedInspection = inspection; + } + else + { + ToastService.ShowError("Inspection not found"); + } + } + + private async Task ShowMaintenanceRequestDetailById(Guid maintenanceId) + { + var maintenanceRequest = await MaintenanceService.GetByIdAsync(maintenanceId); + if (maintenanceRequest != null) + { + selectedMaintenanceRequest = maintenanceRequest; + } + else + { + ToastService.ShowError("Maintenance request not found"); + } + } + + private void CloseModal() + { + selectedEvent = null; + selectedTour = null; + selectedInspection = null; + selectedMaintenanceRequest = null; + } + + private void NavigateToEventDetail() + { + if (selectedEvent == null) return; + + // For tours, navigate to checklist if available + if (selectedEvent.SourceEntityType == nameof(Tour) && selectedTour != null) + { + if (selectedTour.ChecklistId.HasValue) + { + Navigation.NavigateTo($"/PropertyManagement/Checklists/View/{selectedTour.ChecklistId.Value}"); + } + else + { + ToastService.ShowWarning("No checklist found for this tour"); + } + return; + } + + // For other event types, use the router + if (CalendarEventRouter.IsRoutable(selectedEvent)) + { + var route = CalendarEventRouter.GetRouteForEvent(selectedEvent); + if (!string.IsNullOrEmpty(route)) + { + Navigation.NavigateTo(route); + } + } + } + + private void ShowMaintenanceRequestDetail(MaintenanceRequest request) + { + // Navigate to maintenance request detail page + Navigation.NavigateTo($"/PropertyManagement/Maintenance/View/{request.Id}"); + } + + private void ShowPropertyDetail(Property property) + { + // Navigate to property detail page + Navigation.NavigateTo($"/PropertyManagement/Properties/View/{property.Id}"); + } + + private async Task CompleteTour(Guid tourId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + var tour = await TourService.GetByIdAsync(tourId); + if (tour != null) + { + CloseModal(); + if (tour.ChecklistId.HasValue) + { + Navigation.NavigateTo($"/PropertyManagement/Checklists/View/{tour.ChecklistId.Value}"); + } + else + { + ToastService.ShowWarning("No property tour checklist found for this tour"); + } + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error completing tour: {ex.Message}"); + } + } + + private async Task CancelTour(Guid tourId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + await TourService.CancelTourAsync(tourId); + ToastService.ShowSuccess("Tour cancelled successfully"); + CloseModal(); + await LoadEvents(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error cancelling tour: {ex.Message}"); + } + } + + private async Task MarkTourAsNoShow(Guid tourId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + await TourService.MarkTourAsNoShowAsync(tourId); + ToastService.ShowSuccess("Tour marked as No Show"); + CloseModal(); + await LoadEvents(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error marking tour as no show: {ex.Message}"); + } + } + + private async Task StartWork(Guid maintenanceRequestId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + var request = await MaintenanceService.GetByIdAsync(maintenanceRequestId); + if (request != null) + { + request.Status = ApplicationConstants.MaintenanceRequestStatuses.InProgress; + + + await MaintenanceService.UpdateAsync(request); + ToastService.ShowSuccess("Work started on maintenance request"); + + // Reload the maintenance request to show updated status + await ShowMaintenanceRequestDetailById(maintenanceRequestId); + await LoadEvents(); + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error starting work: {ex.Message}"); + } + } + + private async Task CompleteMaintenanceRequest(Guid maintenanceRequestId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + var request = await MaintenanceService.GetByIdAsync(maintenanceRequestId); + if (request != null) + { + request.Status = ApplicationConstants.MaintenanceRequestStatuses.Completed; + request.CompletedOn = request.CompletedOn ?? DateTime.UtcNow; + + await MaintenanceService.UpdateAsync(request); + ToastService.ShowSuccess("Maintenance request marked as complete"); + + // Reload the maintenance request to show updated status + await ShowMaintenanceRequestDetailById(maintenanceRequestId); + await LoadEvents(); + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error completing request: {ex.Message}"); + } + } + + private async Task CancelMaintenanceRequest(Guid maintenanceRequestId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + var request = await MaintenanceService.GetByIdAsync(maintenanceRequestId); + if (request != null) + { + request.Status = ApplicationConstants.MaintenanceRequestStatuses.Cancelled; + + await MaintenanceService.UpdateAsync(request); + ToastService.ShowSuccess("Maintenance request cancelled"); + CloseModal(); + await LoadEvents(); + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error cancelling request: {ex.Message}"); + } + } + + private async Task UpdateCustomEventStatus(Guid eventId, string newStatus) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (!organizationId.HasValue || string.IsNullOrEmpty(userId)) + { + ToastService.ShowError("Unable to identify user or organization"); + return; + } + + var calendarEvent = await CalendarEventService.GetEventByIdAsync(eventId); + if (calendarEvent != null && calendarEvent.IsCustomEvent) + { + calendarEvent.Status = newStatus; + + await CalendarEventService.UpdateCustomEventAsync(calendarEvent); + + // Update the selected event to reflect the change + if (selectedEvent != null && selectedEvent.Id == eventId) + { + selectedEvent.Status = newStatus; + } + + ToastService.ShowSuccess($"Event status updated to {newStatus}"); + await LoadEvents(); + } + else + { + ToastService.ShowError("Event not found or is not a custom event"); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error updating event status: {ex.Message}"); + } + } + + private void NavigateToProspects() + { + Navigation.NavigateTo("/PropertyManagement/ProspectiveTenants"); + } + + private void NavigateToListView() + { + Navigation.NavigateTo("/PropertyManagement/Calendar/ListView"); + } + + private void CompleteRoutineInspection(Guid propertyId) + { + // Navigate to create new inspection form with the property pre-selected + Navigation.NavigateTo($"/PropertyManagement/Inspections/Create?propertyId={propertyId}"); + } + + private string GetStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.TourStatuses.Scheduled => "bg-info", + var s when s == ApplicationConstants.TourStatuses.Completed => "bg-success", + var s when s == ApplicationConstants.TourStatuses.Cancelled => "bg-danger", + var s when s == ApplicationConstants.TourStatuses.NoShow => "bg-warning text-dark", + _ => "bg-secondary" + }; + + private string GetBorderColorClass(string status) => status switch + { + var s when s == ApplicationConstants.TourStatuses.Scheduled => "border-info", + var s when s == ApplicationConstants.TourStatuses.Completed => "border-success", + var s when s == ApplicationConstants.TourStatuses.Cancelled => "border-danger", + var s when s == ApplicationConstants.TourStatuses.NoShow => "border-warning", + _ => "border-secondary" + }; + + private string GetChecklistStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.ChecklistStatuses.Draft => "bg-secondary", + var s when s == ApplicationConstants.ChecklistStatuses.InProgress => "bg-warning text-dark", + var s when s == ApplicationConstants.ChecklistStatuses.Completed => "bg-success", + _ => "bg-secondary" + }; + + private string GetInterestBadgeClass(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "bg-success", + var l when l == ApplicationConstants.TourInterestLevels.Interested => "bg-primary", + var l when l == ApplicationConstants.TourInterestLevels.Neutral => "bg-secondary", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "bg-danger", + _ => "bg-secondary" + }; + + private string GetInterestDisplay(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "Very Interested", + var l when l == ApplicationConstants.TourInterestLevels.Interested => "Interested", + var l when l == ApplicationConstants.TourInterestLevels.Neutral => "Neutral", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "Not Interested", + _ => "Unknown" + }; + + private string GetEventStatusBadgeClass(string status) => status switch + { + var s when s == ApplicationConstants.TourStatuses.Scheduled => "bg-info", + var s when s == ApplicationConstants.TourStatuses.Completed => "bg-success", + var s when s == ApplicationConstants.TourStatuses.Cancelled => "bg-danger", + var s when s == ApplicationConstants.TourStatuses.NoShow => "bg-warning text-dark", + var s when s == ApplicationConstants.MaintenanceRequestStatuses.Completed => "bg-success", + var s when s == ApplicationConstants.MaintenanceRequestStatuses.InProgress => "bg-warning text-dark", + var s when s == ApplicationConstants.MaintenanceRequestStatuses.Submitted => "bg-info", + var s when s == ApplicationConstants.MaintenanceRequestStatuses.Cancelled => "bg-danger", + "Good" => "bg-success", + "Excellent" => "bg-success", + "Fair" => "bg-warning text-dark", + "Poor" => "bg-danger", + _ => "bg-secondary" + }; + + private string GetPriorityBadgeClass(string priority) => priority switch + { + "High" => "bg-danger", + "Medium" => "bg-warning text-dark", + "Low" => "bg-info", + _ => "bg-secondary" + }; + + private string GetInspectionStatusBadgeClass(string status) => status switch + { + "Good" => "bg-success", + "Fair" => "bg-warning text-dark", + "Poor" => "bg-danger", + "Completed" => "bg-success", + _ => "bg-secondary" + }; + + private string GetMonthViewEventBadgeClass(CalendarEvent evt) + { + // Prioritize status-based coloring + if (!string.IsNullOrEmpty(evt.Status)) + { + // Tour statuses + if (evt.Status == ApplicationConstants.TourStatuses.Completed) + return "bg-success"; + if (evt.Status == ApplicationConstants.TourStatuses.Scheduled) + return "bg-info"; + if (evt.Status == ApplicationConstants.TourStatuses.Cancelled) + return "bg-danger"; + if (evt.Status == ApplicationConstants.TourStatuses.NoShow) + return "bg-warning text-dark"; + + // Maintenance request statuses + if (evt.Status == ApplicationConstants.MaintenanceRequestStatuses.Completed) + return "bg-success"; + if (evt.Status == ApplicationConstants.MaintenanceRequestStatuses.InProgress) + return "bg-warning text-dark"; + if (evt.Status == ApplicationConstants.MaintenanceRequestStatuses.Submitted) + return "bg-info"; + if (evt.Status == ApplicationConstants.MaintenanceRequestStatuses.Cancelled) + return "bg-danger"; + + // Inspection overall conditions + if (evt.Status == "Good") + return "bg-success"; + if (evt.Status == "Fair") + return "bg-warning text-dark"; + if (evt.Status == "Poor") + return "bg-danger"; + } + + return "bg-secondary"; + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/CalendarListView.razor b/Aquiis.Professional/Features/PropertyManagement/CalendarListView.razor new file mode 100644 index 0000000..e216eaf --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/CalendarListView.razor @@ -0,0 +1,877 @@ +@page "/PropertyManagement/Calendar/ListView" +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Utilities +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject CalendarEventService CalendarEventService +@inject CalendarSettingsService CalendarSettingsService +@inject PropertyManagementService PropertyManagementService +@inject PropertyService PropertyService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@inject ToastService ToastService + +@inject TourService TourService +@inject InspectionService InspectionService +@inject MaintenanceService MaintenanceService +@inject LeaseService LeaseService + +@rendermode InteractiveServer + +Calendar - List View + +
+
+
+

Calendar - List View

+

All scheduled events for the next 30 days

+
+
+ + +
+
+ + @if (showFilters) + { +
+
+
Event Types
+
+ @foreach (var eventType in CalendarEventTypes.GetAllTypes()) + { + var config = CalendarEventTypes.Config[eventType]; +
+
+ + +
+
+ } +
+
+
+ } + + @if (loading) + { +
+
+ Loading... +
+
+ } + else + { +
+
+ @if (filteredEvents.Any()) + { +
+ + + + + + + + + + + + + @foreach (var evt in pagedEvents) + { + + + + + + + + + } + +
Date/TimeEvent TypeTitleDescriptionStatusActions
+
@evt.StartOn.ToString("MMM dd, yyyy")
+ + @if (evt.EndOn.HasValue) + { + @($"{evt.StartOn.ToString("h:mm tt")} - {evt.EndOn.Value.ToString("h:mm tt")}") + } + else + { + @evt.StartOn.ToString("h:mm tt") + } + +
+ + @CalendarEventTypes.GetDisplayName(evt.EventType) + @evt.Title + @(evt.Description ?? "-") + + @if (!string.IsNullOrEmpty(evt.Status)) + { + @evt.Status + } + else + { + - + } + + +
+
+ + +
+
+ Showing @((currentPage - 1) * pageSize + 1) to @Math.Min(currentPage * pageSize, filteredEvents.Count) of @filteredEvents.Count events +
+ +
+ } + else + { +
+ +

No events found for the next 30 days

+
+ } +
+
+ } +
+ + +@if (selectedEvent != null) +{ + +} + +@code { + private List allEvents = new(); + private List filteredEvents = new(); + private List pagedEvents = new(); + private HashSet selectedEventTypes = new(); + private bool loading = true; + private bool showFilters = false; + + // Modal state + private CalendarEvent? selectedEvent; + private Tour? selectedTour; + private Inspection? selectedInspection; + private MaintenanceRequest? selectedMaintenanceRequest; + + // Pagination + private int currentPage = 1; + private int pageSize = 20; + private int totalPages => (int)Math.Ceiling(filteredEvents.Count / (double)pageSize); + + protected override async Task OnInitializedAsync() + { + await LoadEvents(); + + // Initialize with all event types selected + foreach (var eventType in CalendarEventTypes.GetAllTypes()) + { + selectedEventTypes.Add(eventType); + } + + ApplyFilters(); + } + + private async Task LoadEvents() + { + try + { + loading = true; + + + // Get events for the next 30 days + var startDate = DateTime.Today; + var endDate = DateTime.Today.AddDays(30); + + allEvents = await CalendarEventService.GetEventsAsync(startDate, endDate); + allEvents = allEvents.OrderBy(e => e.StartOn).ToList(); + + ApplyFilters(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading events: {ex.Message}"); + } + finally + { + loading = false; + } + } + + private void ToggleEventType(string eventType) + { + if (selectedEventTypes.Contains(eventType)) + { + selectedEventTypes.Remove(eventType); + } + else + { + selectedEventTypes.Add(eventType); + } + + currentPage = 1; // Reset to first page when filtering + ApplyFilters(); + } + + private void ApplyFilters() + { + filteredEvents = allEvents + .Where(e => selectedEventTypes.Contains(e.EventType)) + .ToList(); + + UpdatePagedEvents(); + } + + private void UpdatePagedEvents() + { + pagedEvents = filteredEvents + .Skip((currentPage - 1) * pageSize) + .Take(pageSize) + .ToList(); + } + + private void ChangePage(int page) + { + if (page < 1 || page > totalPages) + return; + + currentPage = page; + UpdatePagedEvents(); + } + + private void ToggleFilters() + { + showFilters = !showFilters; + } + + private void NavigateToCalendar() + { + Navigation.NavigateTo("/PropertyManagement/Calendar"); + } + + private void CompleteRoutineInspection(Guid propertyId) + { + // Navigate to create new inspection form with the property pre-selected + Navigation.NavigateTo($"/PropertyManagement/Inspections/Create?propertyId={propertyId}"); + } + + private async Task ShowEventDetail(CalendarEvent calendarEvent) + { + // Load entity and show modal for all event types + selectedEvent = calendarEvent; + + if (!calendarEvent.SourceEntityId.HasValue) + { + // Custom event without source entity - just show basic info + return; + } + + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue) return; + + switch (calendarEvent.EventType) + { + case CalendarEventTypes.Tour: + await ShowTourDetailById(calendarEvent.SourceEntityId.Value); + break; + + case CalendarEventTypes.Inspection: + // Check if this is a property-based routine inspection or an actual inspection record + if (calendarEvent.SourceEntityType == "Property") + { + // This is a scheduled routine inspection - no Inspection record exists yet + // Just show the basic calendar event info (selectedEvent is already set) + } + else + { + // This is linked to an actual Inspection record + await ShowInspectionDetailById(calendarEvent.SourceEntityId.Value); + } + break; + + case CalendarEventTypes.Maintenance: + await ShowMaintenanceRequestDetailById(calendarEvent.SourceEntityId.Value); + break; + + // Other event types (LeaseExpiry, RentDue, Custom) just show basic info + default: + break; + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading event details: {ex.Message}"); + } + } + + private async Task ShowTourDetailById(Guid tourId) + { + var tour = await TourService.GetByIdAsync(tourId); + if (tour != null) + { + selectedTour = tour; + } + else + { + ToastService.ShowError("Tour not found"); + } + } + + private async Task ShowInspectionDetailById(Guid inspectionId) + { + var inspection = await InspectionService.GetByIdAsync(inspectionId); + if (inspection != null) + { + selectedInspection = inspection; + } + else + { + ToastService.ShowError("Inspection not found"); + } + } + + private async Task ShowMaintenanceRequestDetailById(Guid maintenanceId) + { + var maintenanceRequest = await MaintenanceService.GetByIdAsync(maintenanceId); + if (maintenanceRequest != null) + { + selectedMaintenanceRequest = maintenanceRequest; + } + else + { + ToastService.ShowError("Maintenance request not found"); + } + } + + private void CloseModal() + { + selectedEvent = null; + selectedTour = null; + selectedInspection = null; + selectedMaintenanceRequest = null; + } + + private string GetInspectionStatusBadgeClass(string status) => status switch + { + "Good" => "bg-success", + "Fair" => "bg-warning text-dark", + "Poor" => "bg-danger", + "Completed" => "bg-success", + _ => "bg-secondary" + }; + + private void NavigateToEventDetail() + { + if (selectedEvent == null) return; + + // For tours, navigate to checklist if available + if (selectedEvent.SourceEntityType == nameof(Tour) && selectedTour != null) + { + if (selectedTour.ChecklistId.HasValue) + { + Navigation.NavigateTo($"/PropertyManagement/Checklists/View/{selectedTour.ChecklistId.Value}"); + } + else + { + ToastService.ShowWarning("No checklist found for this tour"); + } + return; + } + + // For other event types, use the router + if (CalendarEventRouter.IsRoutable(selectedEvent)) + { + var route = CalendarEventRouter.GetRouteForEvent(selectedEvent); + if (!string.IsNullOrEmpty(route)) + { + Navigation.NavigateTo(route); + } + } + } + + private async Task CompleteTour(Guid tourId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + if (organizationId.HasValue) + { + var tour = await TourService.GetByIdAsync(tourId); + if (tour != null) + { + CloseModal(); + if (tour.ChecklistId.HasValue) + { + Navigation.NavigateTo($"/PropertyManagement/Checklists/View/{tour.ChecklistId.Value}"); + } + else + { + ToastService.ShowWarning("No property tour checklist found for this tour"); + } + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error completing tour: {ex.Message}"); + } + } + + private async Task CancelTour(Guid tourId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + await TourService.CancelTourAsync(tourId); + ToastService.ShowSuccess("Tour cancelled successfully"); + CloseModal(); + await LoadEvents(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error cancelling tour: {ex.Message}"); + } + } + + private async Task MarkTourAsNoShow(Guid tourId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + await TourService.MarkTourAsNoShowAsync(tourId); + ToastService.ShowSuccess("Tour marked as No Show"); + CloseModal(); + await LoadEvents(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error marking tour as no show: {ex.Message}"); + } + } + + private async Task StartWork(Guid maintenanceRequestId) + { + try + { + var request = await MaintenanceService.GetByIdAsync(maintenanceRequestId); + if (request != null) + { + request.Status = ApplicationConstants.MaintenanceRequestStatuses.InProgress; + + await MaintenanceService.UpdateAsync(request); + ToastService.ShowSuccess("Work started on maintenance request"); + + await ShowMaintenanceRequestDetailById(maintenanceRequestId); + await LoadEvents(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error starting work: {ex.Message}"); + } + } + + private async Task CompleteMaintenanceRequest(Guid maintenanceRequestId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + var request = await MaintenanceService.GetByIdAsync(maintenanceRequestId); + if (request != null) + { + request.Status = ApplicationConstants.MaintenanceRequestStatuses.Completed; + + await MaintenanceService.UpdateAsync(request); + ToastService.ShowSuccess("Maintenance request marked as complete"); + + await ShowMaintenanceRequestDetailById(maintenanceRequestId); + await LoadEvents(); + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error completing request: {ex.Message}"); + } + } + + private async Task CancelMaintenanceRequest(Guid maintenanceRequestId) + { + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId.HasValue && !string.IsNullOrEmpty(userId)) + { + var request = await MaintenanceService.GetByIdAsync(maintenanceRequestId); + if (request != null) + { + request.Status = ApplicationConstants.MaintenanceRequestStatuses.Cancelled; + + await MaintenanceService.UpdateAsync(request); + ToastService.ShowSuccess("Maintenance request cancelled"); + CloseModal(); + await LoadEvents(); + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error cancelling request: {ex.Message}"); + } + } + + private string GetInterestBadgeClass(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "bg-success", + var l when l == ApplicationConstants.TourInterestLevels.Interested => "bg-primary", + var l when l == ApplicationConstants.TourInterestLevels.Neutral => "bg-secondary", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "bg-danger", + _ => "bg-secondary" + }; + + private string GetInterestDisplay(string? level) => level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "Very Interested", + var l when l == ApplicationConstants.TourInterestLevels.Interested => "Interested", + var l when l == ApplicationConstants.TourInterestLevels.Neutral => "Neutral", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "Not Interested", + _ => "Unknown" + }; + + private string GetPriorityBadgeClass(string priority) => priority switch + { + "High" => "bg-danger", + "Medium" => "bg-warning text-dark", + "Low" => "bg-info", + _ => "bg-secondary" + }; + + private string GetEventStatusBadgeClass(string status) => status switch + { + "Scheduled" => "bg-info", + "Completed" => "bg-success", + "Cancelled" => "bg-danger", + "NoShow" => "bg-warning text-dark", + "In Progress" => "bg-primary", + "Pending" => "bg-warning text-dark", + "Overdue" => "bg-danger", + _ => "bg-secondary" + }; +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Checklists.razor b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Checklists.razor new file mode 100644 index 0000000..55b52bd --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Checklists.razor @@ -0,0 +1,176 @@ +@page "/propertymanagement/checklists" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@inject ChecklistService ChecklistService +@inject UserContextService UserContext +@inject NavigationManager NavigationManager +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + +@rendermode InteractiveServer + +Available Checklists + +
+
+
+

Available Checklists

+

Select a checklist template to complete for your property

+
+
+
+ + +
+
+
+ + @if (errorMessage != null) + { + + } + + @if (templates == null) + { +
+
+ Loading... +
+
+ } + else if (!templates.Any()) + { +
+ No checklist templates available. Contact your administrator to create templates. +
+ } + else + { + +
+
+ +
+
+ +
+
+ + +
+ @foreach (var template in FilteredTemplates) + { +
+
+
+
+ @template.Name +
+
+
+

@(template.Description ?? "No description provided")

+ +
+ @template.Category +
+ +
+ @(template.Items?.Count ?? 0) items + @if (template.Items != null && template.Items.Any(i => i.RequiresValue)) + { + @template.Items.Count(i => i.RequiresValue) need values + } +
+
+ +
+
+ } +
+ } +
+ +@code { + private List? templates; + private string? errorMessage; + private string searchText = ""; + private string filterCategory = ""; + + private IEnumerable FilteredTemplates + { + get + { + if (templates == null) return Enumerable.Empty(); + + var filtered = templates.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(searchText)) + { + filtered = filtered.Where(t => + t.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + (t.Description?.Contains(searchText, StringComparison.OrdinalIgnoreCase) ?? false)); + } + + if (!string.IsNullOrWhiteSpace(filterCategory)) + { + filtered = filtered.Where(t => t.Category == filterCategory); + } + + return filtered; + } + } + + protected override async Task OnInitializedAsync() + { + await LoadTemplates(); + } + + private async Task LoadTemplates() + { + try + { + templates = await ChecklistService.GetChecklistTemplatesAsync(); + } + catch (Exception ex) + { + errorMessage = $"Error loading templates: {ex.Message}"; + } + } + + private void StartChecklist(Guid templateId) + { + // Navigate to complete page with template ID - checklist will be created on save + NavigationManager.NavigateTo($"/propertymanagement/checklists/complete/new?templateId={templateId}"); + } + + private void NavigateToMyChecklists() + { + NavigationManager.NavigateTo("/propertymanagement/checklists/mychecklists"); + } + + private void NavigateToTemplates() + { + NavigationManager.NavigateTo("/propertymanagement/checklists/templates"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Complete.razor b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Complete.razor new file mode 100644 index 0000000..c1af2ad --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Complete.razor @@ -0,0 +1,713 @@ +@page "/propertymanagement/checklists/complete/{ChecklistId:guid}" +@page "/propertymanagement/checklists/complete/new" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components + +@inject ChecklistService ChecklistService +@inject PropertyService PropertyService +@inject LeaseService LeaseService +@inject UserContextService UserContext +@inject ToastService ToastService +@inject NavigationManager NavigationManager +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Complete Checklist + +@if (checklist == null) +{ +
+
+ Loading... +
+
+} +else +{ +
+

Complete Checklist

+
+ +
+
+ + @if (!string.IsNullOrEmpty(successMessage)) + { +
+ @successMessage + +
+ } + + @if (!string.IsNullOrEmpty(errorMessage)) + { +
+ @errorMessage + +
+ } + +
+
+ + + + + @if (!checklist.PropertyId.HasValue) + { +
+
+
Property and Lease Required
+
+
+

This checklist must be assigned to a property before it can be completed.

+
+
+ + +
+
+ + + @if (requiresLease && selectedLeaseId == Guid.Empty) + { + This checklist type requires a lease selection + } +
+
+ +
+
+ } + else + { + +
+
+
Property Information
+
+
+ @if (checklist.Property != null) + { +

@checklist.Property.Address

+

@checklist.Property.City, @checklist.Property.State @checklist.Property.ZipCode

+ } + @if (checklist.Lease != null) + { +
+

Lease: @(checklist.Lease.Tenant?.FirstName ?? "Unknown") @(checklist.Lease.Tenant?.LastName ?? "") (Starts: @checklist.Lease.StartDate.ToString("MM/dd/yyyy"))

+ } +
+
+ } + + +
+
+
Checklist Details
+
+
+
+
+ Name: +

@checklist.Name

+
+
+ Type: +

@checklist.ChecklistType

+
+
+
+
+ + + @if (checklist.Items != null && checklist.Items.Any()) + { + var groupedItems = checklist.Items + .OrderBy(i => i.SectionOrder) + .ThenBy(i => i.ItemOrder) + .GroupBy(i => i.CategorySection ?? "General"); + + @foreach (var group in groupedItems) + { +
+
+
@group.Key
+ +
+
+ @foreach (var item in group) + { +
+
+
+
+ + +
+ @if (item.RequiresValue || RequiresValueByKeyword(item.ItemText)) + { +
+ @if (IsInterestLevelItem(item.ItemText)) + { +
+ @foreach (var level in ApplicationConstants.TourInterestLevels.AllTourInterestLevels) + { + + + } +
+ } + else + { + + } +
+ } +
+ @*
+ + +
*@ +
+ @if (!string.IsNullOrEmpty(item.PhotoUrl)) + { +
+ Item photo +
+ } +
+ } +
+
+ } + + +
+
+
General Notes
+
+
+ + + + Use this section for overall comments. Individual item notes can be added above. + +
+
+ + +
+
+
+ + +
+
+
+ } +
+
+ + +
+
+
+
Progress
+
+
+ @if (checklist.Items != null && checklist.Items.Any()) + { + var totalItems = checklist.Items.Count; + var checkedItems = checklist.Items.Count(i => i.IsChecked); + var itemsWithValues = checklist.Items.Count(i => !string.IsNullOrEmpty(i.Value)); + var itemsWithNotes = checklist.Items.Count(i => !string.IsNullOrEmpty(i.Notes)); + var progressPercent = totalItems > 0 ? (int)((checkedItems * 100.0) / totalItems) : 0; + +
+
+ Checked Items + @checkedItems / @totalItems +
+
+
+ @progressPercent% +
+
+
+ +
+ +
+ Checked: @checkedItems +
+
+ Unchecked: @(totalItems - checkedItems) +
+
+ With Values: @itemsWithValues +
+
+ With Notes: @itemsWithNotes +
+ +
+ +
+ + + Check items as you complete them. Add values (readings, amounts) and notes as needed. + +
+ } +
+
+
+
+} + +@code { + [Parameter] + public Guid ChecklistId { get; set; } + + [SupplyParameterFromQuery(Name = "templateId")] + public Guid? TemplateId { get; set; } + + private Checklist? checklist; + private ChecklistTemplate? template; + private bool isNewChecklist = false; + private List properties = new(); + private List leases = new(); + private List checklistItems = new(); + private Guid selectedPropertyId = Guid.Empty; + private Guid selectedLeaseId = Guid.Empty; + private bool requiresLease = false; + private string? successMessage; + private string? errorMessage; + private bool isSaving = false; + private Dictionary modifiedItems = new(); + + protected override async Task OnInitializedAsync() + { + await LoadProperties(); + + if (TemplateId.HasValue) + { + // New checklist from template + await LoadTemplateForNewChecklist(); + } + else if (ChecklistId != Guid.Empty) + { + // Existing checklist + await LoadChecklist(); + } + } + + private async Task LoadTemplateForNewChecklist() + { + try + { + template = await ChecklistService.GetChecklistTemplateByIdAsync(TemplateId!.Value); + + if (template == null) + { + errorMessage = "Template not found."; + return; + } + + isNewChecklist = true; + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + // Create a temporary checklist object (not saved to DB yet) + checklist = new Checklist + { + Id = Guid.NewGuid(), + Name = template.Name, + ChecklistType = template.Category, + ChecklistTemplateId = template.Id, + Status = ApplicationConstants.ChecklistStatuses.Draft, + OrganizationId = organizationId!.Value, + CreatedBy = userId!, + CreatedOn = DateTime.UtcNow + }; + + // Copy template items to working list + checklistItems = template.Items.Select(ti => new ChecklistItem + { + Id = Guid.NewGuid(), + ItemText = ti.ItemText, + ItemOrder = ti.ItemOrder, + CategorySection = ti.CategorySection, + SectionOrder = ti.SectionOrder, + RequiresValue = ti.RequiresValue, + IsChecked = false, + OrganizationId = organizationId!.Value, + }).ToList(); + + // Set Items collection for display + checklist.Items = checklistItems; + + requiresLease = checklist.ChecklistType == ApplicationConstants.ChecklistTypes.MoveIn || + checklist.ChecklistType == ApplicationConstants.ChecklistTypes.MoveOut; + } + catch (Exception ex) + { + errorMessage = $"Error loading template: {ex.Message}"; + } + } + + private async Task LoadProperties() + { + try + { + properties = await PropertyService.GetAllAsync(); + } + catch (Exception ex) + { + errorMessage = $"Error loading properties: {ex.Message}"; + } + } + + private async Task LoadChecklist() + { + try + { + checklist = await ChecklistService.GetChecklistByIdAsync(ChecklistId); + + if (checklist == null) + { + errorMessage = "Checklist not found."; + return; + } + + // Check if this type requires a lease + requiresLease = checklist.ChecklistType == ApplicationConstants.ChecklistTypes.MoveIn || + checklist.ChecklistType == ApplicationConstants.ChecklistTypes.MoveOut; + + // If checklist is already completed, redirect to view page + if (checklist.Status == ApplicationConstants.ChecklistStatuses.Completed) + { + NavigationManager.NavigateTo($"/propertymanagement/checklists/view/{ChecklistId}"); + } + } + catch (Exception ex) + { + errorMessage = $"Error loading checklist: {ex.Message}"; + } + } + + private async Task OnPropertyChanged() + { + if (selectedPropertyId != Guid.Empty) + { + leases = await LeaseService.GetCurrentAndUpcomingLeasesByPropertyIdAsync(selectedPropertyId); + } + else + { + leases.Clear(); + selectedLeaseId = Guid.Empty; + } + } + + private async Task AssignPropertyAndLease() + { + if (checklist == null) return; + + try + { + isSaving = true; + errorMessage = null; + + checklist.PropertyId = selectedPropertyId != Guid.Empty ? selectedPropertyId : null; + checklist.LeaseId = selectedLeaseId != Guid.Empty ? selectedLeaseId : null; + + if (isNewChecklist) + { + // Create the checklist and persist items + var savedChecklist = await ChecklistService.AddChecklistAsync(checklist); + + // Add any in-memory items to the database + foreach (var item in checklistItems) + { + item.ChecklistId = savedChecklist.Id; + await ChecklistService.AddChecklistItemAsync(item); + } + + ChecklistId = savedChecklist.Id; + isNewChecklist = false; + } + else + { + await ChecklistService.UpdateChecklistAsync(checklist); + } + + await LoadChecklist(); // Reload to get navigation properties + + successMessage = "Property and lease assigned successfully."; + } + catch (Exception ex) + { + errorMessage = $"Error assigning property: {ex.Message}"; + } + finally + { + isSaving = false; + } + } + + private void ToggleItemChecked(ChecklistItem item, bool isChecked) + { + item.IsChecked = isChecked; + OnItemChanged(item); + } + + private void OnItemChanged(ChecklistItem item) + { + if (!modifiedItems.ContainsKey(item.Id)) + { + modifiedItems[item.Id] = item; + } + } + + private void CheckAllInSection(string? sectionName) + { + if (checklist?.Items == null) return; + + var itemsInSection = checklist.Items + .Where(i => (i.CategorySection ?? "General") == (sectionName ?? "General")) + .ToList(); + + foreach (var item in itemsInSection) + { + item.IsChecked = true; + OnItemChanged(item); + } + + StateHasChanged(); + } + + private bool RequiresValueByKeyword(string itemText) + { + var lowerText = itemText.ToLower(); + return lowerText.Contains("meter reading") || + lowerText.Contains("reading recorded") || + lowerText.Contains("deposit") || + lowerText.Contains("amount") || + lowerText.Contains("forwarding address") || + lowerText.Contains("address obtained"); + } + + private bool IsInterestLevelItem(string itemText) + { + var lowerText = itemText.ToLower(); + return lowerText.Contains("interest level"); + } + + private string GetInterestLevelDisplay(string level) + { + return level switch + { + var l when l == ApplicationConstants.TourInterestLevels.VeryInterested => "Very Interested", + var l when l == ApplicationConstants.TourInterestLevels.NotInterested => "Not Interested", + _ => level + }; + } + + private string GetValuePlaceholder(string itemText) + { + var lowerText = itemText.ToLower(); + if (lowerText.Contains("electric") || lowerText.Contains("electricity")) + return "e.g., 12345 kWh"; + if (lowerText.Contains("gas")) + return "e.g., 5678 CCF"; + if (lowerText.Contains("water")) + return "e.g., 9012 gal"; + if (lowerText.Contains("deposit")) + return "e.g., $1500"; + if (lowerText.Contains("address")) + return "e.g., 123 Main St, City, ST 12345"; + return "Enter value"; + } + + private async Task SaveProgress() + { + if (checklist == null) return; + + isSaving = true; + errorMessage = null; + successMessage = null; + + try + { + // If this is a new checklist, create it first + if (isNewChecklist) + { + // Create the checklist + var savedChecklist = await ChecklistService.AddChecklistAsync(checklist); + + // Add the items + foreach (var item in checklistItems) + { + item.ChecklistId = savedChecklist.Id; + await ChecklistService.AddChecklistItemAsync(item); + } + + // Update local reference and flag + ChecklistId = savedChecklist.Id; + isNewChecklist = false; + + // Reload to get full entity with navigation properties + await LoadChecklist(); + + successMessage = "Checklist created and saved successfully."; + } + else + { + // Update checklist status if it's still draft + if (checklist.Status == ApplicationConstants.ChecklistStatuses.Draft) + { + checklist.Status = ApplicationConstants.ChecklistStatuses.InProgress; + await ChecklistService.UpdateChecklistAsync(checklist); + } + + // Save all modified items + foreach (var item in modifiedItems.Values) + { + await ChecklistService.UpdateChecklistItemAsync(item); + } + + modifiedItems.Clear(); + successMessage = "Progress saved successfully."; + } + } + catch (Exception ex) + { + errorMessage = $"Error saving progress: {$"{ex.Message} - {ex.InnerException?.Message}"}"; + ToastService.ShowError(errorMessage); + } + finally + { + isSaving = false; + } + } + + private async Task MarkAsComplete() + { + if (checklist == null) return; + + isSaving = true; + errorMessage = null; + successMessage = null; + + try + { + // If this is a new checklist, create it first + if (isNewChecklist) + { + // Create the checklist + var savedChecklist = await ChecklistService.AddChecklistAsync(checklist); + + // Add the items + foreach (var item in checklistItems) + { + item.ChecklistId = savedChecklist.Id; + await ChecklistService.AddChecklistItemAsync(item); + } + + ChecklistId = savedChecklist.Id; + isNewChecklist = false; + } + else + { + // Save any pending changes first + foreach (var item in modifiedItems.Values) + { + await ChecklistService.UpdateChecklistItemAsync(item); + } + modifiedItems.Clear(); + } + + // Complete the checklist + await ChecklistService.CompleteChecklistAsync(ChecklistId); + + successMessage = "Checklist completed successfully."; + + // Redirect to view page after a short delay + await Task.Delay(1500); + NavigationManager.NavigateTo($"/propertymanagement/checklists/view/{ChecklistId}"); + } + catch (Exception ex) + { + errorMessage = $"Error completing checklist: {$"{ex.Message} - {ex.InnerException?.Message}"}"; + ToastService.ShowError(errorMessage); + isSaving = false; + } + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/checklists"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Create.razor b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Create.razor new file mode 100644 index 0000000..69a32ce --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Create.razor @@ -0,0 +1,352 @@ +@page "/propertymanagement/checklists/create" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components + +@inject ChecklistService ChecklistService +@inject UserContextService UserContext +@inject NavigationManager NavigationManager +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Create Checklist + +
+
+

Create Checklist

+ +
+ + @if (errorMessage != null) + { + + } + + @if (successMessage != null) + { + + } + + @if (loading) + { +
+
+ Loading... +
+
+ } + else + { + + + + +
+
+ +
+
+
Checklist Information
+
+
+
+
+ + +
+
+ + +
+
+ +
+ + Property and lease will be assigned when you complete this checklist. +
+
+
+ + + @if (!checklistItems.Any() && selectedTemplateId != Guid.Empty) + { +
+
+ +

This template has no items. Click below to add your custom items.

+ +
+
+ } + else if (checklistItems.Any()) + { +
+
+
+
Checklist Items
+ +
+
+
+ @{ + var sections = checklistItems + .OrderBy(i => i.SectionOrder) + .ThenBy(i => i.ItemOrder) + .GroupBy(i => i.CategorySection ?? "General"); + } + + @foreach (var itemSection in sections) + { +
@itemSection.Key
+ @foreach (var item in itemSection) + { +
+
+
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+
+ } + } +
+
+ } + + +
+ + +
+
+ + +
+
+
+
Template Information
+
+
+ @if (selectedTemplate != null) + { +

@selectedTemplate.Name

+ @if (!string.IsNullOrEmpty(selectedTemplate.Description)) + { +

@selectedTemplate.Description

+ } +

+ + Type: @selectedTemplate.Category
+ Items: @selectedTemplate.Items.Count +
+

+ } + else + { +

Select a template to view details

+ } +
+
+
+
+
+ } +
+ +@code { + private Checklist checklist = new(); + private List templates = new(); + private List checklistItems = new(); + + private Guid selectedTemplateId = Guid.Empty; + private ChecklistTemplate? selectedTemplate; + private bool loading = true; + private bool isSaving = false; + private string? errorMessage; + private string? successMessage; + + [SupplyParameterFromQuery(Name = "templateId")] + public Guid? TemplateIdFromQuery { get; set; } + + protected override async Task OnInitializedAsync() + { + try + { + loading = true; + + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + if (organizationId == null || string.IsNullOrEmpty(userId)) + { + errorMessage = "Unable to determine user context. Please log in again."; + return; + } + + // Set initial status + checklist.Status = ApplicationConstants.ChecklistStatuses.Draft; + + // Load templates + templates = await ChecklistService.GetChecklistTemplatesAsync(); + + // Pre-select template if provided in query string + if (TemplateIdFromQuery.HasValue) + { + selectedTemplateId = TemplateIdFromQuery.Value; + await OnTemplateChanged(); + } + } + catch (Exception ex) + { + errorMessage = $"Error loading data: {ex.Message}"; + } + finally + { + loading = false; + } + } + + private async Task OnTemplateChanged() + { + if (selectedTemplateId == Guid.Empty) + { + selectedTemplate = null; + checklistItems.Clear(); + return; + } + + if (selectedTemplateId.ToString() == (Guid.Empty + "1").ToString()) // Manage Templates option + { + NavigationManager.NavigateTo("/propertymanagement/checklists/templates"); + return; + } + + selectedTemplate = await ChecklistService.GetChecklistTemplateByIdAsync(selectedTemplateId); + if (selectedTemplate != null) + { + checklist.ChecklistTemplateId = selectedTemplate.Id; + checklist.ChecklistType = selectedTemplate.Category; + checklist.Name = selectedTemplate.Name; + + // Copy template items to checklist items + checklistItems = selectedTemplate.Items.Select(ti => new ChecklistItem + { + Id = Guid.NewGuid(), + ItemText = ti.ItemText, + ItemOrder = ti.ItemOrder, + CategorySection = ti.CategorySection, + SectionOrder = ti.SectionOrder, + RequiresValue = ti.RequiresValue, + OrganizationId = checklist.OrganizationId + }).ToList(); + } + } + + private void AddCustomItem() + { + var maxOrder = checklistItems.Any() ? checklistItems.Max(i => i.ItemOrder) : 0; + var maxSectionOrder = checklistItems.Any() ? checklistItems.Max(i => i.SectionOrder) : 0; + checklistItems.Add(new ChecklistItem + { + Id = Guid.NewGuid(), + ItemText = "", + ItemOrder = maxOrder + 1, + CategorySection = "Custom", + SectionOrder = maxSectionOrder, + OrganizationId = checklist.OrganizationId + }); + } + + private void RemoveItem(ChecklistItem item) + { + checklistItems.Remove(item); + } + + private async Task SaveChecklist() + { + try + { + isSaving = true; + errorMessage = null; + + // Create the checklist + var savedChecklist = await ChecklistService.AddChecklistAsync(checklist); + + // Add the items + foreach (var item in checklistItems) + { + item.ChecklistId = savedChecklist.Id; + await ChecklistService.AddChecklistItemAsync(item); + } + + successMessage = "Checklist created successfully!"; + await Task.Delay(500); + NavigationManager.NavigateTo($"/propertymanagement/checklists/complete/{savedChecklist.Id}"); + } + catch (Exception ex) + { + errorMessage = $"Error saving checklist: {ex.Message}"; + } + finally + { + isSaving = false; + } + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/checklists"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/EditTemplate.razor b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/EditTemplate.razor new file mode 100644 index 0000000..eac1562 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/EditTemplate.razor @@ -0,0 +1,344 @@ +@page "/propertymanagement/checklists/templates/edit/{TemplateId:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components + +@inject ChecklistService ChecklistService +@inject UserContextService UserContext +@inject NavigationManager NavigationManager +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Edit Template + +
+
+

Edit Template

+ +
+ + @if (errorMessage != null) + { + + } + + @if (successMessage != null) + { + + } + + @if (loading) + { +
+
+ Loading... +
+
+ } + else if (template == null) + { +
+ Template not found. +
+ } + else if (template.IsSystemTemplate && !isAdmin) + { +
+ System templates can only be edited by Administrators. Please create a copy instead. +
+ + } + else + { + + + + +
+
+ +
+
+
Template Information
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + + @if (!templateItems.Any()) + { +
+
+ +

This template has no items. Click below to add items.

+ +
+
+ } + else + { +
+
+
+
Template Items (@templateItems.Count)
+ +
+
+
+ @{ + var sections = templateItems.GroupBy(i => i.CategorySection ?? "General").OrderBy(g => g.Key); + } + + @foreach (var itemSection in sections) + { +
@itemSection.Key
+ @foreach (var item in itemSection.OrderBy(i => i.SectionOrder).ThenBy(i => i.ItemOrder)) + { +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+
+ } + } +
+
+ } + + +
+ + +
+
+ + +
+
+
+
Template Summary
+
+
+

@template.Name

+

+ + Category: @template.Category
+ Total Items: @templateItems.Count
+ Required: @templateItems.Count(i => i.IsRequired)
+ Needs Value: @templateItems.Count(i => i.RequiresValue) +
+

+ @if (templateItems.Any()) + { + var sectionCount = templateItems.GroupBy(i => i.CategorySection ?? "General").Count(); +

+ Sections: @sectionCount +

+ } +
+
+
+
+
+ } +
+ +@code { + [Parameter] + public Guid TemplateId { get; set; } + + private ChecklistTemplate? template; + private List templateItems = new(); + private List deletedItemIds = new(); + + private bool loading = true; + private bool isSaving = false; + private bool isAdmin = false; + private string? errorMessage; + private string? successMessage; + + protected override async Task OnInitializedAsync() + { + try + { + loading = true; + + isAdmin = await UserContext.IsInRoleAsync(ApplicationConstants.DefaultAdminRole); + template = await ChecklistService.GetChecklistTemplateByIdAsync(TemplateId); + + if (template != null && template.Items != null) + { + // Use the loaded items directly (they are already tracked by EF) + templateItems = template.Items.ToList(); + } + } + catch (Exception ex) + { + errorMessage = $"Error loading template: {ex.Message}"; + } + finally + { + loading = false; + } + } + + private void AddItem() + { + if (template == null) return; + + var maxOrder = templateItems.Any() ? templateItems.Max(i => i.ItemOrder) : 0; + templateItems.Add(new ChecklistTemplateItem + { + ChecklistTemplateId = template.Id, + ItemText = "", + ItemOrder = maxOrder + 1, + CategorySection = "General", + SectionOrder = 0, + IsRequired = false, + RequiresValue = false, + AllowsNotes = true, + OrganizationId = template.OrganizationId + }); + } + + private void RemoveItem(ChecklistTemplateItem item) + { + // Track deleted items that exist in database + if (item.Id != Guid.Empty) + { + deletedItemIds.Add(item.Id); + } + templateItems.Remove(item); + } + + private async Task SaveTemplate() + { + if (template == null) return; + + try + { + isSaving = true; + errorMessage = null; + + // Update template basic info + await ChecklistService.UpdateChecklistTemplateAsync(template); + + // Delete removed items first + foreach (var deletedId in deletedItemIds) + { + await ChecklistService.DeleteChecklistTemplateItemAsync(deletedId); + } + deletedItemIds.Clear(); + + // Process items: separate new items from existing ones + var existingItems = templateItems.Where(i => i.Id != Guid.Empty).ToList(); + var newItems = templateItems.Where(i => i.Id == Guid.Empty).ToList(); + + // Update existing items + foreach (var item in existingItems) + { + await ChecklistService.UpdateChecklistTemplateItemAsync(item); + } + + // Add new items + foreach (var item in newItems) + { + item.ChecklistTemplateId = template.Id; + var addedItem = await ChecklistService.AddChecklistTemplateItemAsync(item); + // Update the local item with the new ID + item.Id = addedItem.Id; + } + + successMessage = "Template updated successfully!"; + await Task.Delay(1000); + //NavigationManager.NavigateTo("/propertymanagement/checklists/templates"); + } + catch (Exception ex) + { + errorMessage = $"Error saving template: {ex.Message}"; + } + finally + { + isSaving = false; + } + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/checklists/templates"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor new file mode 100644 index 0000000..da78b81 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor @@ -0,0 +1,368 @@ +@page "/propertymanagement/checklists/mychecklists" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components +@inject ChecklistService ChecklistService +@inject UserContextService UserContext +@inject NavigationManager NavigationManager +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + +@rendermode InteractiveServer + +My Checklists + +
+
+
+

My Checklists

+

Manage your created checklists

+
+
+
+ + +
+
+
+ + @if (errorMessage != null) + { + + } + + @if (successMessage != null) + { + + } + + + @if (showDeleteConfirmation && checklistToDelete != null) + { + + } + + @if (checklists == null) + { +
+
+ Loading... +
+
+ } + else if (!checklists.Any()) + { +
+ No checklists found. Click "New Checklist" to create one from a template. +
+ } + else + { + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ + + + + + + + + + + + + + @foreach (var checklist in FilteredChecklists) + { + + + + + + + + + + } + +
NameTypePropertyStatusProgressCreatedActions
+ @checklist.Name + + @checklist.ChecklistType + + @if (checklist.Property != null) + { + @checklist.Property.Address + } + else + { + Not assigned + } + + @checklist.Status + + @if (checklist.Items != null && checklist.Items.Any()) + { + var total = checklist.Items.Count; + var completed = checklist.Items.Count(i => i.IsChecked); + var percent = total > 0 ? (int)((completed * 100.0) / total) : 0; +
+
+ @percent% +
+
+ } + else + { + - + } +
+ @checklist.CreatedOn.ToString("MM/dd/yyyy") + +
+ @if (checklist.Status == ApplicationConstants.ChecklistStatuses.Completed) + { + + } + else + { + + } + +
+
+
+
+
+ } +
+ +@code { + private List? checklists; + private string? errorMessage; + private string? successMessage; + private string searchText = ""; + private string filterStatus = ""; + private string filterType = ""; + private bool showDeleteConfirmation = false; + private bool isDeleting = false; + private Checklist? checklistToDelete = null; + + private IEnumerable FilteredChecklists + { + get + { + if (checklists == null) return Enumerable.Empty(); + + var filtered = checklists.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(searchText)) + { + filtered = filtered.Where(c => + c.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase) || + (c.Property?.Address?.Contains(searchText, StringComparison.OrdinalIgnoreCase) ?? false)); + } + + if (!string.IsNullOrWhiteSpace(filterStatus)) + { + filtered = filtered.Where(c => c.Status == filterStatus); + } + + if (!string.IsNullOrWhiteSpace(filterType)) + { + filtered = filtered.Where(c => c.ChecklistType == filterType); + } + + return filtered; + } + } + + protected override async Task OnInitializedAsync() + { + await LoadChecklists(); + } + + private async Task LoadChecklists() + { + try + { + checklists = await ChecklistService.GetChecklistsAsync(includeArchived: false); + } + catch (Exception ex) + { + errorMessage = $"Error loading checklists: {ex.Message}"; + } + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + var s when s == ApplicationConstants.ChecklistStatuses.Completed => "bg-success", + var s when s == ApplicationConstants.ChecklistStatuses.InProgress => "bg-warning text-dark", + var s when s == ApplicationConstants.ChecklistStatuses.Draft => "bg-secondary", + _ => "bg-secondary" + }; + } + + private void CreateNewChecklist() + { + NavigationManager.NavigateTo("/propertymanagement/checklists/create"); + } + + private void EditChecklist(Guid checklistId) + { + NavigationManager.NavigateTo($"/propertymanagement/checklists/complete/{checklistId}"); + } + + private void ViewChecklist(Guid checklistId) + { + NavigationManager.NavigateTo($"/propertymanagement/checklists/view/{checklistId}"); + } + + private void ShowDeleteConfirmation(Checklist checklist) + { + checklistToDelete = checklist; + showDeleteConfirmation = true; + errorMessage = null; + successMessage = null; + } + + private void CloseDeleteConfirmation() + { + showDeleteConfirmation = false; + checklistToDelete = null; + } + + private async Task DeleteChecklist() + { + if (checklistToDelete == null) return; + + try + { + isDeleting = true; + errorMessage = null; + + // If completed, use soft delete (archive) + if (checklistToDelete.Status == ApplicationConstants.ChecklistStatuses.Completed) + { + await ChecklistService.ArchiveChecklistAsync(checklistToDelete.Id); + successMessage = $"Checklist '{checklistToDelete.Name}' has been archived."; + } + else + { + // If not completed, use hard delete + await ChecklistService.DeleteChecklistAsync(checklistToDelete.Id); + successMessage = $"Checklist '{checklistToDelete.Name}' has been deleted."; + } + + // Reload the list + await LoadChecklists(); + + CloseDeleteConfirmation(); + } + catch (Exception ex) + { + errorMessage = $"Error deleting checklist: {ex.Message}"; + } + finally + { + isDeleting = false; + } + } + + private void NavigateToTemplates() + { + NavigationManager.NavigateTo("/propertymanagement/checklists/templates"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Templates.razor b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Templates.razor new file mode 100644 index 0000000..61d450e --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Templates.razor @@ -0,0 +1,349 @@ +@page "/propertymanagement/checklists/templates" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components +@inject ChecklistService ChecklistService +@inject NavigationManager NavigationManager +@inject UserContextService UserContext +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + +@rendermode InteractiveServer + +Checklist Templates + +
+
+
+

Checklist Templates

+
+
+ +
+
+ + @if (errorMessage != null) + { + + } + + @if (successMessage != null) + { + + } + + + @if (showDeleteConfirmation && templateToDelete != null) + { + + } + + @if (templates == null) + { +
+
+ Loading... +
+
+ } + else if (!templates.Any()) + { +
+ No templates found. +
+ } + else + { + +
+ @foreach (var template in templates) + { +
+
+
+
+
+ @if (template.IsSystemTemplate) + { + + } + @template.Name +
+ @if (template.IsSystemTemplate) + { + System + } + else + { + Custom + } +
+
+
+

@(template.Description ?? "No description provided")

+ +
+ Category: + @template.Category +
+ +
+ Items: @(template.Items?.Count ?? 0) +
+ + @if (template.Items != null && template.Items.Any()) + { + var requiredCount = template.Items.Count(i => i.IsRequired); + var valueCount = template.Items.Count(i => i.RequiresValue); + + @if (requiredCount > 0) + { +
+ @requiredCount required +
+ } + @if (valueCount > 0) + { +
+ @valueCount need values +
+ } + } +
+ +
+
+ } +
+ } +
+ +@code { + private List? templates; + private string? errorMessage; + private string? successMessage; + private bool showDeleteConfirmation = false; + private bool isDeleting = false; + private bool isCopying = false; + private bool isAdmin = false; + private ChecklistTemplate? templateToDelete = null; + + protected override async Task OnInitializedAsync() + { + isAdmin = await UserContext.IsInRoleAsync(ApplicationConstants.DefaultAdminRole); + await LoadTemplates(); + } + + private async Task LoadTemplates() + { + try + { + templates = await ChecklistService.GetChecklistTemplatesAsync(); + } + catch (Exception ex) + { + errorMessage = $"Error loading templates: {ex.Message}"; + } + } + + private void NavigateToCreateChecklist() + { + NavigationManager.NavigateTo("/propertymanagement/checklists/create"); + } + + private void NavigateToCreateWithTemplate(Guid templateId) + { + NavigationManager.NavigateTo($"/propertymanagement/checklists/create?templateId={templateId}"); + } + + private void NavigateToEditTemplate(Guid templateId) + { + NavigationManager.NavigateTo($"/propertymanagement/checklists/templates/edit/{templateId}"); + } + + private async Task CopyTemplate(ChecklistTemplate sourceTemplate) + { + try + { + isCopying = true; + errorMessage = null; + + // Create a new template with a copy of the source + var newTemplate = new ChecklistTemplate + { + Name = $"{sourceTemplate.Name} (Copy)", + Description = sourceTemplate.Description, + Category = sourceTemplate.Category, + IsSystemTemplate = false + }; + + var savedTemplate = await ChecklistService.AddChecklistTemplateAsync(newTemplate); + + // Copy all items from source template + if (sourceTemplate.Items != null) + { + foreach (var sourceItem in sourceTemplate.Items) + { + var newItem = new ChecklistTemplateItem + { + ChecklistTemplateId = savedTemplate.Id, + ItemText = sourceItem.ItemText, + ItemOrder = sourceItem.ItemOrder, + CategorySection = sourceItem.CategorySection, + SectionOrder = sourceItem.SectionOrder, + IsRequired = sourceItem.IsRequired, + RequiresValue = sourceItem.RequiresValue, + AllowsNotes = sourceItem.AllowsNotes + }; + + await ChecklistService.AddChecklistTemplateItemAsync(newItem); + } + } + + successMessage = $"Template '{sourceTemplate.Name}' copied successfully!"; + + // Reload the templates list to show the new copy + await LoadTemplates(); + } + catch (Exception ex) + { + errorMessage = $"Error copying template: {ex.Message}"; + } + finally + { + isCopying = false; + } + } + + private void ShowDeleteConfirmation(ChecklistTemplate template) + { + templateToDelete = template; + showDeleteConfirmation = true; + errorMessage = null; + successMessage = null; + } + + private void CloseDeleteConfirmation() + { + showDeleteConfirmation = false; + templateToDelete = null; + } + + private async Task DeleteTemplate() + { + if (templateToDelete == null) return; + + try + { + isDeleting = true; + errorMessage = null; + + await ChecklistService.DeleteChecklistTemplateAsync(templateToDelete.Id); + + successMessage = $"Template '{templateToDelete.Name}' has been deleted."; + + // Reload the list + await LoadTemplates(); + + CloseDeleteConfirmation(); + } + catch (Exception ex) + { + errorMessage = $"Error deleting template: {ex.Message}"; + } + finally + { + isDeleting = false; + } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/View.razor b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/View.razor new file mode 100644 index 0000000..39ce200 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/View.razor @@ -0,0 +1,588 @@ +@page "/propertymanagement/checklists/view/{ChecklistId:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components +@using System.ComponentModel.DataAnnotations +@using Microsoft.JSInterop + +@inject ChecklistService ChecklistService +@inject UserContextService UserContext +@inject NavigationManager NavigationManager +@inject ChecklistPdfGenerator PdfGenerator +@inject Application.Services.DocumentService DocumentService +@inject IJSRuntime JSRuntime +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +View Checklist + +@if (checklist == null) +{ +
+
+ Loading... +
+
+} +else +{ +
+

Checklist Report

+
+ @if (checklist.Status != ApplicationConstants.ChecklistStatuses.Completed) + { + + } + + @if (checklist.DocumentId.HasValue) + { +
+ + +
+ } + else + { + + } + +
+
+ + @if (!string.IsNullOrEmpty(successMessage)) + { +
+ @successMessage + +
+ } + + @if (!string.IsNullOrEmpty(errorMessage)) + { +
+ @errorMessage + +
+ } + + + @if (showSaveTemplateModal) + { + + } + +
+
+ +
+
+
Property Information
+
+
+ @if (checklist.Property != null) + { +

@checklist.Property.Address

+

@checklist.Property.City, @checklist.Property.State @checklist.Property.ZipCode

+ } + @if (checklist.Lease != null) + { +
+

Lease: @(checklist.Lease.Tenant?.FirstName ?? "Unknown") @(checklist.Lease.Tenant?.LastName ?? "") (Starts: @checklist.Lease.StartDate.ToString("MM/dd/yyyy"))

+ } +
+
+ + +
+
+
Checklist Details
+
+
+
+
+ Name: +

@checklist.Name

+
+
+ Type: +

@checklist.ChecklistType

+
+
+ Status: +

@checklist.Status

+
+
+ @if (checklist.Status == ApplicationConstants.ChecklistStatuses.Completed) + { +
+
+ Completed By: +

@checklist.CompletedBy

+
+
+ Completed On: +

@checklist.CompletedOn?.ToString("MMMM dd, yyyy h:mm tt")

+
+
+ } +
+
+ + + @if (checklist.Items != null && checklist.Items.Any()) + { + var groupedItems = checklist.Items + .OrderBy(i => i.SectionOrder) + .ThenBy(i => i.ItemOrder) + .GroupBy(i => i.CategorySection ?? "General"); + + @foreach (var group in groupedItems) + { +
+
+
@group.Key
+
+
+
+ + + + + + + + + + + @foreach (var item in group) + { + + + + + + + } + +
ItemValueNotes
+ @if (item.IsChecked) + { + + } + else + { + + } + @item.ItemText + @if (!string.IsNullOrEmpty(item.Value)) + { + @item.Value + } + else + { + - + } + + @if (!string.IsNullOrEmpty(item.Notes)) + { + @item.Notes + } + else + { + - + } + @if (!string.IsNullOrEmpty(item.PhotoUrl)) + { +
+ Item photo +
+ } +
+
+
+
+ } + } + + + @if (!string.IsNullOrWhiteSpace(checklist.GeneralNotes)) + { +
+
+
General Notes
+
+
+

@checklist.GeneralNotes

+
+
+ } +
+ + +
+
+
+
Summary
+
+
+ @if (checklist.Items != null && checklist.Items.Any()) + { + var totalItems = checklist.Items.Count; + var checkedItems = checklist.Items.Count(i => i.IsChecked); + var itemsWithValues = checklist.Items.Count(i => !string.IsNullOrEmpty(i.Value)); + var itemsWithNotes = checklist.Items.Count(i => !string.IsNullOrEmpty(i.Notes)); + var progressPercent = totalItems > 0 ? (int)((checkedItems * 100.0) / totalItems) : 0; + +
+
+ Completion + @checkedItems / @totalItems +
+
+
+ @progressPercent% +
+
+
+ +
+ +
+ Checked: @checkedItems +
+
+ Unchecked: @(totalItems - checkedItems) +
+
+ With Values: @itemsWithValues +
+
+ With Notes: @itemsWithNotes +
+ } +
+
+
+
+} + +@code { + [Parameter] + public Guid ChecklistId { get; set; } + + private Checklist? checklist; + private string? successMessage; + private string? errorMessage; + private bool showSaveTemplateModal = false; + private bool isSaving = false; + private bool isGeneratingPdf = false; + private SaveTemplateModel saveTemplateModel = new(); + + public class SaveTemplateModel + { + [Required(ErrorMessage = "Template name is required")] + [StringLength(100, ErrorMessage = "Template name must be less than 100 characters")] + public string TemplateName { get; set; } = string.Empty; + + [StringLength(500, ErrorMessage = "Description must be less than 500 characters")] + public string? TemplateDescription { get; set; } + } + + protected override async Task OnInitializedAsync() + { + await LoadChecklist(); + } + + private async Task LoadChecklist() + { + try + { + checklist = await ChecklistService.GetChecklistByIdAsync(ChecklistId); + + if (checklist == null) + { + errorMessage = "Checklist not found."; + } + } + catch (Exception ex) + { + errorMessage = $"Error loading checklist: {ex.Message}"; + } + } + + private void ShowSaveTemplateModal() + { + saveTemplateModel = new SaveTemplateModel(); + showSaveTemplateModal = true; + errorMessage = null; + successMessage = null; + } + + private void CloseSaveTemplateModal() + { + showSaveTemplateModal = false; + saveTemplateModel = new SaveTemplateModel(); + } + + private async Task SaveAsTemplate() + { + try + { + isSaving = true; + errorMessage = null; + + await ChecklistService.SaveChecklistAsTemplateAsync( + ChecklistId, + saveTemplateModel.TemplateName, + saveTemplateModel.TemplateDescription + ); + + successMessage = $"Checklist saved as template '{saveTemplateModel.TemplateName}' successfully!"; + CloseSaveTemplateModal(); + } + catch (InvalidOperationException ex) + { + // Duplicate name error + errorMessage = ex.Message; + } + catch (Exception ex) + { + errorMessage = $"Error saving template: {ex.Message}"; + } + finally + { + isSaving = false; + } + } + + private async Task HandlePdfAction() + { + if (checklist == null) return; + + if (checklist.DocumentId.HasValue) + { + // View existing PDF + await ViewPdf(); + } + else + { + // Generate new PDF + await GeneratePdf(); + } + } + + private async Task DownloadPdf() + { + if (checklist?.DocumentId == null) return; + + try + { + isGeneratingPdf = true; + errorMessage = null; + + var document = await DocumentService.GetByIdAsync(checklist.DocumentId.Value); + if (document != null) + { + var filename = $"Checklist_{checklist.Name.Replace(" ", "_")}_{DateTime.Now:yyyyMMdd}.pdf"; + await JSRuntime.InvokeVoidAsync("downloadFile", filename, Convert.ToBase64String(document.FileData), document.FileType); + } + else + { + errorMessage = "PDF document not found."; + } + } + catch (Exception ex) + { + errorMessage = $"Error downloading PDF: {ex.Message}"; + } + finally + { + isGeneratingPdf = false; + } + } + + private async Task ViewPdf() + { + if (checklist?.DocumentId == null) return; + + try + { + isGeneratingPdf = true; + errorMessage = null; + + var document = await DocumentService.GetByIdAsync(checklist.DocumentId.Value); + if (document != null) + { + await JSRuntime.InvokeVoidAsync("viewFile", Convert.ToBase64String(document.FileData), document.FileType); + } + else + { + errorMessage = "PDF document not found."; + } + } + catch (Exception ex) + { + errorMessage = $"Error viewing PDF: {ex.Message}"; + } + finally + { + isGeneratingPdf = false; + } + } + + private async Task GeneratePdf() + { + if (checklist == null) return; + + try + { + isGeneratingPdf = true; + errorMessage = null; + + var userId = await UserContext.GetUserIdAsync(); + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + + // Generate PDF + var pdfBytes = PdfGenerator.GenerateChecklistPdf(checklist); + + // Create document record + var document = new Document + { + Id = Guid.NewGuid(), + OrganizationId = organizationId!.Value, + FileName = $"Checklist_{checklist.Name.Replace(" ", "_")}_{DateTime.Now:yyyyMMdd}.pdf", + FileExtension = ".pdf", + FileData = pdfBytes, + ContentType = "application/pdf", + FileType = "application/pdf", + FileSize = pdfBytes.Length, + DocumentType = "Checklist Report", + Description = $"Checklist report for {checklist.Name}", + PropertyId = checklist.PropertyId, + LeaseId = checklist.LeaseId, + CreatedBy = userId!, + CreatedOn = DateTime.UtcNow + }; + + // Save document to database + var savedDocument = await DocumentService.CreateAsync(document); + + // Update checklist with document reference + checklist.DocumentId = savedDocument.Id; + await ChecklistService.UpdateChecklistAsync(checklist); + + // View the PDF + //await JSRuntime.InvokeVoidAsync("viewFile", Convert.ToBase64String(pdfBytes), "application/pdf"); + + successMessage = "PDF generated and saved successfully!"; + + // Reload checklist to update button text + await LoadChecklist(); + } + catch (Exception ex) + { + errorMessage = $"Error generating PDF: {ex.Message}"; + } + finally + { + isGeneratingPdf = false; + } + } + + private string GetStatusBadge() + { + if (checklist == null) return "bg-secondary"; + + return checklist.Status switch + { + var s when s == ApplicationConstants.ChecklistStatuses.Completed => "bg-success", + var s when s == ApplicationConstants.ChecklistStatuses.InProgress => "bg-warning text-dark", + var s when s == ApplicationConstants.ChecklistStatuses.Draft => "bg-secondary", + _ => "bg-secondary" + }; + } + + private void ContinueEditing() + { + NavigationManager.NavigateTo($"/propertymanagement/checklists/complete/{ChecklistId}"); + } + + private void BackToList() + { + NavigationManager.NavigateTo("/propertymanagement/checklists"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/Documents.razor b/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/Documents.razor new file mode 100644 index 0000000..a4b23f0 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/Documents.razor @@ -0,0 +1,586 @@ +@page "/propertymanagement/documents" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization + +@inject IJSRuntime JSRuntime +@inject Application.Services.DocumentService DocumentService +@inject LeaseService LeaseService +@inject PropertyService PropertyService +@inject NavigationManager Navigation + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Documents - Property Management + +
+
+

Documents

+

Documents uploaded in the last 30 days

+
+
+ +@if (isLoading) +{ +
+
+ Loading... +
+
+} +else if (!allDocuments.Any()) +{ +
+

No Recent Documents

+

No documents have been uploaded in the last 30 days.

+
+} +else +{ + +
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+ + +
+
+
+
+
Lease Agreements
+

@allDocuments.Count(d => d.DocumentType == "Lease Agreement")

+
+
+
+
+
+
+
Invoices
+

@allDocuments.Count(d => d.DocumentType == "Invoice")

+
+
+
+
+
+
+
Payment Receipts
+

@allDocuments.Count(d => d.DocumentType == "Payment Receipt")

+
+
+
+
+
+
+
Total Documents
+

@filteredDocuments.Count

+
+
+
+
+ +
+
+ @if (groupByProperty) + { + @foreach (var propertyGroup in groupedDocuments) + { + var property = properties.FirstOrDefault(p => p.Id == propertyGroup.Key); + var propertyDocCount = propertyGroup.Count(); + var isExpanded = expandedProperties.Contains(propertyGroup.Key.GetHashCode()); + +
+
+
+
+ + @(property?.Address ?? "Unassigned") + @if (property != null) + { + @property.City, @property.State @property.ZipCode + } +
+
+ @propertyDocCount document(s) +
+
+
+ @if (isExpanded) + { +
+ + + + + + + + + + + + + @foreach (var doc in propertyGroup.OrderByDescending(d => d.CreatedOn)) + { + var lease = leases.FirstOrDefault(l => l.Id == doc.LeaseId); + + + + + + + + + } + +
DocumentTypeLeaseSizeUploadedActions
+ + @doc.FileName + @if (!string.IsNullOrEmpty(doc.Description)) + { +
@doc.Description + } +
@doc.DocumentType + @if (lease != null) + { + @lease.Tenant?.FullName +
@lease.StartDate.ToString("MMM yyyy") - @lease.EndDate.ToString("MMM yyyy") + } +
@doc.FileSizeFormatted + @doc.CreatedOn.ToString("MMM dd, yyyy") +
@doc.CreatedBy +
+
+ + + @if (lease != null) + { + + } + +
+
+
+ } +
+ } + } + else + { +
+ + + + + + + + + + + + + + @foreach (var doc in pagedDocuments) + { + var lease = leases.FirstOrDefault(l => l.Id == doc.LeaseId); + var property = properties.FirstOrDefault(p => p.Id == doc.PropertyId); + + + + + + + + + + } + +
+ + + + PropertyLeaseSize + + Actions
+ + @doc.FileName + @if (!string.IsNullOrEmpty(doc.Description)) + { +
@doc.Description + } +
@doc.DocumentType + @if (property != null) + { + @property.Address +
@property.City, @property.State + } +
+ @if (lease != null) + { + @lease.Tenant?.FullName +
@lease.StartDate.ToString("MMM yyyy") - @lease.EndDate.ToString("MMM yyyy") + } +
@doc.FileSizeFormatted + @doc.CreatedOn.ToString("MMM dd, yyyy") +
@doc.CreatedBy +
+
+ + + @if (lease != null) + { + + } + +
+
+
+ + + @if (totalPages > 1) + { + + } + } +
+
+} + +@code { + private bool isLoading = true; + private List allDocuments = new(); + private List filteredDocuments = new(); + private List pagedDocuments = new(); + private List leases = new(); + private List properties = new(); + + private string searchTerm = string.Empty; + private string selectedDocumentType = string.Empty; + private bool groupByProperty = true; + private HashSet expandedProperties = new(); + + // Pagination + private int currentPage = 1; + private int pageSize = 20; + private int totalPages = 1; + + // Sorting + private string sortColumn = nameof(Document.CreatedOn); + private bool sortAscending = false; + + private IEnumerable> groupedDocuments = Enumerable.Empty>(); + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + [Parameter] + [SupplyParameterFromQuery] + public Guid? PropertyId { get; set; } + + protected override async Task OnInitializedAsync() + { + await LoadDocuments(); + } + + private async Task LoadDocuments() + { + isLoading = true; + try + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + return; + } + + // Get all documents from last 30 days for the user + var allUserDocs = await DocumentService.GetAllAsync(); + var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30); + allDocuments = allUserDocs + .Where(d => d.CreatedOn >= thirtyDaysAgo && !d.IsDeleted) + .OrderByDescending(d => d.CreatedOn) + .ToList(); + + // Load all leases and properties + var allLeases = await LeaseService.GetAllAsync(); + leases = allLeases.Where(l => !l.IsDeleted).ToList(); + + var allProperties = await PropertyService.GetAllAsync(); + properties = allProperties.Where(p => !p.IsDeleted).ToList(); + + ApplyFilters(); + } + finally + { + isLoading = false; + } + } + + private void ApplyFilters() + { + filteredDocuments = allDocuments; + + // Apply search filter + if (!string.IsNullOrWhiteSpace(searchTerm)) + { + filteredDocuments = filteredDocuments + .Where(d => d.FileName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + (d.Description?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false)) + .ToList(); + } + + // Apply document type filter + if (!string.IsNullOrWhiteSpace(selectedDocumentType)) + { + filteredDocuments = filteredDocuments + .Where(d => d.DocumentType == selectedDocumentType) + .ToList(); + } + + if(PropertyId.HasValue) + { + filteredDocuments = filteredDocuments + .Where(d => d.PropertyId == PropertyId.Value) + .ToList(); + } + + // Apply sorting + ApplySorting(); + + if (groupByProperty) + { + groupedDocuments = filteredDocuments + .Where(d => d.PropertyId.HasValue && d.PropertyId.Value != Guid.Empty) + .GroupBy(d => d.PropertyId!.Value) + .OrderBy(g => properties.FirstOrDefault(p => p.Id == g.Key)?.Address ?? ""); + } + else + { + // Pagination for flat view + totalPages = (int)Math.Ceiling(filteredDocuments.Count / (double)pageSize); + currentPage = Math.Max(1, Math.Min(currentPage, totalPages == 0 ? 1 : totalPages)); + UpdatePagedDocuments(); + } + + StateHasChanged(); + } + + private void ApplySorting() + { + filteredDocuments = sortColumn switch + { + nameof(Document.FileName) => sortAscending + ? filteredDocuments.OrderBy(d => d.FileName).ToList() + : filteredDocuments.OrderByDescending(d => d.FileName).ToList(), + nameof(Document.DocumentType) => sortAscending + ? filteredDocuments.OrderBy(d => d.DocumentType).ToList() + : filteredDocuments.OrderByDescending(d => d.DocumentType).ToList(), + nameof(Document.CreatedOn) => sortAscending + ? filteredDocuments.OrderBy(d => d.CreatedOn).ToList() + : filteredDocuments.OrderByDescending(d => d.CreatedOn).ToList(), + _ => filteredDocuments.OrderByDescending(d => d.CreatedOn).ToList() + }; + } + + private void SortBy(string column) + { + if (sortColumn == column) + { + sortAscending = !sortAscending; + } + else + { + sortColumn = column; + sortAscending = true; + } + ApplyFilters(); + } + + private void UpdatePagedDocuments() + { + pagedDocuments = filteredDocuments + .Skip((currentPage - 1) * pageSize) + .Take(pageSize) + .ToList(); + } + + private void ChangePage(int page) + { + currentPage = page; + UpdatePagedDocuments(); + } + + private void ClearFilters() + { + searchTerm = string.Empty; + selectedDocumentType = string.Empty; + groupByProperty = false; + PropertyId = null; + ApplyFilters(); + } + + private void TogglePropertyGroup(Guid propertyId) + { + if (expandedProperties.Contains(propertyId.GetHashCode())) + { + expandedProperties.Remove(propertyId.GetHashCode()); + } + else + { + expandedProperties.Add(propertyId.GetHashCode()); + } + } + + private async Task ViewDocument(Document doc) + { + var base64Data = Convert.ToBase64String(doc.FileData); + await JSRuntime.InvokeVoidAsync("viewFile", base64Data, doc.FileType); + } + + private async Task DownloadDocument(Document doc) + { + var fileName = doc.FileName; + var fileData = doc.FileData; + var mimeType = doc.FileType; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(fileData), mimeType); + } + + private void GoToLease(Guid leaseId) + { + Navigation.NavigateTo($"/propertymanagement/leases/view/{leaseId}"); + } + + private async Task DeleteDocument(Document doc) + { + var confirmed = await JSRuntime.InvokeAsync("confirm", $"Are you sure you want to delete '{doc.FileName}'? This action cannot be undone."); + + if (confirmed) + { + try + { + await DocumentService.DeleteAsync(doc.Id); + await LoadDocuments(); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"Error deleting document: {ex.Message}"); + } + } + } + + private string GetFileIcon(string extension) + { + return extension.ToLower() switch + { + ".pdf" => "bi-file-pdf text-danger", + ".doc" or ".docx" => "bi-file-word text-primary", + ".jpg" or ".jpeg" or ".png" => "bi-file-image text-success", + ".txt" => "bi-file-text", + _ => "bi-file-earmark" + }; + } + + private string GetDocumentTypeBadge(string documentType) + { + return documentType switch + { + "Lease Agreement" => "bg-primary", + "Invoice" => "bg-warning", + "Payment Receipt" => "bg-success", + "Addendum" => "bg-info", + "Move-In Inspection" or "Move-Out Inspection" => "bg-secondary", + _ => "bg-secondary" + }; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor b/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor new file mode 100644 index 0000000..4445501 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor @@ -0,0 +1,347 @@ +@page "/propertymanagement/leases/{LeaseId:guid}/documents" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization +@inject NavigationManager Navigation +@inject Application.Services.DocumentService DocumentService +@inject LeaseService LeaseService +@inject UserContextService UserContext +@inject IJSRuntime JSRuntime + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Lease Documents - Property Management + +@if (lease == null) +{ +
+
+ Loading... +
+
+} +else +{ +
+
+

Lease Documents

+

+ Property: @lease.Property?.Address | + Tenant: @lease.Tenant?.FullName | + Lease Period: @lease.StartDate.ToString("MMM dd, yyyy") - @lease.EndDate.ToString("MMM dd, yyyy") +

+
+
+ + +
+
+ + @if (showUploadDialog) + { +
+
+
Upload New Document
+
+
+ + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + @if (!string.IsNullOrEmpty(selectedFileName)) + { + Selected: @selectedFileName (@selectedFileSize) + } +
+
+
+ + + +
+
+ + +
+
+
+
+ } + + @if (documents == null) + { +
+
+ Loading documents... +
+
+ } + else if (!documents.Any()) + { +
+

No Documents Found

+

No documents have been uploaded for this lease yet.

+ +
+ } + else + { +
+
+
+ + + + + + + + + + + + + @foreach (var doc in documents) + { + + + + + + + + + } + +
DocumentTypeSizeUploaded ByUpload DateActions
+ + @doc.FileName + @if (!string.IsNullOrEmpty(doc.Description)) + { +
+ @doc.Description + } +
+ @doc.DocumentType + @doc.FileSizeFormatted@doc.CreatedBy@doc.CreatedOn.ToString("MMM dd, yyyy h:mm tt") +
+ + + +
+
+
+
+
+ } +} + +@code { + [Parameter] + public Guid LeaseId { get; set; } + + private Lease? lease; + private List? documents; + private bool showUploadDialog = false; + private bool isUploading = false; + private UploadModel uploadModel = new(); + private IBrowserFile? selectedFile; + private string selectedFileName = string.Empty; + private string selectedFileSize = string.Empty; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadLease(); + await LoadDocuments(); + } + + private async Task LoadLease() + { + lease = await LeaseService.GetByIdAsync(LeaseId); + if (lease == null) + { + Navigation.NavigateTo("/propertymanagement/leases"); + } + } + + private async Task LoadDocuments() + { + documents = await DocumentService.GetDocumentsByLeaseIdAsync(LeaseId); + } + + private void ShowUploadDialog() + { + showUploadDialog = true; + uploadModel = new UploadModel(); + selectedFile = null; + selectedFileName = string.Empty; + selectedFileSize = string.Empty; + } + + private void CancelUpload() + { + showUploadDialog = false; + uploadModel = new UploadModel(); + selectedFile = null; + selectedFileName = string.Empty; + selectedFileSize = string.Empty; + } + + private void HandleFileSelected(InputFileChangeEventArgs e) + { + selectedFile = e.File; + selectedFileName = selectedFile.Name; + selectedFileSize = FormatFileSize(selectedFile.Size); + } + + private async Task HandleUpload() + { + if (selectedFile == null) return; + + isUploading = true; + try + { + var organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + + // Read file data + using var memoryStream = new MemoryStream(); + await selectedFile.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024).CopyToAsync(memoryStream); // 10MB max + + var document = new Document + { + OrganizationId = organizationId, + FileName = selectedFile.Name, + FileExtension = Path.GetExtension(selectedFile.Name), + FileData = memoryStream.ToArray(), + FileSize = selectedFile.Size, + FileType = selectedFile.ContentType, + DocumentType = uploadModel.DocumentType, + FilePath = string.Empty, + Description = uploadModel.Description ?? string.Empty, + LeaseId = LeaseId, + }; + + await DocumentService.CreateAsync(document); + await LoadDocuments(); + CancelUpload(); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"Error uploading file: {ex.Message}"); + } + finally + { + isUploading = false; + } + } + + private async Task ViewDocument(Document doc) + { + var base64Data = Convert.ToBase64String(doc.FileData); + await JSRuntime.InvokeVoidAsync("viewFile", base64Data, doc.FileType); + } + + private async Task DownloadDocument(Document doc) + { + var fileName = doc.FileName; + var fileData = doc.FileData; + var mimeType = doc.FileType; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(fileData), mimeType); + } + + private async Task DeleteDocument(Document doc) + { + if (await JSRuntime.InvokeAsync("confirm", $"Are you sure you want to delete '{doc.FileName}'?")) + { + await DocumentService.DeleteAsync(doc.Id); + await LoadDocuments(); + } + } + + private void GoToLease() + { + Navigation.NavigateTo($"/propertymanagement/leases/view/{LeaseId}"); + } + + private string GetFileIcon(string extension) + { + return extension.ToLower() switch + { + ".pdf" => "bi-file-pdf text-danger", + ".doc" or ".docx" => "bi-file-word text-primary", + ".jpg" or ".jpeg" or ".png" => "bi-file-image text-success", + ".txt" => "bi-file-text", + _ => "bi-file-earmark" + }; + } + + private string FormatFileSize(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB" }; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + + public class UploadModel + { + [Required(ErrorMessage = "Document type is required.")] + public string DocumentType { get; set; } = string.Empty; + + [MaxLength(500)] + public string? Description { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/_Imports.razor b/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/_Imports.razor new file mode 100644 index 0000000..aaa76e6 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Aquiis.Professional +@using Aquiis.Professional.Infrastructure.Data +@using Aquiis.Professional.Core.Entities +@using System.ComponentModel.DataAnnotations diff --git a/Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor b/Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor new file mode 100644 index 0000000..3cf8ddf --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor @@ -0,0 +1,51 @@ +
+
+ +
+
+
+ + +
+
+
+ +
+
+ +@code { + [Parameter] + public string Label { get; set; } = string.Empty; + + [Parameter] + public bool IsGood { get; set; } = true; + + [Parameter] + public EventCallback IsGoodChanged { get; set; } + + [Parameter] + public string? Notes { get; set; } + + [Parameter] + public EventCallback NotesChanged { get; set; } + + private async Task OnIsGoodChanged(ChangeEventArgs e) + { + IsGood = e.Value is bool b && b; + await IsGoodChanged.InvokeAsync(IsGood); + } + + private async Task OnNotesChanged(ChangeEventArgs e) + { + Notes = e.Value?.ToString(); + await NotesChanged.InvokeAsync(Notes); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionSectionView.razor b/Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionSectionView.razor new file mode 100644 index 0000000..5fe9a2e --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionSectionView.razor @@ -0,0 +1,60 @@ +
+
+
@Title
+
+
+ @if (Items != null && Items.Any()) + { +
+ + + + + + + + + + @foreach (var item in Items) + { + + + + + + } + +
ItemStatusNotes
@item.Label + @if (item.IsGood) + { + Good + } + else + { + Issue + } + + @if (!string.IsNullOrEmpty(item.Notes)) + { + @item.Notes + } + else + { + - + } +
+
+ } +
+
+ +@code { + [Parameter] + public string Title { get; set; } = string.Empty; + + [Parameter] + public string Icon { get; set; } = "clipboard-check"; + + [Parameter] + public List<(string Label, bool IsGood, string? Notes)>? Items { get; set; } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Create.razor b/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Create.razor new file mode 100644 index 0000000..bd19949 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Create.razor @@ -0,0 +1,552 @@ +@page "/propertymanagement/inspections/create" +@page "/propertymanagement/inspections/create/{PropertyId:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Validation +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations + +@inject InspectionService InspectionService +@inject PropertyService PropertyService +@inject LeaseService LeaseService +@inject UserContextService UserContext +@inject NavigationManager NavigationManager +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Create Inspection + +
+

Property Inspection

+ +
+ +@if (!string.IsNullOrEmpty(errorMessage)) +{ + +} + +@if (property == null) +{ +
+
+ Loading... +
+
+} +else +{ + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + +
+
+ + + + + +
+
+
Property Information
+
+
+

@property.Address

+

@property.City, @property.State @property.ZipCode

+
+
+ + +
+
+
Inspection Details
+
+
+
+
+ + + +
+
+ + + + + + + +
+
+
+ + +
+
+
+ + +
+
+
Exterior Inspection
+ +
+
+ + + + + + + +
+
+ + +
+
+
Interior Inspection
+ +
+
+ + + + + +
+
+ + +
+
+
Kitchen
+ +
+
+ + + + +
+
+ + +
+
+
Bathroom
+ +
+
+ + + + +
+
+ + +
+
+
Systems & Safety
+ +
+
+ + + + + +
+
+ + +
+
+
Overall Assessment
+
+
+
+ + + + + + + +
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+
+
+ + +
+
+
+
Inspection Tips
+
+
+
    +
  • Take photos of any issues found
  • +
  • Check all appliances for proper operation
  • +
  • Test all outlets and switches
  • +
  • Run water in all sinks/tubs
  • +
  • Check for signs of moisture/leaks
  • +
  • Note any safety concerns immediately
  • +
  • Document any tenant-caused damage
  • +
+
+
+
+
+} + +@code { + [Parameter] + public Guid? PropertyId { get; set; } + + [SupplyParameterFromQuery(Name = "propertyId")] + public Guid? PropertyIdFromQuery { get; set; } + + private Property? property; + private InspectionModel model = new(); + private string? errorMessage; + private string? successMessage; + private bool isSaving = false; + + protected override async Task OnInitializedAsync() + { + // Use query parameter if route parameter is not provided + if (!PropertyId.HasValue && PropertyIdFromQuery.HasValue) + { + PropertyId = PropertyIdFromQuery; + } + + @* // Get the current user's organization and user ID first + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + var userEmail = await UserContext.GetUserEmailAsync(); *@ + + @* if (organizationId == null || string.IsNullOrEmpty(userId)) + { + errorMessage = "Unable to determine user context. Please log in again."; + return; + } *@ + + if (PropertyId.HasValue) + { + property = await PropertyService.GetByIdAsync(PropertyId.Value); + + if (property == null) + { + errorMessage = "Property not found."; + return; + } + + model.PropertyId = PropertyId.Value; + + // Check if there's an active lease + var activeLeases = await LeaseService.GetActiveLeasesByPropertyIdAsync(PropertyId.Value); + if (activeLeases.Any()) + { + model.LeaseId = activeLeases.First().Id; + } + } + else + { + errorMessage = "Property ID is required."; + } + } + + private async Task SaveInspection() + { + try + { + isSaving = true; + errorMessage = null; + successMessage = null; + + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + var userId = await UserContext.GetUserIdAsync(); + + // Create inspection entity from model + var inspection = new Inspection + { + PropertyId = model.PropertyId, + LeaseId = model.LeaseId, + CompletedOn = model.CompletedOn, + InspectionType = model.InspectionType, + InspectedBy = model.InspectedBy, + ExteriorRoofGood = model.ExteriorRoofGood, + ExteriorRoofNotes = model.ExteriorRoofNotes, + ExteriorGuttersGood = model.ExteriorGuttersGood, + ExteriorGuttersNotes = model.ExteriorGuttersNotes, + ExteriorSidingGood = model.ExteriorSidingGood, + ExteriorSidingNotes = model.ExteriorSidingNotes, + ExteriorWindowsGood = model.ExteriorWindowsGood, + ExteriorWindowsNotes = model.ExteriorWindowsNotes, + ExteriorDoorsGood = model.ExteriorDoorsGood, + ExteriorDoorsNotes = model.ExteriorDoorsNotes, + ExteriorFoundationGood = model.ExteriorFoundationGood, + ExteriorFoundationNotes = model.ExteriorFoundationNotes, + LandscapingGood = model.LandscapingGood, + LandscapingNotes = model.LandscapingNotes, + InteriorWallsGood = model.InteriorWallsGood, + InteriorWallsNotes = model.InteriorWallsNotes, + InteriorCeilingsGood = model.InteriorCeilingsGood, + InteriorCeilingsNotes = model.InteriorCeilingsNotes, + InteriorFloorsGood = model.InteriorFloorsGood, + InteriorFloorsNotes = model.InteriorFloorsNotes, + InteriorDoorsGood = model.InteriorDoorsGood, + InteriorDoorsNotes = model.InteriorDoorsNotes, + InteriorWindowsGood = model.InteriorWindowsGood, + InteriorWindowsNotes = model.InteriorWindowsNotes, + KitchenAppliancesGood = model.KitchenAppliancesGood, + KitchenAppliancesNotes = model.KitchenAppliancesNotes, + KitchenCabinetsGood = model.KitchenCabinetsGood, + KitchenCabinetsNotes = model.KitchenCabinetsNotes, + KitchenCountersGood = model.KitchenCountersGood, + KitchenCountersNotes = model.KitchenCountersNotes, + KitchenSinkPlumbingGood = model.KitchenSinkPlumbingGood, + KitchenSinkPlumbingNotes = model.KitchenSinkPlumbingNotes, + BathroomToiletGood = model.BathroomToiletGood, + BathroomToiletNotes = model.BathroomToiletNotes, + BathroomSinkGood = model.BathroomSinkGood, + BathroomSinkNotes = model.BathroomSinkNotes, + BathroomTubShowerGood = model.BathroomTubShowerGood, + BathroomTubShowerNotes = model.BathroomTubShowerNotes, + BathroomVentilationGood = model.BathroomVentilationGood, + BathroomVentilationNotes = model.BathroomVentilationNotes, + HvacSystemGood = model.HvacSystemGood, + HvacSystemNotes = model.HvacSystemNotes, + ElectricalSystemGood = model.ElectricalSystemGood, + ElectricalSystemNotes = model.ElectricalSystemNotes, + PlumbingSystemGood = model.PlumbingSystemGood, + PlumbingSystemNotes = model.PlumbingSystemNotes, + SmokeDetectorsGood = model.SmokeDetectorsGood, + SmokeDetectorsNotes = model.SmokeDetectorsNotes, + CarbonMonoxideDetectorsGood = model.CarbonMonoxideDetectorsGood, + CarbonMonoxideDetectorsNotes = model.CarbonMonoxideDetectorsNotes, + OverallCondition = model.OverallCondition, + GeneralNotes = model.GeneralNotes, + ActionItemsRequired = model.ActionItemsRequired, + }; + + // Add the inspection + await InspectionService.CreateAsync(inspection); + + successMessage = "Inspection saved successfully!"; + + // Navigate to view inspection page after short delay + await Task.Delay(500); + NavigationManager.NavigateTo($"/propertymanagement/inspections/view/{inspection.Id}"); + } + catch (Exception ex) + { + errorMessage = $"Error saving inspection: {ex.Message}"; + } + finally + { + isSaving = false; + } + } + + private void Cancel() + { + if (PropertyId.HasValue) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/view/{PropertyId.Value}"); + } + else + { + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + } + + private void MarkAllExteriorGood() + { + model.ExteriorRoofGood = true; + model.ExteriorGuttersGood = true; + model.ExteriorSidingGood = true; + model.ExteriorWindowsGood = true; + model.ExteriorDoorsGood = true; + model.ExteriorFoundationGood = true; + model.LandscapingGood = true; + } + + private void MarkAllInteriorGood() + { + model.InteriorWallsGood = true; + model.InteriorCeilingsGood = true; + model.InteriorFloorsGood = true; + model.InteriorDoorsGood = true; + model.InteriorWindowsGood = true; + } + + private void MarkAllKitchenGood() + { + model.KitchenAppliancesGood = true; + model.KitchenCabinetsGood = true; + model.KitchenCountersGood = true; + model.KitchenSinkPlumbingGood = true; + } + + private void MarkAllBathroomGood() + { + model.BathroomToiletGood = true; + model.BathroomSinkGood = true; + model.BathroomTubShowerGood = true; + model.BathroomVentilationGood = true; + } + + private void MarkAllSystemsGood() + { + model.HvacSystemGood = true; + model.ElectricalSystemGood = true; + model.PlumbingSystemGood = true; + model.SmokeDetectorsGood = true; + model.CarbonMonoxideDetectorsGood = true; + } + + public class InspectionModel + { + [RequiredGuid] + public Guid PropertyId { get; set; } + + [OptionalGuid] + public Guid? LeaseId { get; set; } + + [Required] + public DateTime CompletedOn { get; set; } = DateTime.Today; + + [Required] + [StringLength(50)] + public string InspectionType { get; set; } = "Routine"; + + [StringLength(100)] + public string? InspectedBy { get; set; } + + // Exterior + public bool ExteriorRoofGood { get; set; } + public string? ExteriorRoofNotes { get; set; } + public bool ExteriorGuttersGood { get; set; } + public string? ExteriorGuttersNotes { get; set; } + public bool ExteriorSidingGood { get; set; } + public string? ExteriorSidingNotes { get; set; } + public bool ExteriorWindowsGood { get; set; } + public string? ExteriorWindowsNotes { get; set; } + public bool ExteriorDoorsGood { get; set; } + public string? ExteriorDoorsNotes { get; set; } + public bool ExteriorFoundationGood { get; set; } + public string? ExteriorFoundationNotes { get; set; } + public bool LandscapingGood { get; set; } + public string? LandscapingNotes { get; set; } + + // Interior + public bool InteriorWallsGood { get; set; } + public string? InteriorWallsNotes { get; set; } + public bool InteriorCeilingsGood { get; set; } + public string? InteriorCeilingsNotes { get; set; } + public bool InteriorFloorsGood { get; set; } + public string? InteriorFloorsNotes { get; set; } + public bool InteriorDoorsGood { get; set; } + public string? InteriorDoorsNotes { get; set; } + public bool InteriorWindowsGood { get; set; } + public string? InteriorWindowsNotes { get; set; } + + // Kitchen + public bool KitchenAppliancesGood { get; set; } + public string? KitchenAppliancesNotes { get; set; } + public bool KitchenCabinetsGood { get; set; } + public string? KitchenCabinetsNotes { get; set; } + public bool KitchenCountersGood { get; set; } + public string? KitchenCountersNotes { get; set; } + public bool KitchenSinkPlumbingGood { get; set; } + public string? KitchenSinkPlumbingNotes { get; set; } + + // Bathroom + public bool BathroomToiletGood { get; set; } + public string? BathroomToiletNotes { get; set; } + public bool BathroomSinkGood { get; set; } + public string? BathroomSinkNotes { get; set; } + public bool BathroomTubShowerGood { get; set; } + public string? BathroomTubShowerNotes { get; set; } + public bool BathroomVentilationGood { get; set; } + public string? BathroomVentilationNotes { get; set; } + + // Systems + public bool HvacSystemGood { get; set; } + public string? HvacSystemNotes { get; set; } + public bool ElectricalSystemGood { get; set; } + public string? ElectricalSystemNotes { get; set; } + public bool PlumbingSystemGood { get; set; } + public string? PlumbingSystemNotes { get; set; } + public bool SmokeDetectorsGood { get; set; } + public string? SmokeDetectorsNotes { get; set; } + public bool CarbonMonoxideDetectorsGood { get; set; } + public string? CarbonMonoxideDetectorsNotes { get; set; } + + // Overall + [Required] + [StringLength(50)] + public string OverallCondition { get; set; } = "Good"; + public string? GeneralNotes { get; set; } + public string? ActionItemsRequired { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Schedule.razor b/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Schedule.razor new file mode 100644 index 0000000..233f84e --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Schedule.razor @@ -0,0 +1,323 @@ +@page "/propertymanagement/inspections/schedule" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization + +@inject PropertyService PropertyService +@inject NavigationManager NavigationManager +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Inspection Schedule + +
+

Routine Inspection Schedule

+ +
+ +@if (isLoading) +{ +
+
+ Loading... +
+
+} +else +{ + +
+
+
+
+
Overdue
+

@overdueProperties.Count

+ Inspections overdue +
+
+
+
+
+
+
Due Soon
+

@dueSoonProperties.Count

+ Due within 30 days +
+
+
+
+
+
+
Scheduled
+

@scheduledProperties.Count

+ Future inspections +
+
+
+
+
+
+
Not Scheduled
+

@notScheduledProperties.Count

+ No inspection date +
+
+
+
+ + + @if (overdueProperties.Any()) + { +
+
+
Overdue Inspections
+
+
+
+ + + + + + + + + + + + @foreach (var property in overdueProperties) + { + + + + + + + + } + +
PropertyLast InspectionDue DateDays OverdueActions
+ @property.Address +
@property.City, @property.State +
+ @if (property.LastRoutineInspectionDate.HasValue) + { + @property.LastRoutineInspectionDate.Value.ToString("MMM dd, yyyy") + } + else + { + Never + } + @property.NextRoutineInspectionDueDate!.Value.ToString("MMM dd, yyyy") + @property.DaysOverdue days + +
+ + +
+
+
+
+
+ } + + + @if (dueSoonProperties.Any()) + { +
+
+
Due Within 30 Days
+
+
+
+ + + + + + + + + + + + @foreach (var property in dueSoonProperties) + { + + + + + + + + } + +
PropertyLast InspectionDue DateDays Until DueActions
+ @property.Address +
@property.City, @property.State +
+ @if (property.LastRoutineInspectionDate.HasValue) + { + @property.LastRoutineInspectionDate.Value.ToString("MMM dd, yyyy") + } + else + { + Never + } + @property.NextRoutineInspectionDueDate!.Value.ToString("MMM dd, yyyy") + @property.DaysUntilInspectionDue days + +
+ + +
+
+
+
+
+ } + + +
+
+
All Properties
+
+
+
+ + + + + + + + + + + + @foreach (var property in allProperties.OrderBy(p => p.NextRoutineInspectionDueDate ?? DateTime.MaxValue)) + { + + + + + + + + } + +
PropertyLast InspectionNext DueStatusActions
+ @property.Address +
@property.City, @property.State +
+ @if (property.LastRoutineInspectionDate.HasValue) + { + @property.LastRoutineInspectionDate.Value.ToString("MMM dd, yyyy") + } + else + { + Never + } + + @if (property.NextRoutineInspectionDueDate.HasValue) + { + @property.NextRoutineInspectionDueDate.Value.ToString("MMM dd, yyyy") + } + else + { + Not scheduled + } + + + @property.InspectionStatus + + +
+ + +
+
+
+
+
+} + +@code { + private bool isLoading = true; + private List allProperties = new(); + private List overdueProperties = new(); + private List dueSoonProperties = new(); + private List scheduledProperties = new(); + private List notScheduledProperties = new(); + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + isLoading = true; + try + { + allProperties = await PropertyService.GetAllAsync(); + overdueProperties = await PropertyService.GetPropertiesWithOverdueInspectionsAsync(); + dueSoonProperties = await PropertyService.GetPropertiesWithInspectionsDueSoonAsync(30); + + scheduledProperties = allProperties + .Where(p => p.NextRoutineInspectionDueDate.HasValue && + p.InspectionStatus == "Scheduled") + .ToList(); + + notScheduledProperties = allProperties + .Where(p => !p.NextRoutineInspectionDueDate.HasValue) + .ToList(); + } + finally + { + isLoading = false; + } + } + + private async Task RefreshData() + { + await LoadData(); + } + + private void ViewProperty(Guid propertyId) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/view/{propertyId}"); + } + + private void CreateInspection(Guid propertyId) + { + NavigationManager.NavigateTo($"/propertymanagement/inspections/create/{propertyId}"); + } + + private string GetInspectionStatusBadge(string status) + { + return status switch + { + "Overdue" => "bg-danger", + "Due Soon" => "bg-warning", + "Scheduled" => "bg-success", + "Not Scheduled" => "bg-secondary", + _ => "bg-secondary" + }; + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/View.razor b/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/View.razor new file mode 100644 index 0000000..cd50715 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/View.razor @@ -0,0 +1,426 @@ +@page "/propertymanagement/inspections/view/{InspectionId:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components + +@inject InspectionService InspectionService +@inject Application.Services.DocumentService DocumentService +@inject UserContextService UserContext +@inject NavigationManager NavigationManager +@inject IJSRuntime JSRuntime +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Inspection Report + +@if (inspection == null) +{ +
+
+ Loading... +
+
+} +else +{ +
+

Inspection Report

+
+ @if (inspection.DocumentId == null) + { + + } + else + { + + + } + +
+
+ + @if (!string.IsNullOrEmpty(successMessage)) + { +
+ @successMessage + +
+ } + + @if (!string.IsNullOrEmpty(errorMessage)) + { +
+ @errorMessage + +
+ } + +
+
+ +
+
+
Property Information
+
+
+ @if (inspection.Property != null) + { +

@inspection.Property.Address

+

@inspection.Property.City, @inspection.Property.State @inspection.Property.ZipCode

+ } +
+
+ + +
+
+
Inspection Details
+
+
+
+
+ Inspection Date: +

@inspection.CompletedOn.ToString("MMMM dd, yyyy")

+
+
+ Type: +

@inspection.InspectionType

+
+
+ Overall Condition: +

@inspection.OverallCondition

+
+
+ @if (!string.IsNullOrEmpty(inspection.InspectedBy)) + { +
+
+ Inspected By: +

@inspection.InspectedBy

+
+
+ } +
+
+ + + + + + + + + + + + + + + + + +
+
+
Overall Assessment
+
+
+ @if (!string.IsNullOrEmpty(inspection.GeneralNotes)) + { +
+ General Notes: +

@inspection.GeneralNotes

+
+ } + @if (!string.IsNullOrEmpty(inspection.ActionItemsRequired)) + { +
+ Action Items Required: +

@inspection.ActionItemsRequired

+
+ } +
+
+
+ + +
+
+
+
Inspection Summary
+
+
+
+
+ Overall Condition: + @inspection.OverallCondition +
+
+
+
+ Items Checked: + @GetTotalItemsCount() +
+
+
+
+ Issues Found: + @GetIssuesCount() +
+
+
+
+ Pass Rate: + @GetPassRate()% +
+
+
+
+ @if (inspection.DocumentId == null) + { + + } + else + { + + + } + +
+
+
+
+
+} + +@code { + [Parameter] + public Guid InspectionId { get; set; } + + private Inspection? inspection; + private string? successMessage; + private string? errorMessage; + private bool isGenerating = false; + private Document? document = null; + + protected override async Task OnInitializedAsync() + { + inspection = await InspectionService.GetByIdAsync(InspectionId); + + // Load the document if it exists + if (inspection?.DocumentId != null) + { + document = await DocumentService.GetByIdAsync(inspection.DocumentId.Value); + } + } + + private List<(string Label, bool IsGood, string? Notes)> GetExteriorItems() => new() + { + ("Roof", inspection!.ExteriorRoofGood, inspection.ExteriorRoofNotes), + ("Gutters & Downspouts", inspection.ExteriorGuttersGood, inspection.ExteriorGuttersNotes), + ("Siding/Paint", inspection.ExteriorSidingGood, inspection.ExteriorSidingNotes), + ("Windows", inspection.ExteriorWindowsGood, inspection.ExteriorWindowsNotes), + ("Doors", inspection.ExteriorDoorsGood, inspection.ExteriorDoorsNotes), + ("Foundation", inspection.ExteriorFoundationGood, inspection.ExteriorFoundationNotes), + ("Landscaping & Drainage", inspection.LandscapingGood, inspection.LandscapingNotes) + }; + + private List<(string Label, bool IsGood, string? Notes)> GetInteriorItems() => new() + { + ("Walls", inspection!.InteriorWallsGood, inspection.InteriorWallsNotes), + ("Ceilings", inspection.InteriorCeilingsGood, inspection.InteriorCeilingsNotes), + ("Floors", inspection.InteriorFloorsGood, inspection.InteriorFloorsNotes), + ("Doors", inspection.InteriorDoorsGood, inspection.InteriorDoorsNotes), + ("Windows", inspection.InteriorWindowsGood, inspection.InteriorWindowsNotes) + }; + + private List<(string Label, bool IsGood, string? Notes)> GetKitchenItems() => new() + { + ("Appliances", inspection!.KitchenAppliancesGood, inspection.KitchenAppliancesNotes), + ("Cabinets & Drawers", inspection.KitchenCabinetsGood, inspection.KitchenCabinetsNotes), + ("Countertops", inspection.KitchenCountersGood, inspection.KitchenCountersNotes), + ("Sink & Plumbing", inspection.KitchenSinkPlumbingGood, inspection.KitchenSinkPlumbingNotes) + }; + + private List<(string Label, bool IsGood, string? Notes)> GetBathroomItems() => new() + { + ("Toilet", inspection!.BathroomToiletGood, inspection.BathroomToiletNotes), + ("Sink & Vanity", inspection.BathroomSinkGood, inspection.BathroomSinkNotes), + ("Tub/Shower", inspection.BathroomTubShowerGood, inspection.BathroomTubShowerNotes), + ("Ventilation/Exhaust Fan", inspection.BathroomVentilationGood, inspection.BathroomVentilationNotes) + }; + + private List<(string Label, bool IsGood, string? Notes)> GetSystemsItems() => new() + { + ("HVAC System", inspection!.HvacSystemGood, inspection.HvacSystemNotes), + ("Electrical System", inspection.ElectricalSystemGood, inspection.ElectricalSystemNotes), + ("Plumbing System", inspection.PlumbingSystemGood, inspection.PlumbingSystemNotes), + ("Smoke Detectors", inspection.SmokeDetectorsGood, inspection.SmokeDetectorsNotes), + ("Carbon Monoxide Detectors", inspection.CarbonMonoxideDetectorsGood, inspection.CarbonMonoxideDetectorsNotes) + }; + + private int GetTotalItemsCount() => 26; // Total checklist items + + private int GetIssuesCount() + { + if (inspection == null) return 0; + + var allItems = new List + { + inspection.ExteriorRoofGood, inspection.ExteriorGuttersGood, inspection.ExteriorSidingGood, + inspection.ExteriorWindowsGood, inspection.ExteriorDoorsGood, inspection.ExteriorFoundationGood, + inspection.LandscapingGood, inspection.InteriorWallsGood, inspection.InteriorCeilingsGood, + inspection.InteriorFloorsGood, inspection.InteriorDoorsGood, inspection.InteriorWindowsGood, + inspection.KitchenAppliancesGood, inspection.KitchenCabinetsGood, inspection.KitchenCountersGood, + inspection.KitchenSinkPlumbingGood, inspection.BathroomToiletGood, inspection.BathroomSinkGood, + inspection.BathroomTubShowerGood, inspection.BathroomVentilationGood, inspection.HvacSystemGood, + inspection.ElectricalSystemGood, inspection.PlumbingSystemGood, inspection.SmokeDetectorsGood, + inspection.CarbonMonoxideDetectorsGood + }; + + return allItems.Count(x => !x); + } + + private string GetPassRate() + { + var total = GetTotalItemsCount(); + var issues = GetIssuesCount(); + var passRate = ((total - issues) / (double)total) * 100; + return passRate.ToString("F0"); + } + + private string GetConditionBadge(string condition) => condition switch + { + "Excellent" => "bg-success", + "Good" => "bg-info", + "Fair" => "bg-warning", + "Poor" => "bg-danger", + _ => "bg-secondary" + }; + + private string GetConditionColor(string condition) => condition switch + { + "Excellent" => "text-success", + "Good" => "text-info", + "Fair" => "text-warning", + "Poor" => "text-danger", + _ => "text-secondary" + }; + + private async Task GeneratePdf() + { + try + { + isGenerating = true; + errorMessage = null; + + var pdfGenerator = new Aquiis.Professional.Application.Services.PdfGenerators.InspectionPdfGenerator(); + var pdfBytes = pdfGenerator.GenerateInspectionPdf(inspection!); + + var userId = await UserContext.GetUserIdAsync(); + var userEmail = await UserContext.GetUserEmailAsync(); + var organizationId = await UserContext.GetActiveOrganizationIdAsync(); + + var newDocument = new Document + { + FileName = $"Inspection_{inspection!.Property?.Address}_{inspection.CompletedOn:yyyyMMdd}.pdf", + FileData = pdfBytes, + FileExtension = ".pdf", + FileSize = pdfBytes.Length, + ContentType = "application/pdf", + FileType = "application/pdf", + DocumentType = "Inspection Report", + PropertyId = inspection.PropertyId, + LeaseId = inspection.LeaseId, + OrganizationId = organizationId!.Value, + CreatedOn = DateTime.UtcNow, + CreatedBy = userId!, + Description = $"{inspection.InspectionType} Inspection - {inspection.CompletedOn:MMM dd, yyyy}" + }; + + await DocumentService.CreateAsync(newDocument); + + // Link the document to the inspection + inspection.DocumentId = newDocument.Id; + await InspectionService.UpdateAsync(inspection); + + document = newDocument; + successMessage = "Inspection PDF generated and saved successfully!"; + } + catch (Exception ex) + { + errorMessage = $"Error generating PDF: {ex.Message}"; + } + finally + { + isGenerating = false; + } + } + + private async Task ViewDocument() + { + if (document != null) + { + var base64Data = Convert.ToBase64String(document.FileData); + await JSRuntime.InvokeVoidAsync("viewFile", base64Data, document.FileType); + } + } + + private async Task DownloadDocument() + { + if (document != null) + { + var fileName = document.FileName; + var fileData = document.FileData; + var mimeType = document.FileType; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(fileData), mimeType); + } + } + + private void EditInspection() + { + // TODO: Implement edit functionality + NavigationManager.NavigateTo($"/propertymanagement/inspections/edit/{InspectionId}"); + } + + private void BackToProperty() + { + if (inspection?.PropertyId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/view/{inspection.PropertyId}"); + } + else + { + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/CreateInvoice.razor b/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/CreateInvoice.razor new file mode 100644 index 0000000..9d54ce3 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/CreateInvoice.razor @@ -0,0 +1,317 @@ +@page "/propertymanagement/invoices/create" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Validation +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations +@inject NavigationManager NavigationManager +@inject InvoiceService InvoiceService +@inject LeaseService LeaseService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Create Invoice - Property Management + +
+

Create Invoice

+ +
+ +@if (errorMessage != null) +{ + +} + +@if (successMessage != null) +{ + +} + +
+
+
+
+
Invoice Information
+
+
+ + + + +
+ + + +
+ +
+
+ + + +
+
+ + + +
+
+ +
+ + + + @if (leases != null) + { + @foreach (var lease in leases) + { + + } + } + + +
+ +
+ + + +
+ +
+
+ +
+ $ + +
+ +
+
+ + + + + + + +
+
+ + @if (invoiceModel.Status == "Paid") + { +
+
+ +
+ $ + +
+ +
+
+ + + +
+
+ } + +
+ + + +
+ +
+ + +
+
+
+
+
+ +
+
+
+
Tips
+
+
+
    +
  • + + Invoice numbers are automatically generated +
  • +
  • + + Select an active lease to create an invoice +
  • +
  • + + The amount defaults to the lease's monthly rent +
  • +
  • + + Use clear descriptions to identify the invoice purpose +
  • +
+
+
+
+
+ +@code { + private InvoiceModel invoiceModel = new InvoiceModel(); + private List? leases; + private string? errorMessage; + private string? successMessage; + private bool isSubmitting = false; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + [Parameter] + [SupplyParameterFromQuery] + public Guid? LeaseId { get; set; } + + protected override async Task OnInitializedAsync() + { + await LoadLeases(); + invoiceModel.InvoiceNumber = await InvoiceService.GenerateInvoiceNumberAsync(); + invoiceModel.InvoicedOn = DateTime.Now; + invoiceModel.DueOn = DateTime.Now.AddDays(30); + if (LeaseId.HasValue) + { + invoiceModel.LeaseId = LeaseId.Value; + OnLeaseSelected(); + } + } + + private async Task LoadLeases() + { + var allLeases = await LeaseService.GetAllAsync(); + leases = allLeases.Where(l => l.Status == "Active").ToList(); + } + + private void OnLeaseSelected() + { + if (invoiceModel.LeaseId != Guid.Empty) + { + var selectedLease = leases?.FirstOrDefault(l => l.Id == invoiceModel.LeaseId); + if (selectedLease != null) + { + invoiceModel.Amount = selectedLease.MonthlyRent; + + // Generate description based on current month/year + var currentMonth = DateTime.Now.ToString("MMMM yyyy"); + invoiceModel.Description = $"Monthly Rent - {currentMonth}"; + } + } + } + + private async Task HandleCreateInvoice() + { + try + { + isSubmitting = true; + errorMessage = null; + successMessage = null; + + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + errorMessage = "User not authenticated."; + return; + } + + var invoice = new Invoice + { + LeaseId = invoiceModel.LeaseId, + InvoiceNumber = invoiceModel.InvoiceNumber, + InvoicedOn = invoiceModel.InvoicedOn, + DueOn = invoiceModel.DueOn, + Amount = invoiceModel.Amount, + Description = invoiceModel.Description, + Status = invoiceModel.Status, + AmountPaid = invoiceModel.Status == "Paid" ? invoiceModel.AmountPaid : 0, + PaidOn = invoiceModel.Status == "Paid" ? invoiceModel.PaidOn : null, + Notes = invoiceModel.Notes ?? string.Empty + }; + + await InvoiceService.CreateAsync(invoice); + + NavigationManager.NavigateTo("/propertymanagement/invoices"); + } + catch (Exception ex) + { + errorMessage = $"Error creating invoice: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/invoices"); + } + + public class InvoiceModel + { + [RequiredGuid(ErrorMessage = "Lease is required")] + public Guid LeaseId { get; set; } + + [Required(ErrorMessage = "Invoice number is required")] + [StringLength(50, ErrorMessage = "Invoice number cannot exceed 50 characters")] + public string InvoiceNumber { get; set; } = string.Empty; + + [Required(ErrorMessage = "Invoice date is required")] + public DateTime InvoicedOn { get; set; } = DateTime.Now; + + [Required(ErrorMessage = "Due date is required")] + public DateTime DueOn { get; set; } = DateTime.Now.AddDays(30); + + [Required(ErrorMessage = "Amount is required")] + [Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than 0")] + public decimal Amount { get; set; } + + [Required(ErrorMessage = "Description is required")] + [StringLength(100, ErrorMessage = "Description cannot exceed 100 characters")] + public string Description { get; set; } = string.Empty; + + [Required] + [StringLength(50)] + public string Status { get; set; } = "Pending"; + + [Range(0, double.MaxValue, ErrorMessage = "Amount paid cannot be negative")] + public decimal AmountPaid { get; set; } + + public DateTime? PaidOn { get; set; } + + [StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")] + public string? Notes { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/EditInvoice.razor b/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/EditInvoice.razor new file mode 100644 index 0000000..485f8cb --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/EditInvoice.razor @@ -0,0 +1,396 @@ +@page "/propertymanagement/invoices/edit/{Id:guid}" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Validation +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations +@inject NavigationManager NavigationManager +@inject InvoiceService InvoiceService +@inject LeaseService LeaseService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Edit Invoice - Property Management + +
+

Edit Invoice

+ +
+ +@if (errorMessage != null) +{ + +} + +@if (successMessage != null) +{ + +} + +@if (invoice == null) +{ +
+
+ Loading... +
+
+} +else +{ +
+
+
+
+
Invoice Information
+
+
+ + + + +
+ + + +
+ +
+
+ + + +
+
+ + + +
+
+ +
+ + + @if (leases != null) + { + @foreach (var lease in leases) + { + + } + } + + Lease cannot be changed after invoice creation +
+ +
+ + + +
+ +
+
+ +
+ $ + +
+ +
+
+ + + + + + + + +
+
+ +
+
+ +
+ $ + +
+ + Balance Due: @((invoiceModel.Amount - invoiceModel.AmountPaid).ToString("C")) +
+
+ + + +
+
+ +
+ + + +
+ +
+ + + +
+
+
+
+
+ +
+
+
+
Invoice Actions
+
+
+
+ + @if (invoice.Status != "Paid" && invoice.BalanceDue > 0) + { + + } + +
+
+
+ +
+
+
Invoice Summary
+
+
+
+ Status +
+ @invoice.Status +
+
+
+ Invoice Amount +
@invoice.Amount.ToString("C")
+
+
+ Paid Amount +
@invoice.AmountPaid.ToString("C")
+
+
+ Balance Due +
+ @invoice.BalanceDue.ToString("C") +
+
+ @if (invoice.IsOverdue) + { +
+ + + @invoice.DaysOverdue days overdue + +
+ } +
+
+
+
+} + +@code { + [Parameter] + public Guid Id { get; set; } + + private Invoice? invoice; + private InvoiceModel invoiceModel = new InvoiceModel(); + private List? leases; + private string? errorMessage; + private string? successMessage; + private bool isSubmitting = false; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadInvoice(); + await LoadLeases(); + } + + private async Task LoadInvoice() + { + invoice = await InvoiceService.GetByIdAsync(Id); + + if (invoice != null) + { + invoiceModel = new InvoiceModel + { + LeaseId = invoice.LeaseId, + InvoiceNumber = invoice.InvoiceNumber, + InvoicedOn = invoice.InvoicedOn, + DueOn = invoice.DueOn, + Amount = invoice.Amount, + Description = invoice.Description, + Status = invoice.Status, + AmountPaid = invoice.AmountPaid, + PaidOn = invoice.PaidOn, + Notes = invoice.Notes + }; + } + } + + private async Task LoadLeases() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (!string.IsNullOrEmpty(userId)) + { + leases = await LeaseService.GetAllAsync(); + } + } + + private async Task UpdateInvoice() + { + try + { + isSubmitting = true; + errorMessage = null; + successMessage = null; + + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + errorMessage = "User not authenticated."; + return; + } + + if (invoice == null) + { + errorMessage = "Invoice not found."; + return; + } + + invoice.InvoicedOn = invoiceModel.InvoicedOn; + invoice.DueOn = invoiceModel.DueOn; + invoice.Amount = invoiceModel.Amount; + invoice.Description = invoiceModel.Description; + invoice.Status = invoiceModel.Status; + invoice.AmountPaid = invoiceModel.AmountPaid; + invoice.PaidOn = invoiceModel.PaidOn; + invoice.Notes = invoiceModel.Notes ?? string.Empty; + + await InvoiceService.UpdateAsync(invoice); + + successMessage = "Invoice updated successfully!"; + } + catch (Exception ex) + { + errorMessage = $"Error updating invoice: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + "Paid" => "bg-success", + "Pending" => "bg-warning", + "Overdue" => "bg-danger", + "Cancelled" => "bg-secondary", + _ => "bg-info" + }; + } + + private void ViewInvoice() + { + NavigationManager.NavigateTo($"/propertymanagement/invoices/view/{Id}"); + } + + private void ViewLease() + { + if (invoice?.LeaseId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/view/{invoice.LeaseId}"); + } + } + + private void RecordPayment() + { + NavigationManager.NavigateTo($"/propertymanagement/payments/create?invoiceId={Id}"); + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/invoices"); + } + + public class InvoiceModel + { + [RequiredGuid(ErrorMessage = "Lease is required")] + public Guid LeaseId { get; set; } + + [Required(ErrorMessage = "Invoice number is required")] + [StringLength(50, ErrorMessage = "Invoice number cannot exceed 50 characters")] + public string InvoiceNumber { get; set; } = string.Empty; + + [Required(ErrorMessage = "Invoice date is required")] + public DateTime InvoicedOn { get; set; } + + [Required(ErrorMessage = "Due date is required")] + public DateTime DueOn { get; set; } + + [Required(ErrorMessage = "Amount is required")] + [Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than 0")] + public decimal Amount { get; set; } + + [Required(ErrorMessage = "Description is required")] + [StringLength(100, ErrorMessage = "Description cannot exceed 100 characters")] + public string Description { get; set; } = string.Empty; + + [Required] + [StringLength(50)] + public string Status { get; set; } = "Pending"; + + [Range(0, double.MaxValue, ErrorMessage = "Paid amount cannot be negative")] + public decimal AmountPaid { get; set; } + + public DateTime? PaidOn { get; set; } + + [StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")] + public string? Notes { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Invoices.razor b/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Invoices.razor new file mode 100644 index 0000000..3fcf0b1 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Invoices.razor @@ -0,0 +1,592 @@ +@page "/propertymanagement/invoices" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@inject NavigationManager Navigation +@inject InvoiceService InvoiceService +@inject IJSRuntime JSRuntime + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Invoices - Property Management + +
+

Invoices

+ +
+ +@if (invoices == null) +{ +
+
+ Loading... +
+
+} +else if (!invoices.Any()) +{ +
+

No Invoices Found

+

Get started by creating your first invoice.

+ +
+} +else +{ +
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+ +
+
+
+
+
Pending
+

@pendingCount

+ @pendingAmount.ToString("C") +
+
+
+
+
+
+
Paid
+

@paidCount

+ @paidAmount.ToString("C") +
+
+
+
+
+
+
Overdue
+

@overdueCount

+ @overdueAmount.ToString("C") +
+
+
+
+
+
+
Total
+

@filteredInvoices.Count

+ @totalAmount.ToString("C") +
+
+
+
+ +
+
+ @if (groupByProperty) + { + @foreach (var propertyGroup in groupedInvoices) + { + var property = propertyGroup.First().Lease?.Property; + var propertyInvoiceCount = propertyGroup.Count(); + var propertyTotal = propertyGroup.Sum(i => i.Amount); + var propertyBalance = propertyGroup.Sum(i => i.BalanceDue); + var isExpanded = expandedProperties.Contains(propertyGroup.Key.GetHashCode()); + +
+
+
+
+ + @property?.Address + @property?.City, @property?.State @property?.ZipCode +
+
+ @propertyInvoiceCount invoice(s) + Total: @propertyTotal.ToString("C") + Balance: @propertyBalance.ToString("C") +
+
+
+ @if (isExpanded) + { +
+ + + + + + + + + + + + + + + @foreach (var invoice in propertyGroup) + { + + + + + + + + + + + } + +
Invoice #TenantInvoice DateDue DateAmountBalance DueStatusActions
+ @invoice.InvoiceNumber +
+ @invoice.Description +
@invoice.Lease?.Tenant?.FullName@invoice.InvoicedOn.ToString("MMM dd, yyyy") + @invoice.DueOn.ToString("MMM dd, yyyy") + @if (invoice.IsOverdue) + { +
+ @invoice.DaysOverdue days overdue + } +
@invoice.Amount.ToString("C") + + @invoice.BalanceDue.ToString("C") + + + + @invoice.Status + + +
+ + + +
+
+
+ } +
+ } + } + else + { +
+ + + + + + + + + + + + + + @foreach (var invoice in pagedInvoices) + { + + + + + + + + + + + + } + +
+ + + + + + + + + + + + Balance DueStatusActions
+ @invoice.InvoiceNumber +
+ @invoice.Description +
@invoice.Lease?.Property?.Address@invoice.Lease?.Tenant?.FullName@invoice.InvoicedOn.ToString("MMM dd, yyyy") + @invoice.DueOn.ToString("MMM dd, yyyy") + @if (invoice.IsOverdue) + { +
+ @invoice.DaysOverdue days overdue + } +
@invoice.Amount.ToString("C") + + @invoice.BalanceDue.ToString("C") + + + + @invoice.Status + + +
+ + + +
+
+
+ } + + @if (totalPages > 1 && !groupByProperty) + { +
+
+ +
+
+ Showing @((currentPage - 1) * pageSize + 1) to @Math.Min(currentPage * pageSize, totalRecords) of @totalRecords invoices +
+ +
+ } +
+
+} + +@code { + private List? invoices; + private List filteredInvoices = new(); + private List pagedInvoices = new(); + private IEnumerable> groupedInvoices = Enumerable.Empty>(); + private HashSet expandedProperties = new(); + private string searchTerm = string.Empty; + private string selectedStatus = string.Empty; + private string sortColumn = nameof(Invoice.DueOn); + private bool sortAscending = false; + private bool groupByProperty = true; + + private int pendingCount = 0; + private int paidCount = 0; + private int overdueCount = 0; + private decimal pendingAmount = 0; + private decimal paidAmount = 0; + private decimal overdueAmount = 0; + private decimal totalAmount = 0; + + private int currentPage = 1; + private int pageSize = 20; + private int totalPages = 1; + private int totalRecords = 0; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + [Parameter] + [SupplyParameterFromQuery] + public Guid? LeaseId { get; set; } + + protected override async Task OnInitializedAsync() + { + await LoadInvoices(); + } + + private async Task LoadInvoices() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (!string.IsNullOrEmpty(userId)) + { + invoices = await InvoiceService.GetAllAsync(); + if (LeaseId.HasValue) + { + invoices = invoices.Where(i => i.LeaseId == LeaseId.Value).ToList(); + } + FilterInvoices(); + UpdateStatistics(); + } + } + + private void FilterInvoices() + { + if (invoices == null) return; + + filteredInvoices = invoices.Where(i => + { + bool matchesSearch = string.IsNullOrWhiteSpace(searchTerm) || + i.InvoiceNumber.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + (i.Lease?.Property?.Address?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false) || + (i.Lease?.Tenant?.FullName?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false) || + i.Description.Contains(searchTerm, StringComparison.OrdinalIgnoreCase); + + bool matchesStatus = string.IsNullOrWhiteSpace(selectedStatus) || + i.Status.Equals(selectedStatus, StringComparison.OrdinalIgnoreCase); + + return matchesSearch && matchesStatus; + }).ToList(); + + SortInvoices(); + + if (groupByProperty) + { + groupedInvoices = filteredInvoices + .Where(i => i.Lease?.PropertyId != null) + .GroupBy(i => i.Lease!.PropertyId) + .OrderBy(g => g.First().Lease?.Property?.Address) + .ToList(); + } + else + { + UpdatePagination(); + } + } + + private void TogglePropertyGroup(Guid propertyId) + { + if (expandedProperties.Contains(propertyId.GetHashCode())) + { + expandedProperties.Remove(propertyId.GetHashCode()); + } + else + { + expandedProperties.Add(propertyId.GetHashCode()); + } + } + + private void SortBy(string column) + { + if (sortColumn == column) + { + sortAscending = !sortAscending; + } + else + { + sortColumn = column; + sortAscending = true; + } + SortInvoices(); + UpdatePagination(); + } + + private void SortInvoices() + { + filteredInvoices = sortColumn switch + { + nameof(Invoice.InvoiceNumber) => sortAscending + ? filteredInvoices.OrderBy(i => i.InvoiceNumber).ToList() + : filteredInvoices.OrderByDescending(i => i.InvoiceNumber).ToList(), + "Property" => sortAscending + ? filteredInvoices.OrderBy(i => i.Lease?.Property?.Address).ToList() + : filteredInvoices.OrderByDescending(i => i.Lease?.Property?.Address).ToList(), + "Tenant" => sortAscending + ? filteredInvoices.OrderBy(i => i.Lease?.Tenant?.FullName).ToList() + : filteredInvoices.OrderByDescending(i => i.Lease?.Tenant?.FullName).ToList(), + nameof(Invoice.InvoicedOn) => sortAscending + ? filteredInvoices.OrderBy(i => i.InvoicedOn).ToList() + : filteredInvoices.OrderByDescending(i => i.InvoicedOn).ToList(), + nameof(Invoice.DueOn) => sortAscending + ? filteredInvoices.OrderBy(i => i.DueOn).ToList() + : filteredInvoices.OrderByDescending(i => i.DueOn).ToList(), + nameof(Invoice.Amount) => sortAscending + ? filteredInvoices.OrderBy(i => i.Amount).ToList() + : filteredInvoices.OrderByDescending(i => i.Amount).ToList(), + _ => filteredInvoices.OrderByDescending(i => i.DueOn).ToList() + }; + } + + private void UpdateStatistics() + { + if (invoices == null) return; + + pendingCount = invoices.Count(i => i.Status == "Pending"); + paidCount = invoices.Count(i => i.Status == "Paid"); + overdueCount = invoices.Count(i => i.IsOverdue && i.Status != "Paid"); + + pendingAmount = invoices.Where(i => i.Status == "Pending").Sum(i => i.BalanceDue); + paidAmount = invoices.Where(i => i.Status == "Paid").Sum(i => i.Amount); + overdueAmount = invoices.Where(i => i.IsOverdue && i.Status != "Paid").Sum(i => i.BalanceDue); + totalAmount = invoices.Sum(i => i.Amount); + } + + private void UpdatePagination() + { + totalRecords = filteredInvoices.Count; + totalPages = (int)Math.Ceiling(totalRecords / (double)pageSize); + currentPage = Math.Min(currentPage, Math.Max(1, totalPages)); + + pagedInvoices = filteredInvoices + .Skip((currentPage - 1) * pageSize) + .Take(pageSize) + .ToList(); + } + + private void ClearFilters() + { + searchTerm = string.Empty; + selectedStatus = string.Empty; + groupByProperty = false; + FilterInvoices(); + } + + private void FirstPage() => GoToPage(1); + private void LastPage() => GoToPage(totalPages); + private void NextPage() => GoToPage(currentPage + 1); + private void PreviousPage() => GoToPage(currentPage - 1); + + private void GoToPage(int page) + { + currentPage = Math.Max(1, Math.Min(page, totalPages)); + UpdatePagination(); + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + "Paid" => "bg-success", + "Pending" => "bg-warning", + "Overdue" => "bg-danger", + "Cancelled" => "bg-secondary", + _ => "bg-info" + }; + } + + private void CreateInvoice() + { + Navigation.NavigateTo("/propertymanagement/invoices/create"); + } + + private void ViewInvoice(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/invoices/view/{id}"); + } + + private void EditInvoice(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/invoices/edit/{id}"); + } + + private async Task DeleteInvoice(Invoice invoice) + { + if (await JSRuntime.InvokeAsync("confirm", $"Are you sure you want to delete invoice {invoice.InvoiceNumber}?")) + { + await InvoiceService.DeleteAsync(invoice.Id); + await LoadInvoices(); + } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/ViewInvoice.razor b/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/ViewInvoice.razor new file mode 100644 index 0000000..3f42340 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/ViewInvoice.razor @@ -0,0 +1,408 @@ +@page "/propertymanagement/invoices/view/{Id:guid}" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Web +@inject NavigationManager NavigationManager +@inject InvoiceService InvoiceService +@inject Application.Services.DocumentService DocumentService +@inject UserContextService UserContextService +@inject IJSRuntime JSRuntime + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +View Invoice - Property Management + +@if (invoice == null) +{ +
+
+ Loading... +
+
+} +else +{ +
+

Invoice Details

+
+ +
+
+ +
+
+
+
+
Invoice Information
+ @invoice.Status +
+
+
+
+
+ +
@invoice.InvoiceNumber
+
+
+ +
@invoice.InvoicedOn.ToString("MMMM dd, yyyy")
+
+
+ +
@invoice.Description
+
+
+
+
+ +
+ @invoice.DueOn.ToString("MMMM dd, yyyy") + @if (invoice.IsOverdue) + { +
+ (@invoice.DaysOverdue days overdue) + } +
+
+
+ +
@invoice.CreatedOn.ToString("MMMM dd, yyyy")
+
+
+
+ +
+ +
+
+
+ +
@invoice.Amount.ToString("C")
+
+
+
+
+ +
@invoice.AmountPaid.ToString("C")
+
+
+
+
+ +
+ @invoice.BalanceDue.ToString("C") +
+
+
+
+ + @if (invoice.PaidOn.HasValue) + { +
+ +
@invoice.PaidOn.Value.ToString("MMMM dd, yyyy")
+
+ } + + @if (!string.IsNullOrWhiteSpace(invoice.Notes)) + { +
+
+ +
@invoice.Notes
+
+ } +
+
+ +
+
+
Lease Information
+
+
+ @if (invoice.Lease != null) + { +
+
+ +
+ +
+ @invoice.Lease.StartDate.ToString("MMM dd, yyyy") - + @invoice.Lease.EndDate.ToString("MMM dd, yyyy") +
+
+
+
+ +
+ +
@invoice.Lease.MonthlyRent.ToString("C")
+
+
+
+ } +
+
+ + @if (invoice.Payments != null && invoice.Payments.Any()) + { +
+
+
Payment History
+
+
+
+ + + + + + + + + + + @foreach (var payment in invoice.Payments.OrderByDescending(p => p.PaidOn)) + { + + + + + + + } + +
DateAmountMethodNotes
@payment.PaidOn.ToString("MMM dd, yyyy")@payment.Amount.ToString("C")@payment.PaymentMethod@payment.Notes
+
+
+
+ } +
+ +
+
+
+
Quick Actions
+
+
+
+ + @if (invoice.Status != "Paid" && invoice.BalanceDue > 0) + { + + } + + @if (invoice.DocumentId == null) + { + + } + else + { + + + } +
+
+
+ +
+
+
Metadata
+
+
+
+ Created By: +
@(!string.IsNullOrEmpty(invoice.CreatedBy) ? invoice.CreatedBy : "System")
+
+ @if (invoice.LastModifiedOn.HasValue) + { +
+ Last Modified: +
@invoice.LastModifiedOn.Value.ToString("MMM dd, yyyy h:mm tt")
+
+
+ Modified By: +
@(!string.IsNullOrEmpty(invoice.LastModifiedBy) ? invoice.LastModifiedBy : "System")
+
+ } +
+
+
+
+} + +@code { + [Parameter] + public Guid Id { get; set; } + + private Invoice? invoice; + private bool isGenerating = false; + private Document? document = null; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadInvoice(); + } + + private async Task LoadInvoice() + { + invoice = await InvoiceService.GetByIdAsync(Id); + + // Load the document if it exists + if (invoice?.DocumentId != null) + { + document = await DocumentService.GetByIdAsync(invoice.DocumentId.Value); + } + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + "Paid" => "bg-success", + "Pending" => "bg-warning", + "Overdue" => "bg-danger", + "Cancelled" => "bg-secondary", + _ => "bg-info" + }; + } + + private void BackToList() + { + NavigationManager.NavigateTo("/propertymanagement/invoices"); + } + + private void EditInvoice() + { + NavigationManager.NavigateTo($"/propertymanagement/invoices/edit/{Id}"); + } + + private void RecordPayment() + { + NavigationManager.NavigateTo($"/propertymanagement/payments/create?invoiceId={Id}"); + } + + private void ViewLease() + { + if (invoice?.LeaseId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/view/{invoice.LeaseId}"); + } + } + + private async Task ViewDocument() + { + if (document != null) + { + var base64Data = Convert.ToBase64String(document.FileData); + await JSRuntime.InvokeVoidAsync("viewFile", base64Data, document.FileType); + } + } + + private async Task DownloadDocument() + { + if (document != null) + { + var fileName = document.FileName; + var fileData = document.FileData; + var mimeType = document.FileType; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(fileData), mimeType); + } + } + + private async Task GenerateInvoicePdf() + { + isGenerating = true; + StateHasChanged(); + + try + { + // Generate the PDF + byte[] pdfBytes = Aquiis.Professional.Application.Services.PdfGenerators.InvoicePdfGenerator.GenerateInvoicePdf(invoice!); + + // Create the document entity + var document = new Document + { + FileName = $"Invoice_{invoice!.InvoiceNumber?.Replace(" ", "_")}_{DateTime.Now:yyyyMMdd}.pdf", + FileExtension = ".pdf", + FileData = pdfBytes, + FileSize = pdfBytes.Length, + FileType = "application/pdf", + ContentType = "application/pdf", + DocumentType = "Invoice", + Description = $"Invoice {invoice.InvoiceNumber}", + LeaseId = invoice.LeaseId, + PropertyId = invoice.Lease?.PropertyId, + TenantId = invoice.Lease?.TenantId, + IsDeleted = false + }; + + // Save to database + await DocumentService.CreateAsync(document); + + // Update invoice with DocumentId + invoice.DocumentId = document.Id; + + await InvoiceService.UpdateAsync(invoice); + + // Reload invoice and document + await LoadInvoice(); + StateHasChanged(); + + await JSRuntime.InvokeVoidAsync("alert", "Invoice PDF generated successfully!"); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"Error generating invoice PDF: {ex.Message}"); + } + finally + { + isGenerating = false; + StateHasChanged(); + } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/LeaseOffers.razor b/Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/LeaseOffers.razor new file mode 100644 index 0000000..c3e68f3 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/LeaseOffers.razor @@ -0,0 +1,165 @@ +@page "/propertymanagement/leaseoffers" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization + +@inject NavigationManager Navigation +@inject UserContextService UserContext +@inject RentalApplicationService RentalApplicationService +@inject LeaseOfferService LeaseOfferService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Lease Offers - Property Management + +
+

Lease Offers

+
+ +@if (isLoading) +{ +
+
+ Loading... +
+
+} +else if (!leaseOffers.Any()) +{ +
+

No Lease Offers

+

There are currently no lease offers in the system.

+
+} +else +{ +
+
+
+ + + + + + + + + + + + + + @foreach (var offer in leaseOffers) + { + + + + + + + + + + } + +
PropertyProspective TenantOffered OnExpires OnMonthly RentStatusActions
+ @offer.Property?.Address
+ @offer.Property?.City, @offer.Property?.State +
+ @offer.ProspectiveTenant?.FullName
+ @offer.ProspectiveTenant?.Email +
@offer.OfferedOn.ToString("MMM dd, yyyy") + @offer.ExpiresOn.ToString("MMM dd, yyyy") + @if (offer.ExpiresOn < DateTime.UtcNow && offer.Status == "Pending") + { +
Expired + } + else if (offer.Status == "Pending" && (offer.ExpiresOn - DateTime.UtcNow).TotalDays <= 7) + { +
Expires Soon + } +
@offer.MonthlyRent.ToString("C") + @offer.Status + + + @if (offer.Status == "Accepted" && offer.ConvertedLeaseId.HasValue) + { + + } +
+
+
+
+} + +@code { + private List leaseOffers = new(); + private bool isLoading = true; + private Guid organizationId = Guid.Empty; + + protected override async Task OnInitializedAsync() + { + try + { + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + await LoadLeaseOffers(); + } + catch (Exception ex) + { + Console.WriteLine($"Error loading lease offers: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task LoadLeaseOffers() + { + // Get all lease offers for the organization + var allOffers = new List(); + + // We'll need to get offers from all applications + var applications = await RentalApplicationService.GetAllAsync(); + + foreach (var app in applications) + { + var offer = await LeaseOfferService.GetLeaseOfferByApplicationIdAsync(app.Id); + if (offer != null && !offer.IsDeleted) + { + allOffers.Add(offer); + } + } + + leaseOffers = allOffers.OrderByDescending(o => o.OfferedOn).ToList(); + } + + private void ViewOffer(Guid offerId) + { + Navigation.NavigateTo($"/propertymanagement/leaseoffers/view/{offerId}"); + } + + private void ViewLease(Guid leaseId) + { + Navigation.NavigateTo($"/propertymanagement/leases/view/{leaseId}"); + } + + private string GetStatusBadgeClass(string status) => status switch + { + "Pending" => "bg-warning", + "Accepted" => "bg-success", + "Declined" => "bg-danger", + "Expired" => "bg-secondary", + "Withdrawn" => "bg-dark", + _ => "bg-secondary" + }; +} diff --git a/Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor b/Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor new file mode 100644 index 0000000..bcbef55 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor @@ -0,0 +1,574 @@ +@page "/propertymanagement/leaseoffers/view/{Id:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Application.Services.Workflows +@using Aquiis.Professional.Infrastructure.Data +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.EntityFrameworkCore +@using System.ComponentModel.DataAnnotations + +@inject LeaseOfferService LeaseOfferService +@inject ApplicationWorkflowService WorkflowService +@inject ApplicationDbContext DbContext +@inject NavigationManager Navigation +@inject AuthenticationStateProvider AuthStateProvider +@inject UserContextService UserContext +@inject ToastService ToastService +@inject SecurityDepositService SecurityDepositService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Lease Offer Details + +
+
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (leaseOffer == null) + { +
+

Lease Offer Not Found

+

The lease offer you are trying to view does not exist or you do not have permission to access it.

+
+ Return to Applications +
+ } + else + { +
+
+
+
+
+

Lease Offer Details

+ @leaseOffer.Status +
+
+
+ @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + +
+
Property & Prospective Tenant
+
+
+ Property: +

@leaseOffer.Property?.Address

+ @leaseOffer.Property?.City, @leaseOffer.Property?.State +
+
+ Prospective Tenant: +

@leaseOffer.ProspectiveTenant?.FullName

+ @leaseOffer.ProspectiveTenant?.Email +
+
+
+ + +
+
Lease Terms
+
+
+ Start Date: +

@leaseOffer.StartDate.ToString("MMMM dd, yyyy")

+
+
+ End Date: +

@leaseOffer.EndDate.ToString("MMMM dd, yyyy")

+ Duration: @CalculateDuration() months +
+
+ Monthly Rent: +

@leaseOffer.MonthlyRent.ToString("C")

+
+
+ Security Deposit: +

@leaseOffer.SecurityDeposit.ToString("C")

+
+
+
+ + +
+
Offer Status
+
+
+ Offered On: +

@leaseOffer.OfferedOn.ToString("MMMM dd, yyyy")

+
+
+ Expires On: +

@leaseOffer.ExpiresOn.ToString("MMMM dd, yyyy")

+ @if (leaseOffer.ExpiresOn < DateTime.UtcNow && leaseOffer.Status == "Pending") + { + Expired + } + else if ((leaseOffer.ExpiresOn - DateTime.UtcNow).TotalDays < 7 && leaseOffer.Status == "Pending") + { + Expires Soon + } +
+ @if (leaseOffer.RespondedOn.HasValue) + { +
+ Responded On: +

@leaseOffer.RespondedOn?.ToString("MMMM dd, yyyy")

+
+ } + @if (!string.IsNullOrEmpty(leaseOffer.ResponseNotes)) + { +
+ Response Notes: +

@leaseOffer.ResponseNotes

+
+ } +
+
+ + + @if (!string.IsNullOrEmpty(leaseOffer.Terms)) + { +
+
Terms & Conditions
+
@leaseOffer.Terms
+
+ } + + + @if (!string.IsNullOrEmpty(leaseOffer.Notes)) + { +
+
Internal Notes
+
+ + (Not visible to tenant) +

@leaseOffer.Notes

+
+
+ } + + +
+ +
+ @if (leaseOffer.Status == "Pending" && leaseOffer.ExpiresOn > DateTime.UtcNow) + { + + + } + @if (leaseOffer.ConvertedLeaseId.HasValue) + { + + } +
+
+
+
+
+ +
+
+
+
Workflow Status
+
+
+
+
+ + Application Approved +
+
+ + Lease Offer Generated +
+
+ + Awaiting Response +
+
+ + Converted to Lease +
+
+
+
+ + @if (leaseOffer.RentalApplication != null) + { +
+
+
Application Info
+
+
+

Applied On:
@leaseOffer.RentalApplication.AppliedOn.ToString("MMM dd, yyyy")

+

Application Fee:
@leaseOffer.RentalApplication.ApplicationFee.ToString("C")

+

Monthly Income:
@leaseOffer.RentalApplication.MonthlyIncome.ToString("C")

+
+
+ } +
+
+ } +
+ + +@if (showAcceptModal) +{ + +} + + +@if (showDeclineModal) +{ + +} + + + +@code { + [Parameter] + public Guid Id { get; set; } + + private LeaseOffer? leaseOffer; + private bool isLoading = true; + private bool isSubmitting = false; + private bool showAcceptModal = false; + private bool showDeclineModal = false; + private string errorMessage = string.Empty; + private string declineReason = string.Empty; + private string userId = string.Empty; + private Guid organizationId = Guid.Empty; + private DepositPaymentModel depositPaymentModel = new(); + + protected override async Task OnInitializedAsync() + { + try + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? string.Empty; + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + + await LoadLeaseOffer(); + } + catch (Exception ex) + { + errorMessage = $"Error loading lease offer: {ex.Message}"; + } + finally + { + isLoading = false; + } + } + + private async Task LoadLeaseOffer() + { + leaseOffer = await LeaseOfferService.GetLeaseOfferWithRelationsAsync(Id); + } + + private int CalculateDuration() + { + if (leaseOffer == null) return 0; + + var months = ((leaseOffer.EndDate.Year - leaseOffer.StartDate.Year) * 12) + + leaseOffer.EndDate.Month - leaseOffer.StartDate.Month; + return months; + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + "Pending" => "bg-warning", + "Accepted" => "bg-success", + "Declined" => "bg-danger", + "Expired" => "bg-secondary", + "Withdrawn" => "bg-dark", + _ => "bg-secondary" + }; + } + + private async Task AcceptOffer() + { + if (leaseOffer == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + var result = await WorkflowService.AcceptLeaseOfferAsync( + leaseOffer.Id, + depositPaymentModel.PaymentMethod, + depositPaymentModel.PaymentDate, + depositPaymentModel.ReferenceNumber, + depositPaymentModel.Notes); + + if (result.Success) + { + ToastService.ShowSuccess($"Lease offer accepted! Security deposit of {leaseOffer.SecurityDeposit:C} collected via {depositPaymentModel.PaymentMethod}."); + showAcceptModal = false; + depositPaymentModel = new(); + + // Navigate to the newly created lease + if (result.Data != null) + { + Navigation.NavigateTo($"/propertymanagement/leases/view/{result.Data.Id}"); + } + } + else + { + errorMessage = string.Join(", ", result.Errors); + ToastService.ShowError($"Failed to accept lease offer: {errorMessage}"); + } + } + catch (Exception ex) + { + var innerMessage = ex.InnerException?.Message ?? ex.Message; + var fullMessage = ex.InnerException != null + ? $"{ex.Message} | Inner: {innerMessage}" + : ex.Message; + errorMessage = $"Error accepting lease offer: {fullMessage}"; + ToastService.ShowError($"Failed to accept lease offer: {fullMessage}"); + } + finally + { + isSubmitting = false; + } + } + + private async Task DeclineOffer() + { + if (leaseOffer == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + var result = await WorkflowService.DeclineLeaseOfferAsync(leaseOffer.Id, declineReason ?? "Declined by applicant"); + + if (result.Success) + { + ToastService.ShowSuccess("Lease offer declined."); + showDeclineModal = false; + await LoadLeaseOffer(); + } + else + { + errorMessage = string.Join(", ", result.Errors); + } + } + catch (Exception ex) + { + errorMessage = $"Error declining offer: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private void ViewConvertedLease() + { + if (leaseOffer?.ConvertedLeaseId.HasValue == true) + { + Navigation.NavigateTo($"/propertymanagement/leases/view/{leaseOffer.ConvertedLeaseId.Value}"); + } + } + + private void BackToApplication() + { + if (leaseOffer?.RentalApplicationId != null) + { + Navigation.NavigateTo($"/propertymanagement/applications/{leaseOffer.RentalApplicationId}/review"); + } + else + { + Navigation.NavigateTo("/propertymanagement/applications"); + } + } + + public class DepositPaymentModel + { + [Required(ErrorMessage = "Payment method is required")] + public string PaymentMethod { get; set; } = string.Empty; + + public string? ReferenceNumber { get; set; } + + public DateTime PaymentDate { get; set; } = DateTime.Today; + + public string? Notes { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/AcceptLease.razor b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/AcceptLease.razor new file mode 100644 index 0000000..748639d --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/AcceptLease.razor @@ -0,0 +1,637 @@ +@page "/propertymanagement/leases/{LeaseId:guid}/accept" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@using System.Net +@using System.ComponentModel.DataAnnotations + +@inject LeaseService LeaseService +@inject PropertyService PropertyService +@inject RentalApplicationService RentalApplicationService +@inject ProspectiveTenantService ProspectiveTenantService +@inject TenantConversionService TenantConversionService +@inject NavigationManager Navigation +@inject AuthenticationStateProvider AuthStateProvider +@inject UserContextService UserContext +@inject ToastService ToastService +@inject SecurityDepositService SecurityDepositService +@inject IHttpContextAccessor HttpContextAccessor + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Accept Lease Offer + +
+
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (lease == null) + { +
+

Lease Not Found

+

The lease you are trying to access does not exist or you do not have permission to view it.

+
+ Return to Leases +
+ } + else if (lease.Status != "Offered") + { +
+

Invalid Lease Status

+

This lease cannot be accepted. Current status: @lease.Status

+
+ View Lease +
+ } + else if (isExpired) + { +
+

Lease Offer Expired

+

This lease offer expired on @lease.ExpiresOn?.ToString("MMM dd, yyyy"). A new offer must be generated.

+
+ Return to Leases +
+ } + else + { +
+
+
+
+

Accept Lease Offer

+
+
+ @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + +
+ + Offer Expires: @lease.ExpiresOn?.ToString("MMM dd, yyyy h:mm tt") + (@GetTimeRemaining()) +
+ +
+
Lease Summary
+
+
+ Property:
+ @lease.Property?.Address +
+
+ Lease Term:
+ @lease.StartDate.ToString("MMM dd, yyyy") - @lease.EndDate.ToString("MMM dd, yyyy") +
+
+ Monthly Rent:
+ @lease.MonthlyRent.ToString("C") +
+
+ Security Deposit:
+ @lease.SecurityDeposit.ToString("C") +
+
+
+ +
+
Lease Terms & Conditions
+
+
+
@lease.Terms
+
+
+
+ + + + +
+
Prospective Tenant Information
+ @if (prospectiveTenant != null) + { +
+
+ Name: @prospectiveTenant.FullName +
+
+ Email: @prospectiveTenant.Email +
+
+ Phone: @prospectiveTenant.Phone +
+
+ Date of Birth: @prospectiveTenant.DateOfBirth?.ToString("MMM dd, yyyy") +
+
+ } + else + { +
+ Unable to load prospective tenant information. +
+ } +
+ +
+
Signature & Acceptance
+ +
+ + + +
+ +
+ + + +
+ +
+ + + + @foreach (var method in ApplicationConstants.PaymentMethods.AllPaymentMethods) + { + + } + + +
+ +
+ + + Check number, bank transfer confirmation, etc. +
+ +
+ + +
+
+ +
+ + Audit Trail: This acceptance will be recorded with the following information: +
    +
  • Acceptance timestamp: @DateTime.UtcNow.ToString("MMM dd, yyyy h:mm:ss tt UTC")
  • +
  • IP Address: @GetClientIpAddress()
  • +
  • Processed by: @userId
  • +
+
+ +
+ +
+ + +
+
+
+
+
+
+ +
+
+
+
Next Steps
+
+
+
    +
  1. Tenant record will be created automatically
  2. +
  3. Security deposit will be recorded and tracked
  4. +
  5. Deposit added to investment pool when lease starts
  6. +
  7. Property status will update to "Occupied"
  8. +
  9. All competing applications will be denied
  10. +
  11. Move-in inspection will be scheduled
  12. +
+
+
+ + @if (lease.Property != null) + { +
+
+
Property Details
+
+
+

Type: @lease.Property.PropertyType

+

Bedrooms: @lease.Property.Bedrooms

+

Bathrooms: @lease.Property.Bathrooms

+

Sq Ft: @lease.Property.SquareFeet

+
+
+ } +
+
+ } +
+ + +@if (showDeclineModal) +{ + +} + +@code { + [Parameter] + public Guid LeaseId { get; set; } + + private Lease? lease; + private ProspectiveTenant? prospectiveTenant; + private RentalApplication? application; + private LeaseAcceptanceModel acceptanceModel = new(); + private bool isLoading = true; + private bool isSubmitting = false; + private bool isExpired = false; + private bool showDeclineModal = false; + private string errorMessage = string.Empty; + private string successMessage = string.Empty; + private string declineReason = string.Empty; + private string userId = string.Empty; + private Guid organizationId = Guid.Empty; + + protected override async Task OnInitializedAsync() + { + try + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? string.Empty; + organizationId = await UserContext.GetActiveOrganizationIdAsync() ?? Guid.Empty; + + await LoadLease(); + } + catch (Exception ex) + { + errorMessage = $"Error loading lease: {ex.Message}"; + } + finally + { + isLoading = false; + } + } + + private async Task LoadLease() + { + lease = await LeaseService.GetByIdAsync(LeaseId); + + if (lease != null) + { + // Check if expired + if (lease.ExpiresOn.HasValue && lease.ExpiresOn.Value < DateTime.UtcNow) + { + isExpired = true; + } + + // Find the application and prospective tenant + if (lease.Property != null) + { + var allApplications = await RentalApplicationService.GetAllAsync(); + application = allApplications.FirstOrDefault(a => + a.PropertyId == lease.PropertyId && + a.Status == ApplicationConstants.ApplicationStatuses.Approved); + + if (application != null) + { + prospectiveTenant = await ProspectiveTenantService.GetByIdAsync( + application.ProspectiveTenantId); + } + } + } + } + + private async Task HandleAcceptLease() + { + if (lease == null || prospectiveTenant == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + // Convert prospect to tenant + var tenant = await TenantConversionService.ConvertProspectToTenantAsync( + prospectiveTenant.Id); + + if (tenant == null) + { + errorMessage = "Failed to create tenant record."; + isSubmitting = false; + ToastService.ShowError(errorMessage); + Console.WriteLine(errorMessage); + return; + } + + // CRITICAL: Collect security deposit - this MUST succeed before proceeding + SecurityDeposit securityDeposit; + try + { + securityDeposit = await SecurityDepositService.CollectSecurityDepositAsync( + lease.Id, + lease.SecurityDeposit, + acceptanceModel.PaymentMethod, + acceptanceModel.TransactionReference, + tenant.Id); // Pass the newly created tenant ID + } + catch (Exception depositEx) + { + var message = depositEx.Message + (depositEx.InnerException != null ? $" Inner: {depositEx.InnerException.Message}" : string.Empty); + errorMessage = $"CRITICAL ERROR: Failed to collect security deposit. Lease acceptance aborted. Error: {message}"; + isSubmitting = false; + ToastService.ShowError(errorMessage); + return; + } + + if(securityDeposit == null || securityDeposit.Id == Guid.Empty) + { + errorMessage = "CRITICAL ERROR: Security deposit record not created. Lease acceptance aborted."; + isSubmitting = false; + ToastService.ShowError(errorMessage); + return; + } + + // Add deposit to investment pool (will start earning dividends) + try + { + await SecurityDepositService.AddToInvestmentPoolAsync(securityDeposit.Id); + } + catch (Exception poolEx) + { + // Non-critical: deposit collected but not added to pool yet + // Can be added manually later from Security Deposits page + var message = poolEx.Message + (poolEx.InnerException != null ? $" - {poolEx.InnerException}" : string.Empty); + ToastService.ShowWarning($"Security deposit collected but not added to investment pool: {message}"); + } + // Update lease with tenant ID and signed status + lease.TenantId = tenant.Id; + lease.Status = ApplicationConstants.LeaseStatuses.Active; + lease.SignedOn = DateTime.UtcNow; + lease.Notes += $"\n\nAccepted on: {DateTime.UtcNow:MMM dd, yyyy h:mm tt UTC}\n" + + $"IP Address: {GetClientIpAddress()}\n" + + $"Security Deposit: {lease.SecurityDeposit:C} ({acceptanceModel.PaymentMethod})\n" + + $"Transaction Ref: {acceptanceModel.TransactionReference ?? "N/A"}\n" + + $"Processed by: {userId}"; + + if (!string.IsNullOrWhiteSpace(acceptanceModel.Notes)) + { + lease.Notes += $"\nAcceptance Notes: {acceptanceModel.Notes}"; + } + + + await LeaseService.UpdateAsync(lease); + + // Update property status to Occupied + if (lease.Property != null) + { + lease.Property.Status = ApplicationConstants.PropertyStatuses.Occupied; + lease.Property.IsAvailable = false; + + await PropertyService.UpdateAsync(lease.Property); + } + + // Update application status + if (application != null) + { + application.Status = "LeaseAccepted"; // We'll add this status + + await RentalApplicationService.UpdateAsync(application); + } + + // Update prospect status to ConvertedToTenant + prospectiveTenant.Status = ApplicationConstants.ProspectiveStatuses.ConvertedToTenant; + + await ProspectiveTenantService.UpdateAsync(prospectiveTenant); + + ToastService.ShowSuccess($"Lease accepted! Tenant {tenant.FullName} created successfully."); + + // Navigate to the tenant view + Navigation.NavigateTo($"/propertymanagement/tenants/{tenant.Id}"); + } + catch (Exception ex) + { + var message = ex.Message + (ex.InnerException != null ? $" - {ex.InnerException}" : string.Empty); + errorMessage = $"Error accepting lease: {message}"; + ToastService.ShowError(errorMessage); + Console.WriteLine(errorMessage); + } + finally + { + isSubmitting = false; + } + } + + private async Task HandleDeclineLease() + { + if (lease == null) return; + + isSubmitting = true; + errorMessage = string.Empty; + + try + { + // Update lease status to declined + lease.Status = "Declined"; + lease.DeclinedOn = DateTime.UtcNow; + lease.Notes += $"\n\nDeclined on: {DateTime.UtcNow:MMM dd, yyyy h:mm tt UTC}\n" + + $"Declined by: {userId}\n" + + $"Reason: {(string.IsNullOrWhiteSpace(declineReason) ? "Not specified" : declineReason)}"; + await LeaseService.UpdateAsync(lease); + + // Check if there are other pending applications + var allApplications = await RentalApplicationService.GetAllAsync(); + var otherPendingApps = allApplications.Any(a => + a.PropertyId == lease.PropertyId && + (a.Status == ApplicationConstants.ApplicationStatuses.Submitted || + a.Status == ApplicationConstants.ApplicationStatuses.UnderReview || + a.Status == ApplicationConstants.ApplicationStatuses.Screening)); + + // Update property status + if (lease.Property != null) + { + lease.Property.Status = otherPendingApps + ? ApplicationConstants.PropertyStatuses.ApplicationPending + : ApplicationConstants.PropertyStatuses.Available; + lease.Property.IsAvailable = !otherPendingApps; + + await PropertyService.UpdateAsync(lease.Property); + } + + // Update application and prospect status + if (application != null) + { + application.Status = "LeaseDeclined"; + + await RentalApplicationService.UpdateAsync(application); + } + + if (prospectiveTenant != null) + { + prospectiveTenant.Status = "LeaseDeclined"; + + await ProspectiveTenantService.UpdateAsync(prospectiveTenant); + } + + showDeclineModal = false; + ToastService.ShowInfo("Lease offer declined."); + Navigation.NavigateTo("/propertymanagement/leases"); + } + catch (Exception ex) + { + errorMessage = $"Error declining lease: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private string GetTimeRemaining() + { + if (lease?.ExpiresOn == null) return "N/A"; + + var timeSpan = lease.ExpiresOn.Value - DateTime.UtcNow; + + if (timeSpan.TotalDays > 1) + return $"{(int)timeSpan.TotalDays} days remaining"; + else if (timeSpan.TotalHours > 1) + return $"{(int)timeSpan.TotalHours} hours remaining"; + else if (timeSpan.TotalMinutes > 0) + return $"{(int)timeSpan.TotalMinutes} minutes remaining"; + else + return "Expired"; + } + + private string GetClientIpAddress() + { + try + { + var context = HttpContextAccessor.HttpContext; + if (context != null) + { + var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + if (!string.IsNullOrEmpty(forwardedFor)) + { + return forwardedFor.Split(',')[0].Trim(); + } + return context.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; + } + } + catch { } + + return "Unknown"; + } + + private void Cancel() + { + Navigation.NavigateTo($"/propertymanagement/leases/{LeaseId}"); + } + + public class LeaseAcceptanceModel + { + [Required(ErrorMessage = "You must confirm that the tenant agrees to the terms")] + public bool AgreesToTerms { get; set; } + + [Required(ErrorMessage = "You must confirm that the security deposit has been paid")] + public bool SecurityDepositPaid { get; set; } + + [Required(ErrorMessage = "Payment method is required")] + public string PaymentMethod { get; set; } = string.Empty; + + [StringLength(100)] + public string? TransactionReference { get; set; } + + [StringLength(1000)] + public string? Notes { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/CreateLease.razor b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/CreateLease.razor new file mode 100644 index 0000000..bc1796e --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/CreateLease.razor @@ -0,0 +1,383 @@ +@page "/propertymanagement/leases/create" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Core.Validation +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations + +@inject NavigationManager Navigation +@inject OrganizationService OrganizationService +@inject LeaseService LeaseService +@inject PropertyService PropertyService +@inject TenantService TenantService + +@inject AuthenticationStateProvider AuthenticationStateProvider + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Create Lease + +
+
+
+
+

Create New Lease

+
+
+ + + + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + +
+
+ + + + @foreach (var property in availableProperties) + { + + } + + +
+
+ + + + @foreach (var tenant in userTenants) + { + + } + + +
+
+ + @if (selectedProperty != null) + { +
+ Selected Property: @selectedProperty.Address
+ Monthly Rent: @selectedProperty.MonthlyRent.ToString("C") +
+ } + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + @foreach (var status in ApplicationConstants.LeaseStatuses.AllLeaseStatuses) + { + + } + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
Quick Actions
+
+
+
+ +
+
+
+ + @if (selectedProperty != null) + { +
+
+
Property Details
+
+
+

Address: @selectedProperty.Address

+

Type: @selectedProperty.PropertyType

+

Bedrooms: @selectedProperty.Bedrooms

+

Bathrooms: @selectedProperty.Bathrooms

+

Square Feet: @selectedProperty.SquareFeet.ToString("N0")

+
+
+ } +
+
+ +@code { + [Parameter] + [SupplyParameterFromQuery] + public Guid? PropertyId { get; set; } + + [Parameter] + [SupplyParameterFromQuery] + public Guid? TenantId { get; set; } + + private LeaseModel leaseModel = new(); + private List availableProperties = new(); + private List userTenants = new(); + private Property? selectedProperty; + private bool isSubmitting = false; + private string errorMessage = string.Empty; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + + // If PropertyId is provided in query string, pre-select it + if (PropertyId.HasValue) + { + leaseModel.PropertyId = PropertyId.Value; + await OnPropertyChanged(); + } + + // If TenantId is provided in query string, pre-select it + if (TenantId.HasValue) + { + leaseModel.TenantId = TenantId.Value; + } + } + + private async Task LoadData() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + return; + + // Load available properties (only available ones) + List? allProperties = await PropertyService.GetAllAsync(); + + availableProperties = allProperties + .Where(p => p.IsAvailable) + .ToList() ?? new List(); + + // Load user's tenants + userTenants = await TenantService.GetAllAsync(); + userTenants = userTenants + .Where(t => t.IsActive) + .ToList(); + + // Set default values + leaseModel.StartDate = DateTime.Today; + leaseModel.EndDate = DateTime.Today.AddYears(1); + leaseModel.Status = ApplicationConstants.LeaseStatuses.Active; + } + + private async Task OnPropertyChanged() + { + if (leaseModel.PropertyId != Guid.Empty) + { + selectedProperty = availableProperties.FirstOrDefault(p => p.Id == leaseModel.PropertyId); + if (selectedProperty != null) + { + // Get organization settings for security deposit calculation + var settings = await OrganizationService.GetOrganizationSettingsAsync(); + var depositMultiplier = settings?.AutoCalculateSecurityDeposit == true + ? settings.SecurityDepositMultiplier + : 1.0m; + + leaseModel.PropertyAddress = selectedProperty.Address; + leaseModel.MonthlyRent = selectedProperty.MonthlyRent; + leaseModel.SecurityDeposit = selectedProperty.MonthlyRent * depositMultiplier; + } + } + else + { + selectedProperty = null; + } + StateHasChanged(); + } + + private async Task HandleValidSubmit() + { + try + { + isSubmitting = true; + errorMessage = string.Empty; + + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + errorMessage = "User not authenticated."; + return; + } + + // Verify property and tenant belong to user + var property = await PropertyService.GetByIdAsync(leaseModel.PropertyId); + var tenant = await TenantService.GetByIdAsync(leaseModel.TenantId); + + if (property == null) + { + errorMessage = $"Property with ID {leaseModel.PropertyId} not found or access denied."; + return; + } + + if (tenant == null) + { + errorMessage = $"Tenant with ID {leaseModel.TenantId} not found or access denied."; + return; + } + + var lease = new Lease + { + + PropertyId = leaseModel.PropertyId, + TenantId = leaseModel.TenantId, + StartDate = leaseModel.StartDate, + EndDate = leaseModel.EndDate, + MonthlyRent = leaseModel.MonthlyRent, + SecurityDeposit = leaseModel.SecurityDeposit, + Status = leaseModel.Status, + Terms = leaseModel.Terms, + Notes = leaseModel.Notes + }; + + await LeaseService.CreateAsync(lease); + + // Mark property as unavailable if lease is active + if (leaseModel.Status == ApplicationConstants.LeaseStatuses.Active) + { + property.IsAvailable = false; + } + + await PropertyService.UpdateAsync(property); + + Navigation.NavigateTo("/propertymanagement/leases"); + } + catch (Exception ex) + { + errorMessage = $"Error creating lease: {ex.Message}"; + if (ex.InnerException != null) + { + errorMessage += $" Inner Exception: {ex.InnerException.Message}"; + } + } + finally + { + isSubmitting = false; + } + } + + private void CreateTenant() + { + Navigation.NavigateTo("/propertymanagement/tenants/create"); + } + + private void Cancel() + { + Navigation.NavigateTo("/propertymanagement/leases"); + } + + public class LeaseModel + { + [RequiredGuid(ErrorMessage = "Property is required")] + public Guid PropertyId { get; set; } + + public string PropertyAddress { get; set; } = string.Empty; + + [RequiredGuid(ErrorMessage = "Tenant is required")] + public Guid TenantId { get; set; } + + [Required(ErrorMessage = "Start date is required")] + public DateTime StartDate { get; set; } = DateTime.Today; + + [Required(ErrorMessage = "End date is required")] + public DateTime EndDate { get; set; } = DateTime.Today.AddYears(1); + + [Required(ErrorMessage = "Monthly rent is required")] + [Range(0.01, double.MaxValue, ErrorMessage = "Monthly rent must be greater than 0")] + public decimal MonthlyRent { get; set; } + + [Range(0, double.MaxValue, ErrorMessage = "Security deposit cannot be negative")] + public decimal SecurityDeposit { get; set; } + + [Required(ErrorMessage = "Status is required")] + [StringLength(50)] + public string Status { get; set; } = ApplicationConstants.LeaseStatuses.Active; + + [StringLength(1000, ErrorMessage = "Terms cannot exceed 1000 characters")] + public string Terms { get; set; } = string.Empty; + + [StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")] + public string Notes { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/EditLease.razor b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/EditLease.razor new file mode 100644 index 0000000..ef47db0 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/EditLease.razor @@ -0,0 +1,357 @@ +@page "/propertymanagement/leases/edit/{Id:guid}" + +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Web +@using Aquiis.Professional.Features.PropertyManagement +@using System.ComponentModel.DataAnnotations + +@inject NavigationManager Navigation +@inject LeaseService LeaseService +@inject UserContextService UserContextService + +@rendermode InteractiveServer + + +@if (lease == null) +{ +
+
+ Loading... +
+
+} +else if (!isAuthorized) +{ +
+

Access Denied

+

You don't have permission to edit this lease.

+ Back to Leases +
+} +else +{ +
+
+
+
+

Edit Lease

+
+
+ + + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + +
+
+ + + Property cannot be changed for existing lease +
+
+ + + Tenant cannot be changed for existing lease +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + + + + + + +
+
+ +
+
+ + + +
+
+ +
+
+ +
+
+ +
+ + + +
+
+
+
+
+ +
+
+
+
Lease Actions
+
+
+
+ + + +
+
+
+ +
+
+
Lease Information
+
+
+ + Created: @lease.CreatedOn.ToString("MMMM dd, yyyy") +
+ @if (lease.LastModifiedOn.HasValue) + { + Last Modified: @lease.LastModifiedOn.Value.ToString("MMMM dd, yyyy") + } +
+
+
+ + @if (statusChangeWarning) + { +
+
+
+ + Note: Changing the lease status may affect property availability. +
+
+
+ } +
+
+} + +@code { + [Parameter] public Guid Id { get; set; } + + private Lease? lease; + private LeaseModel leaseModel = new(); + private bool isSubmitting = false; + private bool isAuthorized = true; + private bool statusChangeWarning = false; + private string errorMessage = string.Empty; + private string successMessage = string.Empty; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadLease(); + } + + private async Task LoadLease() + { + lease = await LeaseService.GetByIdAsync(Id); + + if (lease == null) + { + isAuthorized = false; + return; + } + + // Map lease to model + leaseModel = new LeaseModel + { + StartDate = lease.StartDate, + EndDate = lease.EndDate, + MonthlyRent = lease.MonthlyRent, + SecurityDeposit = lease.SecurityDeposit, + Status = lease.Status, + Terms = lease.Terms, + }; + } + + private async Task UpdateLease() + { + try + { + isSubmitting = true; + errorMessage = string.Empty; + successMessage = string.Empty; + + var oldStatus = lease!.Status; + + // Update lease with form data + lease.StartDate = leaseModel.StartDate; + lease.EndDate = leaseModel.EndDate; + lease.MonthlyRent = leaseModel.MonthlyRent; + lease.SecurityDeposit = leaseModel.SecurityDeposit; + lease.Status = leaseModel.Status; + lease.Terms = leaseModel.Terms; + + // Update property availability based on lease status change + if (lease.Property != null && oldStatus != leaseModel.Status) + { + if (leaseModel.Status == "Active") + { + lease.Property.IsAvailable = false; + } + else if (oldStatus == "Active" && leaseModel.Status != "Active") + { + // Check if there are other active leases for this property + var activeLeases = await LeaseService.GetActiveLeasesByPropertyIdAsync(lease.PropertyId); + var otherActiveLeases = activeLeases.Any(l => l.PropertyId == lease.PropertyId && l.Id != Id && l.Status == "Active"); + + if (!otherActiveLeases) + { + lease.Property.IsAvailable = true; + } + } + } + + await LeaseService.UpdateAsync(lease); + successMessage = "Lease updated successfully!"; + statusChangeWarning = false; + } + catch (Exception ex) + { + errorMessage = $"Error updating lease: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private void OnStatusChanged() + { + statusChangeWarning = true; + StateHasChanged(); + } + + private void ViewLease() + { + Navigation.NavigateTo($"/propertymanagement/leases/view/{Id}"); + } + + private void CreateInvoice() + { + Navigation.NavigateTo($"/propertymanagement/invoices/create?leaseId={Id}"); + } + + private void Cancel() + { + Navigation.NavigateTo("/propertymanagement/leases"); + } + + private async Task DeleteLease() + { + if (lease != null) + { + try + { + // If deleting an active lease, make property available + if (lease.Status == "Active" && lease.Property != null) + { + var otherActiveLeases = await LeaseService.GetActiveLeasesByPropertyIdAsync(lease.PropertyId); + var otherActiveLeasesExist = otherActiveLeases.Any(l => l.Id != Id && l.Status == "Active"); + + if (!otherActiveLeasesExist) + { + lease.Property.IsAvailable = true; + } + } + + await LeaseService.DeleteAsync(lease.Id); + Navigation.NavigateTo("/propertymanagement/leases"); + } + catch (Exception ex) + { + errorMessage = $"Error deleting lease: {ex.Message}"; + } + } + } + + public class LeaseModel + { + [Required(ErrorMessage = "Start date is required")] + public DateTime StartDate { get; set; } = DateTime.Today; + + [Required(ErrorMessage = "End date is required")] + public DateTime EndDate { get; set; } = DateTime.Today.AddYears(1); + + [Required(ErrorMessage = "Monthly rent is required")] + [Range(0.01, double.MaxValue, ErrorMessage = "Monthly rent must be greater than 0")] + public decimal MonthlyRent { get; set; } + + [Range(0.00, double.MaxValue, ErrorMessage = "Security deposit cannot be negative")] + public decimal SecurityDeposit { get; set; } + + [Required(ErrorMessage = "Status is required")] + [StringLength(50)] + public string Status { get; set; } = "Active"; + + [StringLength(5000, ErrorMessage = "Terms cannot exceed 2000 characters")] + public string Terms { get; set; } = string.Empty; + + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Leases.razor b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Leases.razor new file mode 100644 index 0000000..4ed018a --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Leases.razor @@ -0,0 +1,837 @@ +@page "/propertymanagement/leases" + +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.EntityFrameworkCore +@using Aquiis.Professional.Infrastructure.Data +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Shared.Components.Account + +@inject NavigationManager NavigationManager +@inject LeaseService LeaseService +@inject TenantService TenantService +@inject PropertyService PropertyService +@inject IJSRuntime JSRuntime + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Leases - Property Management + +
+
+

Leases

+ @if (filterTenant != null) + { +

+ Showing leases for tenant: @filterTenant.FullName + +

+ } + else if (filterProperty != null) + { +

+ Showing leases for property: @filterProperty.Address + +

+ } +
+
+ + + @if (filterTenant != null) + { + + } + else if (filterProperty != null) + { + + } +
+
+ +@if (leases == null) +{ +
+
+ Loading... +
+
+} +else if (!leases.Any()) +{ +
+ @if (filterTenant != null) + { +

No Leases Found for @filterTenant.FullName

+

This tenant doesn't have any lease agreements yet.

+ + + } + else if (filterProperty != null) + { +

No Leases Found for @filterProperty.Address

+

This property doesn't have any lease agreements yet.

+ + + } + else + { +

No Leases Found

+

Get started by converting a lease offer to your first lease agreement.

+ + } +
+} +else +{ +
+
+
+ + +
+
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+
+
Active Leases
+

@activeCount

+
+
+
+
+
+
+
Expiring Soon
+

@expiringSoonCount

+
+
+
+
+
+
+
Total Rent/Month
+

@totalMonthlyRent.ToString("C")

+
+
+
+
+
+
+
Total Leases
+

@filteredLeases.Count

+
+
+
+
+ +
+
+ @if (groupByProperty) + { + @foreach (var propertyGroup in groupedLeases) + { + var property = propertyGroup.First().Property; + var propertyLeaseCount = propertyGroup.Count(); + var activeLeaseCount = propertyGroup.Count(l => l.Status == "Active"); + var isExpanded = expandedProperties.Contains(propertyGroup.Key.GetHashCode()); + +
+
+
+
+ + @property?.Address + @property?.City, @property?.State @property?.ZipCode +
+
+ @activeLeaseCount active + @propertyLeaseCount total lease(s) +
+
+
+ @if (isExpanded) + { +
+ + + + + + + + + + + + + @foreach (var lease in propertyGroup) + { + + + + + + + + + } + +
TenantStart DateEnd DateMonthly RentStatusActions
+ @if (lease.Tenant != null) + { + @lease.Tenant.FullName +
+ @lease.Tenant.Email + } + else + { + Pending Acceptance +
+ Lease offer awaiting tenant + } +
@lease.StartDate.ToString("MMM dd, yyyy")@lease.EndDate.ToString("MMM dd, yyyy")@lease.MonthlyRent.ToString("C") + + @lease.Status + + @if (lease.IsActive && lease.DaysRemaining <= 30 && lease.DaysRemaining > 0) + { +
+ @lease.DaysRemaining days remaining + } +
+
+ + + +
+
+
+ } +
+ } + } + else + { +
+ + + + + + + + + + + + @foreach (var lease in pagedLeases) + { + + + + + + + + + + } + +
+ + + + + + + + + + + + Actions
+ @lease.Property?.Address + @if (lease.Property != null) + { +
+ @lease.Property.City, @lease.Property.State + } +
+ @if (lease.Tenant != null) + { + @lease.Tenant.FullName +
+ @lease.Tenant.Email + } + else + { + Pending Acceptance +
+ Lease offer awaiting tenant + } +
@lease.StartDate.ToString("MMM dd, yyyy")@lease.EndDate.ToString("MMM dd, yyyy")@lease.MonthlyRent.ToString("C") + + @lease.Status + + @if (lease.IsActive && lease.DaysRemaining <= 30 && lease.DaysRemaining > 0) + { +
+ @lease.DaysRemaining days remaining + } +
+
+ + + +
+
+
+ } + @if (totalPages > 1 && !groupByProperty) + { + + } +
+
+} + +@code { + private List? leases; + private List filteredLeases = new(); + private List pagedLeases = new(); + private IEnumerable> groupedLeases = Enumerable.Empty>(); + private HashSet expandedProperties = new(); + private string searchTerm = string.Empty; + private string selectedLeaseStatus = string.Empty; + private Guid? selectedTenantId; + private List? availableTenants; + private int activeCount = 0; + private int expiringSoonCount = 0; + private decimal totalMonthlyRent = 0; + private Tenant? filterTenant; + private Property? filterProperty; + private bool groupByProperty = true; + + // Paging variables + private int currentPage = 1; + private int pageSize = 10; + private int totalPages = 1; + private int totalRecords = 0; + + // Sorting variables + private string sortColumn = "StartDate"; + private bool sortAscending = false; + + [Parameter] + [SupplyParameterFromQuery] + public Guid? TenantId { get; set; } + + [Parameter] + [SupplyParameterFromQuery] + public Guid? PropertyId { get; set; } + + [Parameter] + [SupplyParameterFromQuery] + public int? LeaseId { get; set; } + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadFilterEntities(); + await LoadLeases(); + LoadFilterOptions(); + FilterLeases(); + CalculateMetrics(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && LeaseId.HasValue) + { + await JSRuntime.InvokeVoidAsync("scrollToElement", $"lease-{LeaseId.Value}"); + } + } + + protected override async Task OnParametersSetAsync() + { + await LoadFilterEntities(); + await LoadLeases(); + LoadFilterOptions(); + FilterLeases(); + CalculateMetrics(); + StateHasChanged(); + } + + private async Task LoadFilterEntities() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) return; + + if (TenantId.HasValue) + { + filterTenant = await TenantService.GetByIdAsync(TenantId.Value); + } + + if (PropertyId.HasValue) + { + filterProperty = await PropertyService.GetByIdAsync(PropertyId.Value); + } + } + + private async Task LoadLeases() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + leases = new List(); + return; + } + + var allLeases = await LeaseService.GetAllAsync(); + leases = allLeases + .Where(l => + (!TenantId.HasValue || l.TenantId == TenantId.Value) && + (!PropertyId.HasValue || l.PropertyId == PropertyId.Value)) + .OrderByDescending(l => l.StartDate) + .ToList(); + } + + private void LoadFilterOptions() + { + if (leases != null) + { + // Load available tenants from leases + availableTenants = leases + .Where(l => l.Tenant != null) + .Select(l => l.Tenant!) + .DistinctBy(t => t.Id) + .OrderBy(t => t.FirstName) + .ThenBy(t => t.LastName) + .ToList(); + } + } + + private void FilterLeases() + { + if (leases == null) + { + filteredLeases = new(); + pagedLeases = new(); + CalculateMetrics(); + return; + } + + filteredLeases = leases.Where(l => + (string.IsNullOrEmpty(searchTerm) || + l.Property?.Address.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) == true || + (l.Tenant != null && l.Tenant.FullName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) || + l.Notes.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + l.Terms.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) && + (string.IsNullOrEmpty(selectedLeaseStatus) || l.Status == selectedLeaseStatus) && + (!selectedTenantId.HasValue || l.TenantId == selectedTenantId.Value) + ).ToList(); + + // Apply sorting + ApplySorting(); + + if (groupByProperty) + { + groupedLeases = filteredLeases + .Where(l => l.PropertyId != Guid.Empty) + .GroupBy(l => l.PropertyId) + .OrderBy(g => g.First().Property?.Address) + .ToList(); + } + else + { + // Apply paging + totalRecords = filteredLeases.Count; + totalPages = (int)Math.Ceiling((double)totalRecords / pageSize); + if (currentPage > totalPages) currentPage = Math.Max(1, totalPages); + + pagedLeases = filteredLeases + .Skip((currentPage - 1) * pageSize) + .Take(pageSize) + .ToList(); + } + + CalculateMetrics(); + } + + private void TogglePropertyGroup(Guid propertyId) + { + if (expandedProperties.Contains(propertyId.GetHashCode())) + { + expandedProperties.Remove(propertyId.GetHashCode()); + } + else + { + expandedProperties.Add(propertyId.GetHashCode()); + } + } + + private void ApplySorting() + { + filteredLeases = sortColumn switch + { + "Property" => sortAscending + ? filteredLeases.OrderBy(l => l.Property?.Address).ToList() + : filteredLeases.OrderByDescending(l => l.Property?.Address).ToList(), + "Tenant" => sortAscending + ? filteredLeases.OrderBy(l => l.Tenant?.FullName).ToList() + : filteredLeases.OrderByDescending(l => l.Tenant?.FullName).ToList(), + "StartDate" => sortAscending + ? filteredLeases.OrderBy(l => l.StartDate).ToList() + : filteredLeases.OrderByDescending(l => l.StartDate).ToList(), + "EndDate" => sortAscending + ? filteredLeases.OrderBy(l => l.EndDate).ToList() + : filteredLeases.OrderByDescending(l => l.EndDate).ToList(), + "MonthlyRent" => sortAscending + ? filteredLeases.OrderBy(l => l.MonthlyRent).ToList() + : filteredLeases.OrderByDescending(l => l.MonthlyRent).ToList(), + "Status" => sortAscending + ? filteredLeases.OrderBy(l => l.Status).ToList() + : filteredLeases.OrderByDescending(l => l.Status).ToList(), + _ => filteredLeases + }; + } + + private void SortBy(string column) + { + if (sortColumn == column) + { + sortAscending = !sortAscending; + } + else + { + sortColumn = column; + sortAscending = true; + } + currentPage = 1; + FilterLeases(); + } + + private void GoToPage(int page) + { + if (page >= 1 && page <= totalPages) + { + currentPage = page; + FilterLeases(); + } + } + + private void CalculateMetrics() + { + if (filteredLeases != null && filteredLeases.Any()) + { + activeCount = filteredLeases.Count(l => l.Status == "Active"); + + // Expiring within 30 days + var thirtyDaysFromNow = DateTime.Now.AddDays(30); + expiringSoonCount = filteredLeases.Count(l => + l.Status == "Active" && l.EndDate <= thirtyDaysFromNow); + + totalMonthlyRent = filteredLeases + .Where(l => l.Status == "Active") + .Sum(l => l.MonthlyRent); + } + else + { + activeCount = 0; + expiringSoonCount = 0; + totalMonthlyRent = 0; + } + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + "Active" => "bg-success", + "Pending" => "bg-info", + "Expired" => "bg-warning", + "Terminated" => "bg-danger", + _ => "bg-secondary" + }; + } + + private void ViewLeaseOffers() + { + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); + } + + private void CreateLease() + { + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); + } + + private void CreateLeaseForTenant() + { + @* if (TenantId.HasValue) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/create?tenantId={TenantId.Value}"); + } + else + { + NavigationManager.NavigateTo("/propertymanagement/leases/create"); + } *@ + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); + } + + private void CreateLeaseForProperty() + { + @* if (PropertyId.HasValue) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/create?propertyId={PropertyId.Value}"); + } + else + { + NavigationManager.NavigateTo("/propertymanagement/leases/create"); + } *@ + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); + } + + private void ClearFilter() + { + TenantId = null; + PropertyId = null; + filterTenant = null; + filterProperty = null; + selectedLeaseStatus = string.Empty; + selectedTenantId = null; + searchTerm = string.Empty; + NavigationManager.NavigateTo("/propertymanagement/leases", forceLoad: true); + } + + private void ViewLease(Guid id) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/view/{id}"); + } + + private void EditLease(Guid id) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/edit/{id}"); + } + + private async Task DeleteLease(Guid id) + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + return; + + // Add confirmation dialog in a real application + var confirmed = await JSRuntime.InvokeAsync("confirm", $"Are you sure you want to delete lease {id}?"); + if (!confirmed) + return; + + await LeaseService.DeleteAsync(id); + await LoadLeases(); + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/ViewLease.razor b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/ViewLease.razor new file mode 100644 index 0000000..a3b205a --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/ViewLease.razor @@ -0,0 +1,1263 @@ +@page "/propertymanagement/leases/view/{Id:guid}" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Validation +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Web +@using Aquiis.Professional.Features.PropertyManagement +@using System.ComponentModel.DataAnnotations +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Application.Services.Workflows +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators + +@inject NavigationManager Navigation +@inject LeaseService LeaseService +@inject InvoiceService InvoiceService +@inject Application.Services.DocumentService DocumentService +@inject LeaseWorkflowService LeaseWorkflowService +@inject UserContextService UserContextService +@inject LeaseRenewalPdfGenerator RenewalPdfGenerator +@inject ToastService ToastService +@inject OrganizationService OrganizationService +@inject IJSRuntime JSRuntime +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + +@rendermode InteractiveServer + +@if (lease == null) +{ +
+
+ Loading... +
+
+} +else if (!isAuthorized) +{ +
+

Access Denied

+

You don't have permission to view this lease.

+ Back to Leases +
+} +else +{ +
+

Lease Details

+
+ + +
+
+ +
+
+
+
+
Lease Information
+ + @lease.Status + +
+
+
+
+ Property: +

@lease.Property?.Address

+ @lease.Property?.City, @lease.Property?.State +
+
+ Tenant: + @if (lease.Tenant != null) + { +

@lease.Tenant.FullName

+ @lease.Tenant.Email + } + else + { +

Lease Offer - Awaiting Acceptance

+ Tenant will be assigned upon acceptance + } +
+
+ +
+
+ Start Date: +

@lease.StartDate.ToString("MMMM dd, yyyy")

+
+
+ End Date: +

@lease.EndDate.ToString("MMMM dd, yyyy")

+
+
+ +
+
+ Monthly Rent: +

@lease.MonthlyRent.ToString("C")

+
+
+ Security Deposit: +

@lease.SecurityDeposit.ToString("C")

+
+
+ + @if (!string.IsNullOrEmpty(lease.Terms)) + { +
+
+ Lease Terms: +

@lease.Terms

+
+
+ } + + @if (!string.IsNullOrEmpty(lease.Notes)) + { +
+
+ Notes: +

@lease.Notes

+
+
+ } + +
+
+ Created: +

@lease.CreatedOn.ToString("MMMM dd, yyyy")

+
+ @if (lease.LastModifiedOn.HasValue) + { +
+ Last Modified: +

@lease.LastModifiedOn.Value.ToString("MMMM dd, yyyy")

+
+ } +
+ + @if (lease.IsActive) + { +
+
+
+ + Active Lease: This lease is currently active with @lease.DaysRemaining days remaining. +
+
+
+ } +
+
+
+ +
+ @if (lease.IsExpiringSoon) + { +
+
+
+ Renewal Alert +
+
+
+

+ Expires in: + @lease.DaysRemaining days +

+

+ End Date: @lease.EndDate.ToString("MMM dd, yyyy") +

+ + @if (!string.IsNullOrEmpty(lease.RenewalStatus)) + { +

+ Status: + + @lease.RenewalStatus + +

+ } + + @if (lease.ProposedRenewalRent.HasValue) + { +

+ Proposed Rent: @lease.ProposedRenewalRent.Value.ToString("C") + @if (lease.ProposedRenewalRent != lease.MonthlyRent) + { + var increase = lease.ProposedRenewalRent.Value - lease.MonthlyRent; + var percentage = (increase / lease.MonthlyRent) * 100; + + (@(increase > 0 ? "+" : "")@increase.ToString("C"), @percentage.ToString("F1")%) + + } +

+ } + + @if (lease.RenewalNotificationSentOn.HasValue) + { + + Notification sent: @lease.RenewalNotificationSentOn.Value.ToString("MMM dd, yyyy") + + } + + @if (!string.IsNullOrEmpty(lease.RenewalNotes)) + { +
+ + Notes:
+ @lease.RenewalNotes +
+ } + +
+ @if (lease.RenewalStatus == "Pending" || string.IsNullOrEmpty(lease.RenewalStatus)) + { + + + } + @if (lease.RenewalStatus == "Offered") + { + + + + } +
+
+
+ } + +
+
+
Quick Actions
+
+
+
+ + + + + @if (lease.DocumentId == null) + { + + } + else + { + + + } +
+
+
+ +
+
+
Lease Summary
+
+
+

Duration: @((lease.EndDate - lease.StartDate).Days) days

+

Total Rent: @((lease.MonthlyRent * 12).ToString("C"))/year

+ @if (lease.IsActive) + { +

Days Remaining: @lease.DaysRemaining

+ } + @if (recentInvoices.Any()) + { +
+ + Recent Invoices:
+ @foreach (var invoice in recentInvoices.Take(3)) + { + + @invoice.InvoiceNumber + + } +
+ } +
+
+ + @* Lease Lifecycle Management Card *@ + @if (lease.Status == "Active" || lease.Status == "MonthToMonth" || lease.Status == "NoticeGiven") + { +
+
+
Lease Management
+
+
+
+ @if (lease.Status == "Active" || lease.Status == "MonthToMonth") + { + + + + } + @if (lease.Status == "NoticeGiven") + { +
+ + Notice Given: @lease.TerminationNoticedOn?.ToString("MMM dd, yyyy")
+ Expected Move-Out: @lease.ExpectedMoveOutDate?.ToString("MMM dd, yyyy") +
+
+ + + } +
+
+
+ } +
+
+
+
+
+
+
Notes
+
+
+ +
+
+
+
+ @* Renewal Offer Modal *@ + @if (showRenewalModal && lease != null) + { + + } + + @* Termination Notice Modal *@ + @if (showTerminationNoticeModal && lease != null) + { + + } + + @* Early Termination Modal *@ + @if (showEarlyTerminationModal && lease != null) + { + + } + + @* Move-Out Completion Modal *@ + @if (showMoveOutModal && lease != null) + { + + } + + @* Convert to Month-to-Month Modal *@ + @if (showConvertMTMModal && lease != null) + { + + } +} + +@code { + [Parameter] public Guid Id { get; set; } + + [Parameter] + [SupplyParameterFromQuery] + public Guid? PropertyId { get; set; } + [Parameter] + [SupplyParameterFromQuery] + public Guid? TenantId { get; set; } + + private Lease? lease; + private List recentInvoices = new(); + private bool isAuthorized = true; + private bool isGenerating = false; + private bool isGeneratingPdf = false; + private bool isSubmitting = false; + private bool showRenewalModal = false; + private decimal proposedRent = 0; + private string renewalNotes = ""; + private Document? document = null; + + // Termination Notice state + private bool showTerminationNoticeModal = false; + private string terminationNoticeType = ""; + private DateTime terminationNoticeDate = DateTime.Today; + private DateTime terminationExpectedMoveOutDate = DateTime.Today.AddDays(30); + private string terminationReason = ""; + + // Early Termination state + private bool showEarlyTerminationModal = false; + private string earlyTerminationType = ""; + private DateTime earlyTerminationDate = DateTime.Today; + private string earlyTerminationReason = ""; + + // Move-Out state + private bool showMoveOutModal = false; + private DateTime actualMoveOutDate = DateTime.Today; + private bool moveOutFinalInspection = false; + private bool moveOutKeysReturned = false; + private string moveOutNotes = ""; + + // Month-to-Month conversion state + private bool showConvertMTMModal = false; + private decimal? mtmNewRent = null; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + private LeaseModel leaseModel = new(); + private Property? selectedProperty; + private List availableProperties = new(); + + protected override async Task OnInitializedAsync() + { + await LoadLease(); + + // If PropertyId is provided in query string, pre-select it + if (PropertyId.HasValue) + { + leaseModel.PropertyId = PropertyId.Value; + await OnPropertyChanged(); + } + + // If TenantId is provided in query string, pre-select it + if (TenantId.HasValue) + { + leaseModel.TenantId = TenantId.Value; + } + } + + private async Task LoadLease() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + isAuthorized = false; + return; + } + + lease = await LeaseService.GetByIdAsync(Id); + + if (lease == null) + { + isAuthorized = false; + return; + } + + var invoices = await InvoiceService.GetInvoicesByLeaseIdAsync(Id); + recentInvoices = invoices + .OrderByDescending(i => i.DueOn) + .Take(5) + .ToList(); + + // Load the document if it exists + if (lease.DocumentId != null) + { + document = await DocumentService.GetByIdAsync(lease.DocumentId.Value); + } + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + "Active" => "bg-success", + "Pending" => "bg-warning", + "Expired" => "bg-secondary", + "Terminated" => "bg-danger", + _ => "bg-secondary" + }; + } + + private string GetRenewalStatusBadgeClass(string status) + { + return status switch + { + "Pending" => "secondary", + "Offered" => "info", + "Accepted" => "success", + "Declined" => "danger", + "Expired" => "dark", + _ => "secondary" + }; + } + + private void ShowRenewalOfferModal() + { + proposedRent = lease?.MonthlyRent ?? 0; + renewalNotes = ""; + showRenewalModal = true; + } + + private async Task SendRenewalOffer() + { + if (lease == null) return; + + try + { + // Update lease with renewal offer details + lease.RenewalStatus = "Offered"; + lease.ProposedRenewalRent = proposedRent; + lease.RenewalOfferedOn = DateTime.UtcNow; + lease.RenewalNotes = renewalNotes; + + await LeaseService.UpdateAsync(lease); + + // TODO: Send email notification to tenant + + showRenewalModal = false; + await LoadLease(); + StateHasChanged(); + + ToastService.ShowSuccess("Renewal offer sent successfully! You can now generate the offer letter PDF."); + } + catch (Exception ex) + { + ToastService.ShowError($"Error sending renewal offer: {ex.Message}"); + } + } + + private async Task GenerateRenewalOfferPdf() + { + if (lease == null) return; + + try + { + isGeneratingPdf = true; + StateHasChanged(); + + // Ensure proposed rent is set + if (!lease.ProposedRenewalRent.HasValue) + { + lease.ProposedRenewalRent = lease.MonthlyRent; + } + + // Generate renewal offer PDF + var pdfBytes = RenewalPdfGenerator.GenerateRenewalOfferLetter(lease, lease.Property, lease.Tenant!); + var fileName = $"Lease_Renewal_Offer_{lease.Property?.Address?.Replace(" ", "_")}_{DateTime.Now:yyyyMMdd_HHmmss}.pdf"; + + // Save PDF to Documents table + var document = new Document + { + PropertyId = lease.PropertyId, + TenantId = lease.TenantId, + LeaseId = lease.Id, + FileName = fileName, + FileType = "application/pdf", + FileSize = pdfBytes.Length, + FileData = pdfBytes, + FileExtension = ".pdf", + ContentType = "application/pdf", + DocumentType = "Lease Renewal Offer", + Description = $"Renewal offer letter for {lease.Property?.Address}. Proposed rent: {lease.ProposedRenewalRent:C}" + }; + + await DocumentService.CreateAsync(document); + + ToastService.ShowSuccess($"Renewal offer letter generated and saved to documents!"); + } + catch (Exception ex) + { + ToastService.ShowError($"Error generating PDF: {ex.Message}"); + } + finally + { + isGeneratingPdf = false; + StateHasChanged(); + } + } + + private async Task MarkRenewalAccepted() + { + if (lease == null) return; + + try + { + // Create renewal model with proposed terms + var renewalModel = new LeaseRenewalModel + { + NewStartDate = DateTime.Today, + NewEndDate = DateTime.Today.AddYears(1), + NewMonthlyRent = lease.ProposedRenewalRent ?? lease.MonthlyRent, + UpdatedSecurityDeposit = lease.SecurityDeposit, + NewTerms = lease.Terms + }; + + var result = await LeaseWorkflowService.RenewLeaseAsync(lease.Id, renewalModel); + + if (result.Success && result.Data != null) + { + await LoadLease(); + StateHasChanged(); + + ToastService.ShowSuccess($"Renewal accepted! New lease created from {result.Data.StartDate:MMM dd, yyyy} to {result.Data.EndDate:MMM dd, yyyy}."); + } + else + { + ToastService.ShowError($"Error accepting renewal: {string.Join(", ", result.Errors)}"); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error accepting renewal: {ex.Message}"); + } + } + + private async Task MarkRenewalDeclined() + { + if (lease == null) return; + + try + { + lease.RenewalStatus = "Declined"; + lease.RenewalResponseOn = DateTime.UtcNow; + await LeaseService.UpdateAsync(lease); + await LoadLease(); + StateHasChanged(); + + ToastService.ShowWarning("Renewal offer marked as declined."); + } + catch (Exception ex) + { + ToastService.ShowError($"Error updating renewal status: {ex.Message}"); + } + } + + #region Lease Workflow Methods + + private async Task RecordTerminationNotice() + { + if (lease == null || string.IsNullOrWhiteSpace(terminationNoticeType) || string.IsNullOrWhiteSpace(terminationReason)) + return; + + isSubmitting = true; + StateHasChanged(); + + try + { + var result = await LeaseWorkflowService.RecordTerminationNoticeAsync( + lease.Id, + terminationNoticeDate, + terminationExpectedMoveOutDate, + terminationNoticeType, + terminationReason); + + if (result.Success) + { + showTerminationNoticeModal = false; + ResetTerminationNoticeForm(); + await LoadLease(); + ToastService.ShowSuccess(result.Message); + } + else + { + ToastService.ShowError(string.Join(", ", result.Errors)); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error recording termination notice: {ex.Message}"); + } + finally + { + isSubmitting = false; + StateHasChanged(); + } + } + + private async Task EarlyTerminateLease() + { + if (lease == null || string.IsNullOrWhiteSpace(earlyTerminationType) || string.IsNullOrWhiteSpace(earlyTerminationReason)) + return; + + isSubmitting = true; + StateHasChanged(); + + try + { + var result = await LeaseWorkflowService.EarlyTerminateAsync( + lease.Id, + earlyTerminationType, + earlyTerminationReason, + earlyTerminationDate); + + if (result.Success) + { + showEarlyTerminationModal = false; + ResetEarlyTerminationForm(); + await LoadLease(); + ToastService.ShowSuccess(result.Message); + } + else + { + ToastService.ShowError(string.Join(", ", result.Errors)); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error terminating lease: {ex.Message}"); + } + finally + { + isSubmitting = false; + StateHasChanged(); + } + } + + private async Task CompleteMoveOut() + { + if (lease == null) return; + + isSubmitting = true; + StateHasChanged(); + + try + { + var moveOutModel = new MoveOutModel + { + FinalInspectionCompleted = moveOutFinalInspection, + KeysReturned = moveOutKeysReturned, + Notes = moveOutNotes + }; + + var result = await LeaseWorkflowService.CompleteMoveOutAsync( + lease.Id, + actualMoveOutDate, + moveOutModel); + + if (result.Success) + { + showMoveOutModal = false; + ResetMoveOutForm(); + await LoadLease(); + ToastService.ShowSuccess(result.Message); + } + else + { + ToastService.ShowError(string.Join(", ", result.Errors)); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error completing move-out: {ex.Message}"); + } + finally + { + isSubmitting = false; + StateHasChanged(); + } + } + + private async Task ConvertToMonthToMonth() + { + if (lease == null) return; + + isSubmitting = true; + StateHasChanged(); + + try + { + var result = await LeaseWorkflowService.ConvertToMonthToMonthAsync( + lease.Id, + mtmNewRent); + + if (result.Success) + { + showConvertMTMModal = false; + mtmNewRent = null; + await LoadLease(); + ToastService.ShowSuccess(result.Message); + } + else + { + ToastService.ShowError(string.Join(", ", result.Errors)); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error converting to month-to-month: {ex.Message}"); + } + finally + { + isSubmitting = false; + StateHasChanged(); + } + } + + private void ResetTerminationNoticeForm() + { + terminationNoticeType = ""; + terminationNoticeDate = DateTime.Today; + terminationExpectedMoveOutDate = DateTime.Today.AddDays(30); + terminationReason = ""; + } + + private void ResetEarlyTerminationForm() + { + earlyTerminationType = ""; + earlyTerminationDate = DateTime.Today; + earlyTerminationReason = ""; + } + + private void ResetMoveOutForm() + { + actualMoveOutDate = DateTime.Today; + moveOutFinalInspection = false; + moveOutKeysReturned = false; + moveOutNotes = ""; + } + + #endregion + + private async Task OnPropertyChanged() + { + if (leaseModel.PropertyId != Guid.Empty) + { + selectedProperty = availableProperties.FirstOrDefault(p => p.Id == leaseModel.PropertyId); + if (selectedProperty != null) + { + // Get organization settings for security deposit calculation + var settings = await OrganizationService.GetOrganizationSettingsAsync(); + var depositMultiplier = settings?.AutoCalculateSecurityDeposit == true + ? settings.SecurityDepositMultiplier + : 1.0m; + + leaseModel.MonthlyRent = selectedProperty.MonthlyRent; + leaseModel.SecurityDeposit = selectedProperty.MonthlyRent * depositMultiplier; + } + } + else + { + selectedProperty = null; + } + StateHasChanged(); + } + + private void EditLease() + { + Navigation.NavigateTo($"/propertymanagement/leases/edit/{Id}"); + } + + private void BackToList() + { + Navigation.NavigateTo("/propertymanagement/leases"); + } + + private void CreateInvoice() + { + Navigation.NavigateTo($"/propertymanagement/invoices/create?leaseId={Id}"); + } + + private void ViewInvoices() + { + Navigation.NavigateTo($"/propertymanagement/invoices?leaseId={Id}"); + } + + private void ViewDocuments() + { + Navigation.NavigateTo($"/propertymanagement/leases/{Id}/documents"); + } + + private async Task ViewDocument() + { + if (document != null) + { + var base64Data = Convert.ToBase64String(document.FileData); + await JSRuntime.InvokeVoidAsync("viewFile", base64Data, document.FileType); + } + } + + private async Task DownloadDocument() + { + if (document != null) + { + var fileName = document.FileName; + var fileData = document.FileData; + var mimeType = document.FileType; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(fileData), mimeType); + } + } + + private async Task GenerateLeaseDocument() + { + isGenerating = true; + StateHasChanged(); + + try + { + // Generate the PDF + byte[] pdfBytes = await LeasePdfGenerator.GenerateLeasePdf(lease!); + + // Create the document entity + var document = new Document + { + FileName = $"Lease_{lease!.Property?.Address?.Replace(" ", "_")}_{DateTime.Now:yyyyMMdd}.pdf", + FileExtension = ".pdf", + FileData = pdfBytes, + FileSize = pdfBytes.Length, + FileType = "application/pdf", + DocumentType = "Lease Agreement", + Description = "Auto-generated lease agreement", + LeaseId = lease.Id, + PropertyId = lease.PropertyId, + TenantId = lease.TenantId, + }; + + // Save to database + await DocumentService.CreateAsync(document); + + // Update lease with DocumentId + lease.DocumentId = document.Id; + + await LeaseService.UpdateAsync(lease); + + // Reload lease and document + await LoadLease(); + StateHasChanged(); + + await JSRuntime.InvokeVoidAsync("alert", "Lease document generated successfully!"); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"Error generating lease document: {ex.Message}"); + } + finally + { + isGenerating = false; + StateHasChanged(); + } + } + + public class LeaseModel + { + [RequiredGuid(ErrorMessage = "Property is required")] + public Guid PropertyId { get; set; } + + [RequiredGuid(ErrorMessage = "Tenant is required")] + public Guid TenantId { get; set; } + + [Required(ErrorMessage = "Start date is required")] + public DateTime StartDate { get; set; } = DateTime.Today; + + [Required(ErrorMessage = "End date is required")] + public DateTime EndDate { get; set; } = DateTime.Today.AddYears(1); + + [Required(ErrorMessage = "Monthly rent is required")] + [Range(0.01, double.MaxValue, ErrorMessage = "Monthly rent must be greater than 0")] + public decimal MonthlyRent { get; set; } + + [Range(0, double.MaxValue, ErrorMessage = "Security deposit cannot be negative")] + public decimal SecurityDeposit { get; set; } + + [Required(ErrorMessage = "Status is required")] + [StringLength(50)] + public string Status { get; set; } = "Active"; + + [StringLength(1000, ErrorMessage = "Terms cannot exceed 1000 characters")] + public string Terms { get; set; } = string.Empty; + + [StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")] + public string Notes { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor new file mode 100644 index 0000000..b8533c1 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor @@ -0,0 +1,354 @@ +@page "/propertymanagement/maintenance/create/{PropertyId:int?}" +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.Extensions.Configuration.UserSecrets +@using System.ComponentModel.DataAnnotations +@inject MaintenanceService MaintenanceService +@inject PropertyService PropertyService +@inject LeaseService LeaseService +@inject NavigationManager NavigationManager + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Create Maintenance Request + +
+
+

Create Maintenance Request

+ +
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else + { +
+
+
+
+ + + + +
+
+ + + + @foreach (var property in properties) + { + + } + + +
+
+ +
+ @if (currentLease != null) + { + @currentLease.Tenant?.FullName - @currentLease.Status + } + else + { + No active leases + } +
+
+
+ +
+ + + +
+ +
+ + + +
+ +
+
+ + + + @foreach (var type in ApplicationConstants.MaintenanceRequestTypes.AllMaintenanceRequestTypes) + { + + } + + +
+
+ + + @foreach (var priority in ApplicationConstants.MaintenanceRequestPriorities.AllMaintenanceRequestPriorities) + { + + } + + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
Information
+
+
+
Priority Levels
+
    +
  • + Urgent - Immediate attention required +
  • +
  • + High - Should be addressed soon +
  • +
  • + Medium - Normal priority +
  • +
  • + Low - Can wait +
  • +
+ +
+ +
Request Types
+
    +
  • Plumbing
  • +
  • Electrical
  • +
  • Heating/Cooling
  • +
  • Appliance
  • +
  • Structural
  • +
  • Landscaping
  • +
  • Pest Control
  • +
  • Other
  • +
+
+
+
+
+ } +
+ +@code { + [Parameter] + [SupplyParameterFromQuery] + public Guid? PropertyId { get; set; } + + [Parameter] + [SupplyParameterFromQuery] + public Guid? LeaseId { get; set; } + private MaintenanceRequestModel maintenanceRequest = new(); + private List properties = new(); + private Lease? currentLease = null; + private bool isLoading = true; + private bool isSaving = false; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + protected override async Task OnParametersSetAsync() + { + if (PropertyId.HasValue && PropertyId.Value != Guid.Empty && maintenanceRequest.PropertyId != PropertyId.Value) + { + maintenanceRequest.PropertyId = PropertyId.Value; + if (properties.Any()) + { + await LoadLeaseForProperty(PropertyId.Value); + } + } + if (LeaseId.HasValue && LeaseId.Value != Guid.Empty && maintenanceRequest.LeaseId != LeaseId.Value) + { + maintenanceRequest.LeaseId = LeaseId.Value; + } + } + + private async Task LoadData() + { + isLoading = true; + try + { + properties = await PropertyService.GetAllAsync(); + + if (PropertyId.HasValue && PropertyId.Value != Guid.Empty) + { + maintenanceRequest.PropertyId = PropertyId.Value; + await LoadLeaseForProperty(PropertyId.Value); + } + if (LeaseId.HasValue && LeaseId.Value != Guid.Empty) + { + maintenanceRequest.LeaseId = LeaseId.Value; + } + } + finally + { + isLoading = false; + } + } + + private async Task OnPropertyChangedAsync() + { + if (maintenanceRequest.PropertyId != Guid.Empty) + { + await LoadLeaseForProperty(maintenanceRequest.PropertyId); + } + else + { + currentLease = null; + maintenanceRequest.LeaseId = null; + } + } + + private async Task LoadLeaseForProperty(Guid propertyId) + { + var leases = await LeaseService.GetCurrentAndUpcomingLeasesByPropertyIdAsync(propertyId); + currentLease = leases.FirstOrDefault(); + maintenanceRequest.LeaseId = currentLease?.Id; + } + + private async Task HandleValidSubmit() + { + isSaving = true; + try + { + var request = new MaintenanceRequest + { + PropertyId = maintenanceRequest.PropertyId, + LeaseId = maintenanceRequest.LeaseId, + Title = maintenanceRequest.Title, + Description = maintenanceRequest.Description, + RequestType = maintenanceRequest.RequestType, + Priority = maintenanceRequest.Priority, + RequestedBy = maintenanceRequest.RequestedBy, + RequestedByEmail = maintenanceRequest.RequestedByEmail, + RequestedByPhone = maintenanceRequest.RequestedByPhone, + RequestedOn = maintenanceRequest.RequestedOn, + ScheduledOn = maintenanceRequest.ScheduledOn, + EstimatedCost = maintenanceRequest.EstimatedCost, + AssignedTo = maintenanceRequest.AssignedTo + }; + + await MaintenanceService.CreateAsync(request); + NavigationManager.NavigateTo("/propertymanagement/maintenance"); + } + finally + { + isSaving = false; + } + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/maintenance"); + } + + public class MaintenanceRequestModel + { + [Required(ErrorMessage = "Property is required")] + public Guid PropertyId { get; set; } + + public Guid? LeaseId { get; set; } + + [Required(ErrorMessage = "Title is required")] + [StringLength(100, ErrorMessage = "Title cannot exceed 100 characters")] + public string Title { get; set; } = string.Empty; + + [Required(ErrorMessage = "Description is required")] + [StringLength(2000, ErrorMessage = "Description cannot exceed 2000 characters")] + public string Description { get; set; } = string.Empty; + + [Required(ErrorMessage = "Request type is required")] + public string RequestType { get; set; } = string.Empty; + + [Required(ErrorMessage = "Priority is required")] + public string Priority { get; set; } = "Medium"; + + public string RequestedBy { get; set; } = string.Empty; + public string RequestedByEmail { get; set; } = string.Empty; + public string RequestedByPhone { get; set; } = string.Empty; + + [Required] + public DateTime RequestedOn { get; set; } = DateTime.Today; + + public DateTime? ScheduledOn { get; set; } + + public decimal EstimatedCost { get; set; } + public string AssignedTo { get; set; } = string.Empty; + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/EditMaintenanceRequest.razor b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/EditMaintenanceRequest.razor new file mode 100644 index 0000000..545ed1b --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/EditMaintenanceRequest.razor @@ -0,0 +1,306 @@ +@page "/propertymanagement/maintenance/edit/{Id:guid}" +@inject MaintenanceService MaintenanceService +@inject PropertyService PropertyService +@inject LeaseService LeaseService +@inject NavigationManager NavigationManager +@inject IJSRuntime JSRuntime +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Edit Maintenance Request + +
+
+

Edit Maintenance Request #@Id

+ +
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (maintenanceRequest == null) + { +
+ Maintenance request not found. +
+ } + else + { +
+
+
+
+ + + + +
+
+ + + + @foreach (var property in properties) + { + + } + + +
+
+ + + + @foreach (var lease in availableLeases) + { + + } + +
+
+ +
+ + + +
+ +
+ + + +
+ +
+
+ + + + @foreach (var type in ApplicationConstants.MaintenanceRequestTypes.AllMaintenanceRequestTypes) + { + + } + + +
+
+ + + @foreach (var priority in ApplicationConstants.MaintenanceRequestPriorities.AllMaintenanceRequestPriorities) + { + + } + + +
+
+ + + @foreach (var status in ApplicationConstants.MaintenanceRequestStatuses.AllMaintenanceRequestStatuses) + { + + } + + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+
+
+
+
+ +
+
+
+
Status Information
+
+
+
+ +

@maintenanceRequest.Priority

+
+
+ +

@maintenanceRequest.Status

+
+
+ +

@maintenanceRequest.DaysOpen days

+
+ @if (maintenanceRequest.IsOverdue) + { +
+ Overdue +
+ } +
+
+
+
+ } +
+ +@code { + [Parameter] + public Guid Id { get; set; } + + private MaintenanceRequest? maintenanceRequest; + private List properties = new(); + private List availableLeases = new(); + private bool isLoading = true; + private bool isSaving = false; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + isLoading = true; + try + { + maintenanceRequest = await MaintenanceService.GetByIdAsync(Id); + properties = await PropertyService.GetAllAsync(); + + if (maintenanceRequest?.PropertyId != null) + { + await LoadLeasesForProperty(maintenanceRequest.PropertyId); + } + } + finally + { + isLoading = false; + } + } + + private async Task OnPropertyChanged(ChangeEventArgs e) + { + if (Guid.TryParse(e.Value?.ToString(), out Guid propertyId) && propertyId != Guid.Empty) + { + await LoadLeasesForProperty(propertyId); + } + else + { + availableLeases.Clear(); + } + } + + private async Task LoadLeasesForProperty(Guid propertyId) + { + var allLeases = await LeaseService.GetLeasesByPropertyIdAsync(propertyId); + availableLeases = allLeases.Where(l => l.Status == "Active" || l.Status == "Pending").ToList(); + } + + private async Task HandleValidSubmit() + { + if (maintenanceRequest == null) return; + + isSaving = true; + try + { + await MaintenanceService.UpdateAsync(maintenanceRequest); + NavigationManager.NavigateTo($"/propertymanagement/maintenance/view/{Id}"); + } + finally + { + isSaving = false; + } + } + + private async Task DeleteRequest() + { + if (maintenanceRequest == null) return; + + var confirmed = await JSRuntime.InvokeAsync("confirm", "Are you sure you want to delete this maintenance request?"); + if (confirmed) + { + await MaintenanceService.DeleteAsync(Id); + NavigationManager.NavigateTo("/propertymanagement/maintenance"); + } + } + + private void Cancel() + { + NavigationManager.NavigateTo($"/propertymanagement/maintenance/view/{Id}"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/MaintenanceRequests.razor b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/MaintenanceRequests.razor new file mode 100644 index 0000000..bd605ee --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/MaintenanceRequests.razor @@ -0,0 +1,350 @@ +@page "/propertymanagement/maintenance" +@inject MaintenanceService MaintenanceService +@inject NavigationManager NavigationManager +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Maintenance Requests + +
+

Maintenance Requests

+ +
+ +@if (isLoading) +{ +
+
+ Loading... +
+
+} +else +{ + +
+
+
+
+
Urgent
+

@urgentRequests.Count

+ High priority requests +
+
+
+
+
+
+
In Progress
+

@inProgressRequests.Count

+ Currently being worked on +
+
+
+
+
+
+
Submitted
+

@submittedRequests.Count

+ Awaiting assignment +
+
+
+
+
+
+
Completed
+

@completedRequests.Count

+ This month +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + + @if (overdueRequests.Any()) + { +
+
+
Overdue Requests
+
+
+
+ + + + + + + + + + + + + + + @foreach (var request in overdueRequests) + { + + + + + + + + + + + } + +
IDPropertyTitleTypePriorityScheduledDays OpenActions
@request.Id + @request.Property?.Address + @request.Title@request.RequestType@request.Priority@request.ScheduledOn?.ToString("MMM dd")@request.DaysOpen days + +
+
+
+
+ } + + +
+
+
+ + @if (!string.IsNullOrEmpty(currentStatusFilter)) + { + @currentStatusFilter Requests + } + else + { + All Requests + } + (@filteredRequests.Count) +
+
+
+ @if (filteredRequests.Any()) + { +
+ + + + + + + + + + + + + + + + @foreach (var request in filteredRequests) + { + + + + + + + + + + + + } + +
IDPropertyTitleTypePriorityStatusRequestedAssigned ToActions
@request.Id + @request.Property?.Address + + @request.Title + @if (request.IsOverdue) + { + + } + @request.RequestType@request.Priority@request.Status@request.RequestedOn.ToString("MMM dd, yyyy")@(string.IsNullOrEmpty(request.AssignedTo) ? "Unassigned" : request.AssignedTo) +
+ + +
+
+
+ } + else + { +
+ +

No maintenance requests found

+
+ } +
+
+} + +@code { + private List allRequests = new(); + private List filteredRequests = new(); + private List urgentRequests = new(); + private List inProgressRequests = new(); + private List submittedRequests = new(); + private List completedRequests = new(); + private List overdueRequests = new(); + + private string currentStatusFilter = ""; + private string currentPriorityFilter = ""; + private string currentTypeFilter = ""; + + private bool isLoading = true; + + [Parameter] + [SupplyParameterFromQuery] + public Guid? PropertyId { get; set; } + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + isLoading = true; + try + { + allRequests = await MaintenanceService.GetAllAsync(); + + if (PropertyId.HasValue) + { + allRequests = allRequests.Where(r => r.PropertyId == PropertyId.Value).ToList(); + } + + // Summary cards + urgentRequests = allRequests.Where(r => r.Priority == "Urgent" && r.Status != "Completed" && r.Status != "Cancelled").ToList(); + inProgressRequests = allRequests.Where(r => r.Status == "In Progress").ToList(); + submittedRequests = allRequests.Where(r => r.Status == "Submitted").ToList(); + completedRequests = allRequests.Where(r => r.Status == "Completed" && r.CompletedOn?.Month == DateTime.Today.Month).ToList(); + overdueRequests = await MaintenanceService.GetOverdueMaintenanceRequestsAsync(); + + ApplyFilters(); + } + finally + { + isLoading = false; + } + } + + private void ApplyFilters() + { + filteredRequests = allRequests; + + if (!string.IsNullOrEmpty(currentStatusFilter)) + { + filteredRequests = filteredRequests.Where(r => r.Status == currentStatusFilter).ToList(); + } + + if (!string.IsNullOrEmpty(currentPriorityFilter)) + { + filteredRequests = filteredRequests.Where(r => r.Priority == currentPriorityFilter).ToList(); + } + + if (!string.IsNullOrEmpty(currentTypeFilter)) + { + filteredRequests = filteredRequests.Where(r => r.RequestType == currentTypeFilter).ToList(); + } + } + + private void OnStatusFilterChanged(ChangeEventArgs e) + { + currentStatusFilter = e.Value?.ToString() ?? ""; + ApplyFilters(); + } + + private void OnPriorityFilterChanged(ChangeEventArgs e) + { + currentPriorityFilter = e.Value?.ToString() ?? ""; + ApplyFilters(); + } + + private void OnTypeFilterChanged(ChangeEventArgs e) + { + currentTypeFilter = e.Value?.ToString() ?? ""; + ApplyFilters(); + } + + private void ClearFilters() + { + currentStatusFilter = ""; + currentPriorityFilter = ""; + currentTypeFilter = ""; + ApplyFilters(); + } + + private void CreateNew() + { + NavigationManager.NavigateTo("/propertymanagement/maintenance/create"); + } + + private void ViewRequest(Guid requestId) + { + NavigationManager.NavigateTo($"/propertymanagement/maintenance/view/{requestId}"); + } + + private void ViewProperty(Guid propertyId) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/view/{propertyId}"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/ViewMaintenanceRequest.razor b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/ViewMaintenanceRequest.razor new file mode 100644 index 0000000..43edf24 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/ViewMaintenanceRequest.razor @@ -0,0 +1,309 @@ +@page "/propertymanagement/maintenance/view/{Id:guid}" + +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators + +@inject MaintenanceService MaintenanceService +@inject NavigationManager NavigationManager +@inject ToastService ToastService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Maintenance Request Details + +@if (isLoading) +{ +
+
+ Loading... +
+
+} +else if (maintenanceRequest == null) +{ +
+ Maintenance request not found. +
+} +else +{ +
+

Maintenance Request #@maintenanceRequest.Id

+
+ + +
+
+ +
+
+ +
+
+
Request Details
+
+ @maintenanceRequest.Priority + @maintenanceRequest.Status +
+
+
+
+
+ +

+ @maintenanceRequest.Property?.Address
+ @maintenanceRequest.Property?.City, @maintenanceRequest.Property?.State @maintenanceRequest.Property?.ZipCode +

+
+
+ +

@maintenanceRequest.RequestType

+
+
+ +
+ +

@maintenanceRequest.Title

+
+ +
+ +

@maintenanceRequest.Description

+
+ + @if (maintenanceRequest.LeaseId.HasValue && maintenanceRequest.Lease != null) + { +
+ +

+ Lease #@maintenanceRequest.LeaseId - @maintenanceRequest.Lease.Tenant?.FullName +

+
+ } +
+
+ + +
+
+
Contact Information
+
+
+
+
+ +

@(string.IsNullOrEmpty(maintenanceRequest.RequestedBy) ? "N/A" : maintenanceRequest.RequestedBy)

+
+
+ +

@(string.IsNullOrEmpty(maintenanceRequest.RequestedByEmail) ? "N/A" : maintenanceRequest.RequestedByEmail)

+
+
+ +

@(string.IsNullOrEmpty(maintenanceRequest.RequestedByPhone) ? "N/A" : maintenanceRequest.RequestedByPhone)

+
+
+
+
+ + +
+
+
Timeline
+
+
+
+
+ +

@maintenanceRequest.RequestedOn.ToString("MMM dd, yyyy")

+
+
+ +

@(maintenanceRequest.ScheduledOn?.ToString("MMM dd, yyyy") ?? "Not scheduled")

+
+
+ +

@(maintenanceRequest.CompletedOn?.ToString("MMM dd, yyyy") ?? "Not completed")

+
+
+ + @if (maintenanceRequest.Status != "Completed" && maintenanceRequest.Status != "Cancelled") + { +
+ +

+ @maintenanceRequest.DaysOpen days +

+
+ } + + @if (maintenanceRequest.IsOverdue) + { +
+ Overdue - Scheduled date has passed +
+ } +
+
+ + +
+
+
Assignment & Cost
+
+
+
+
+ +

@(string.IsNullOrEmpty(maintenanceRequest.AssignedTo) ? "Unassigned" : maintenanceRequest.AssignedTo)

+
+
+
+
+ +

@maintenanceRequest.EstimatedCost.ToString("C")

+
+
+ +

@maintenanceRequest.ActualCost.ToString("C")

+
+
+
+
+ + + @if (!string.IsNullOrEmpty(maintenanceRequest.ResolutionNotes) || maintenanceRequest.Status == "Completed") + { +
+
+
Resolution Notes
+
+
+

@(string.IsNullOrEmpty(maintenanceRequest.ResolutionNotes) ? "No notes provided" : maintenanceRequest.ResolutionNotes)

+
+
+ } +
+ +
+ +
+
+
Quick Actions
+
+
+ @if (maintenanceRequest.Status != "Completed" && maintenanceRequest.Status != "Cancelled") + { +
+ @if (maintenanceRequest.Status == "Submitted") + { + + } + @if (maintenanceRequest.Status == "In Progress") + { + + } + +
+ } + else + { +
+ Request is @maintenanceRequest.Status.ToLower() +
+ } +
+
+ + + @if (maintenanceRequest.Property != null) + { +
+
+
Property Info
+
+
+

@maintenanceRequest.Property.Address

+

+ + @maintenanceRequest.Property.City, @maintenanceRequest.Property.State @maintenanceRequest.Property.ZipCode + +

+

+ Type: @maintenanceRequest.Property.PropertyType +

+ +
+
+ } +
+
+} + +@code { + [Parameter] + public Guid Id { get; set; } + + private MaintenanceRequest? maintenanceRequest; + private bool isLoading = true; + + protected override async Task OnInitializedAsync() + { + await LoadMaintenanceRequest(); + } + + private async Task LoadMaintenanceRequest() + { + isLoading = true; + try + { + maintenanceRequest = await MaintenanceService.GetByIdAsync(Id); + } + finally + { + isLoading = false; + } + } + + private async Task UpdateStatus(string newStatus) + { + if (maintenanceRequest != null) + { + await MaintenanceService.UpdateMaintenanceRequestStatusAsync(maintenanceRequest.Id, newStatus); + ToastService.ShowSuccess($"Maintenance request status updated to '{newStatus}'."); + await LoadMaintenanceRequest(); + } + } + + private void Edit() + { + NavigationManager.NavigateTo($"/propertymanagement/maintenance/edit/{Id}"); + } + + private void ViewProperty() + { + if (maintenanceRequest?.PropertyId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/view/{maintenanceRequest.PropertyId}"); + } + } + + private void GoBack() + { + NavigationManager.NavigateTo("/propertymanagement/maintenance"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor new file mode 100644 index 0000000..5871bf1 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor @@ -0,0 +1,4 @@ +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization diff --git a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/CreatePayment.razor b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/CreatePayment.razor new file mode 100644 index 0000000..a8bbbec --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/CreatePayment.razor @@ -0,0 +1,285 @@ +@page "/propertymanagement/payments/create" +@using Aquiis.Professional.Features.PropertyManagement +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Http.HttpResults +@inject NavigationManager Navigation +@inject PaymentService PaymentService +@inject InvoiceService InvoiceService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Record Payment - Property Management + +
+
+
+

Record Payment

+ +
+ + @if (invoices == null || !invoices.Any()) + { +
+

No Unpaid Invoices

+

There are no outstanding invoices to record payments for.

+ Go to Invoices +
+ } + else + { +
+
+ + + + +
+ + + + @foreach (var invoice in invoices) + { + var displayText = $"{invoice.InvoiceNumber} - {invoice.Lease?.Property?.Address} - {invoice.Lease?.Tenant?.FullName} - Balance: {invoice.BalanceDue:C}"; + + } + + +
+ +
+ + + +
+ +
+ + + + @if (selectedInvoice != null) + { + Invoice balance due: @selectedInvoice.BalanceDue.ToString("C") + } +
+ +
+ + + + + + + + + + + + +
+ +
+ + + +
+ +
+ + +
+
+
+
+ } +
+ +
+ @if (selectedInvoice != null) + { +
+
+
Invoice Summary
+
+
+
+ +

@selectedInvoice.InvoiceNumber

+
+
+ +

@selectedInvoice.Lease?.Property?.Address

+
+
+ +

@selectedInvoice.Lease?.Tenant?.FullName

+
+
+
+
+ Invoice Amount: + @selectedInvoice.Amount.ToString("C") +
+
+
+
+ Already Paid: + @selectedInvoice.AmountPaid.ToString("C") +
+
+
+
+ Balance Due: + @selectedInvoice.BalanceDue.ToString("C") +
+
+ @if (paymentModel.Amount > 0) + { +
+
+
+ This Payment: + @paymentModel.Amount.ToString("C") +
+
+
+
+ Remaining Balance: + + @remainingBalance.ToString("C") + +
+
+ @if (remainingBalance < 0) + { +
+ Warning: Payment amount exceeds balance due. +
+ } + else if (remainingBalance == 0) + { +
+ This payment will mark the invoice as Paid. +
+ } + else + { +
+ This will be a partial payment. +
+ } + } +
+
+ } + else + { +
+
+ +

Select an invoice to see details

+
+
+ } +
+
+ +@code { + private List? invoices; + private Invoice? selectedInvoice; + private PaymentModel paymentModel = new(); + private decimal remainingBalance => selectedInvoice != null ? selectedInvoice.BalanceDue - paymentModel.Amount : 0; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + [Parameter] + [SupplyParameterFromQuery] + public Guid? InvoiceId { get; set; } + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + + // Get all invoices and filter to those with outstanding balance + List? allInvoices = await InvoiceService.GetAllAsync(); + invoices = allInvoices + .Where(i => i.BalanceDue > 0 && i.Status != "Cancelled") + .OrderByDescending(i => i.DueOn) + .ToList(); + + paymentModel.PaidOn = DateTime.Now; + if (InvoiceId.HasValue) + { + paymentModel.InvoiceId = InvoiceId.Value; + await OnInvoiceSelected(); + } + } + + private async Task OnInvoiceSelected() + { + if (paymentModel.InvoiceId != Guid.Empty) + { + selectedInvoice = invoices?.FirstOrDefault(i => i.Id == paymentModel.InvoiceId); + if (selectedInvoice != null) + { + // Default payment amount to the balance due + paymentModel.Amount = selectedInvoice.BalanceDue; + } + } + else + { + selectedInvoice = null; + paymentModel.Amount = 0; + } + + await InvokeAsync(StateHasChanged); + } + + private async Task HandleCreatePayment() + { + Payment payment = new Payment + { + InvoiceId = paymentModel.InvoiceId, + PaidOn = paymentModel.PaidOn, + Amount = paymentModel.Amount, + PaymentMethod = paymentModel.PaymentMethod, + Notes = paymentModel.Notes! + }; + await PaymentService.CreateAsync(payment); + Navigation.NavigateTo("/propertymanagement/payments"); + } + + private void Cancel() + { + Navigation.NavigateTo("/propertymanagement/payments"); + } + + public class PaymentModel + { + [Required(ErrorMessage = "Please select an invoice.")] + public Guid InvoiceId { get; set; } + + [Required(ErrorMessage = "Payment date is required.")] + public DateTime PaidOn { get; set; } = DateTime.Now; + + [Required(ErrorMessage = "Amount is required.")] + [Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero.")] + public decimal Amount { get; set; } + + [Required(ErrorMessage = "Payment method is required.")] + public string PaymentMethod { get; set; } = string.Empty; + + [MaxLength(1000)] + public string? Notes { get; set; } = string.Empty; + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/EditPayment.razor b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/EditPayment.razor new file mode 100644 index 0000000..eb492c0 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/EditPayment.razor @@ -0,0 +1,278 @@ +@page "/propertymanagement/payments/edit/{PaymentId:guid}" +@using Aquiis.Professional.Features.PropertyManagement +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations +@inject NavigationManager Navigation +@inject PaymentService PaymentService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Edit Payment - Property Management + +@if (payment == null || paymentModel == null) +{ +
+
+ Loading... +
+
+} +else +{ +
+
+
+

Edit Payment

+ +
+ +
+
+ + + + +
+ + + Invoice cannot be changed after payment is created. +
+ +
+ + + +
+ +
+ + + + @if (payment.Invoice != null) + { + + Current invoice balance (before this edit): @currentInvoiceBalance.ToString("C") + + } +
+ +
+ + + + + + + + + + + + +
+ +
+ + + +
+ +
+ + +
+
+
+
+
+ +
+ @if (payment.Invoice != null) + { +
+
+
Invoice Information
+
+
+
+ +

+ + @payment.Invoice.InvoiceNumber + +

+
+
+ +

@payment.Invoice.Lease?.Property?.Address

+
+
+ +

@payment.Invoice.Lease?.Tenant?.FullName

+
+
+
+
+ Invoice Amount: + @payment.Invoice.Amount.ToString("C") +
+
+
+
+ Total Paid: + @payment.Invoice.AmountPaid.ToString("C") +
+
+
+
+ Balance Due: + + @payment.Invoice.BalanceDue.ToString("C") + +
+
+
+
+ Status: + + @if (payment.Invoice.Status == "Paid") + { + @payment.Invoice.Status + } + else if (payment.Invoice.Status == "Partial") + { + Partially Paid + } + else if (payment.Invoice.Status == "Overdue") + { + @payment.Invoice.Status + } + else + { + @payment.Invoice.Status + } + +
+
+
+
+ +
+
+
Current Payment
+
+
+
+ +

@payment.Amount.ToString("C")

+
+ @if (paymentModel.Amount != payment.Amount) + { +
+ +

@paymentModel.Amount.ToString("C")

+
+
+ +

+ @(amountDifference >= 0 ? "+" : "")@amountDifference.ToString("C") +

+
+
+
+ +

+ @newInvoiceBalance.ToString("C") +

+
+ @if (newInvoiceBalance < 0) + { +
+ Warning: Total payments exceed invoice amount. +
+ } + else if (newInvoiceBalance == 0) + { +
+ Invoice will be marked as Paid. +
+ } + } +
+
+ } +
+
+} + +@code { + [Parameter] + public Guid PaymentId { get; set; } + + private Payment? payment; + private PaymentModel? paymentModel; + + private decimal currentInvoiceBalance => payment?.Invoice != null ? payment.Invoice.BalanceDue + payment.Amount : 0; + private decimal amountDifference => paymentModel != null && payment != null ? paymentModel.Amount - payment.Amount : 0; + private decimal newInvoiceBalance => currentInvoiceBalance - (paymentModel?.Amount ?? 0) + (payment?.Amount ?? 0); + + protected override async Task OnInitializedAsync() + { + payment = await PaymentService.GetByIdAsync(PaymentId); + + if (payment == null) + { + Navigation.NavigateTo("/propertymanagement/payments"); + return; + } + + paymentModel = new PaymentModel + { + PaidOn = payment.PaidOn, + Amount = payment.Amount, + PaymentMethod = payment.PaymentMethod, + Notes = payment.Notes + }; + } + + private async Task HandleUpdatePayment() + { + if (payment == null || paymentModel == null) return; + + payment.PaidOn = paymentModel.PaidOn; + payment.Amount = paymentModel.Amount; + payment.PaymentMethod = paymentModel.PaymentMethod; + payment.Notes = paymentModel.Notes!; + + await PaymentService.UpdateAsync(payment); + Navigation.NavigateTo("/propertymanagement/payments"); + } + + private void Cancel() + { + Navigation.NavigateTo("/propertymanagement/payments"); + } + + public class PaymentModel + { + [Required(ErrorMessage = "Payment date is required.")] + public DateTime PaidOn { get; set; } + + [Required(ErrorMessage = "Amount is required.")] + [Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero.")] + public decimal Amount { get; set; } + + [Required(ErrorMessage = "Payment method is required.")] + public string PaymentMethod { get; set; } = string.Empty; + + [MaxLength(1000)] + public string? Notes { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Payments.razor b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Payments.razor new file mode 100644 index 0000000..00f21fb --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Payments.razor @@ -0,0 +1,492 @@ +@page "/propertymanagement/payments" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@inject NavigationManager Navigation +@inject PaymentService PaymentService +@inject IJSRuntime JSRuntime + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Payments - Property Management + +
+

Payments

+ +
+ +@if (payments == null) +{ +
+
+ Loading... +
+
+} +else if (!payments.Any()) +{ +
+

No Payments Found

+

Get started by recording your first payment.

+ +
+} +else +{ +
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+ +
+
+
+
+
Total Payments
+

@paymentsCount

+ @totalAmount.ToString("C") +
+
+
+
+
+
+
This Month
+

@thisMonthCount

+ @thisMonthAmount.ToString("C") +
+
+
+
+
+
+
This Year
+

@thisYearCount

+ @thisYearAmount.ToString("C") +
+
+
+
+
+
+
Average Payment
+

@averageAmount.ToString("C")

+ Per transaction +
+
+
+
+ +
+
+ @if (groupByInvoice) + { + @foreach (var invoiceGroup in groupedPayments) + { + var invoice = invoiceGroup.First().Invoice; + var invoiceTotal = invoiceGroup.Sum(p => p.Amount); + var isExpanded = expandedInvoices.Contains(invoiceGroup.Key); + +
+
+
+
+ + Invoice: @invoice?.InvoiceNumber + @invoice?.Lease?.Property?.Address + • @invoice?.Lease?.Tenant?.FullName +
+
+ @invoiceGroup.Count() payment(s) + @invoiceTotal.ToString("C") +
+
+
+ @if (isExpanded) + { +
+ + + + + + + + + + + + @foreach (var payment in invoiceGroup) + { + + + + + + + + } + +
Payment DateAmountPayment MethodNotesActions
@payment.PaidOn.ToString("MMM dd, yyyy")@payment.Amount.ToString("C")@payment.PaymentMethod@(string.IsNullOrEmpty(payment.Notes) ? "-" : payment.Notes) +
+ + + +
+
+
+ } +
+ } + } + else + { +
+ + + + + + + + + + + + + + @foreach (var payment in pagedPayments) + { + + + + + + + + + + } + +
+ + Invoice #PropertyTenant + + Payment MethodActions
@payment.PaidOn.ToString("MMM dd, yyyy") + + @payment.Invoice?.InvoiceNumber + + @payment.Invoice?.Lease?.Property?.Address@payment.Invoice?.Lease?.Tenant?.FullName@payment.Amount.ToString("C") + @payment.PaymentMethod + +
+ + + +
+
+
+ } + + @if (totalPages > 1 && !groupByInvoice) + { +
+
+ +
+
+ Showing @((currentPage - 1) * pageSize + 1) to @Math.Min(currentPage * pageSize, totalRecords) of @totalRecords payments +
+ +
+ } +
+
+} + +@code { + private List? payments; + private List filteredPayments = new(); + private List pagedPayments = new(); + private IEnumerable> groupedPayments = Enumerable.Empty>(); + private HashSet expandedInvoices = new(); + private string searchTerm = string.Empty; + private string selectedMethod = string.Empty; + private string sortColumn = nameof(Payment.PaidOn); + private bool sortAscending = false; + private bool groupByInvoice = true; + + private int paymentsCount = 0; + private int thisMonthCount = 0; + private int thisYearCount = 0; + private decimal totalAmount = 0; + private decimal thisMonthAmount = 0; + private decimal thisYearAmount = 0; + private decimal averageAmount = 0; + + private int currentPage = 1; + private int pageSize = 20; + private int totalPages = 1; + private int totalRecords = 0; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadPayments(); + } + + private async Task LoadPayments() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (!string.IsNullOrEmpty(userId)) + { + payments = await PaymentService.GetAllAsync(); + FilterPayments(); + UpdateStatistics(); + } + } + + private void FilterPayments() + { + if (payments == null) return; + + filteredPayments = payments.Where(p => + { + bool matchesSearch = string.IsNullOrWhiteSpace(searchTerm) || + (p.Invoice?.InvoiceNumber?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false) || + (p.Invoice?.Lease?.Property?.Address?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false) || + (p.Invoice?.Lease?.Tenant?.FullName?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ?? false) || + p.PaymentMethod.Contains(searchTerm, StringComparison.OrdinalIgnoreCase); + + bool matchesMethod = string.IsNullOrWhiteSpace(selectedMethod) || + p.PaymentMethod.Equals(selectedMethod, StringComparison.OrdinalIgnoreCase); + + return matchesSearch && matchesMethod; + }).ToList(); + + SortPayments(); + + if (groupByInvoice) + { + groupedPayments = filteredPayments + .GroupBy(p => p.InvoiceId) + .OrderByDescending(g => g.Max(p => p.PaidOn)) + .ToList(); + } + else + { + UpdatePagination(); + } + } + + private void ToggleInvoiceGroup(Guid invoiceId) + { + if (expandedInvoices.Contains(invoiceId)) + { + expandedInvoices.Remove(invoiceId); + } + else + { + expandedInvoices.Add(invoiceId); + } + } + + private void SortBy(string column) + { + if (sortColumn == column) + { + sortAscending = !sortAscending; + } + else + { + sortColumn = column; + sortAscending = true; + } + SortPayments(); + UpdatePagination(); + } + + private void SortPayments() + { + filteredPayments = sortColumn switch + { + nameof(Payment.PaidOn) => sortAscending + ? filteredPayments.OrderBy(p => p.PaidOn).ToList() + : filteredPayments.OrderByDescending(p => p.PaidOn).ToList(), + nameof(Payment.Amount) => sortAscending + ? filteredPayments.OrderBy(p => p.Amount).ToList() + : filteredPayments.OrderByDescending(p => p.Amount).ToList(), + _ => filteredPayments.OrderByDescending(p => p.PaidOn).ToList() + }; + } + + private void UpdateStatistics() + { + if (payments == null) return; + + var now = DateTime.Now; + var firstDayOfMonth = new DateTime(now.Year, now.Month, 1); + var firstDayOfYear = new DateTime(now.Year, 1, 1); + + paymentsCount = payments.Count; + thisMonthCount = payments.Count(p => p.PaidOn >= firstDayOfMonth); + thisYearCount = payments.Count(p => p.PaidOn >= firstDayOfYear); + + totalAmount = payments.Sum(p => p.Amount); + thisMonthAmount = payments.Where(p => p.PaidOn >= firstDayOfMonth).Sum(p => p.Amount); + thisYearAmount = payments.Where(p => p.PaidOn >= firstDayOfYear).Sum(p => p.Amount); + averageAmount = paymentsCount > 0 ? totalAmount / paymentsCount : 0; + } + + private void UpdatePagination() + { + totalRecords = filteredPayments.Count; + totalPages = (int)Math.Ceiling(totalRecords / (double)pageSize); + currentPage = Math.Min(currentPage, Math.Max(1, totalPages)); + + pagedPayments = filteredPayments + .Skip((currentPage - 1) * pageSize) + .Take(pageSize) + .ToList(); + } + + private void ClearFilters() + { + searchTerm = string.Empty; + selectedMethod = string.Empty; + groupByInvoice = false; + FilterPayments(); + } + + private void FirstPage() => GoToPage(1); + private void LastPage() => GoToPage(totalPages); + private void NextPage() => GoToPage(currentPage + 1); + private void PreviousPage() => GoToPage(currentPage - 1); + + private void GoToPage(int page) + { + currentPage = Math.Max(1, Math.Min(page, totalPages)); + UpdatePagination(); + } + + private void CreatePayment() + { + Navigation.NavigateTo("/propertymanagement/payments/create"); + } + + private void ViewPayment(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/payments/view/{id}"); + } + + private void EditPayment(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/payments/edit/{id}"); + } + + private async Task DeletePayment(Payment payment) + { + if (await JSRuntime.InvokeAsync("confirm", $"Are you sure you want to delete this payment of {payment.Amount:C}?")) + { + await PaymentService.DeleteAsync(payment.Id); + await LoadPayments(); + } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/ViewPayment.razor b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/ViewPayment.razor new file mode 100644 index 0000000..d1ca627 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/ViewPayment.razor @@ -0,0 +1,417 @@ +@page "/propertymanagement/payments/view/{PaymentId:guid}" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@inject NavigationManager Navigation +@inject PaymentService PaymentService +@inject Application.Services.DocumentService DocumentService +@inject UserContextService UserContextService +@inject IJSRuntime JSRuntime + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +View Payment - Property Management + +@if (payment == null) +{ +
+
+ Loading... +
+
+} +else +{ +
+
+

Payment Details

+

Payment Date: @payment.PaidOn.ToString("MMMM dd, yyyy")

+
+
+ + +
+
+ +
+
+
+
+
Payment Information
+
+
+
+
+ +

@payment.PaidOn.ToString("MMMM dd, yyyy")

+
+
+ +

@payment.Amount.ToString("C")

+
+
+
+
+ +

+ @payment.PaymentMethod +

+
+
+ @if (!string.IsNullOrWhiteSpace(payment.Notes)) + { +
+
+ +

@payment.Notes

+
+
+ } +
+
+ +
+
+
Invoice Information
+
+
+ @if (payment.Invoice != null) + { +
+
+ +

+ + @payment.Invoice.InvoiceNumber + +

+
+
+ +

+ @if (payment.Invoice.Status == "Paid") + { + @payment.Invoice.Status + } + else if (payment.Invoice.Status == "Partial") + { + Partially Paid + } + else if (payment.Invoice.Status == "Overdue") + { + @payment.Invoice.Status + } + else + { + @payment.Invoice.Status + } +

+
+
+
+
+ +

@payment.Invoice.Amount.ToString("C")

+
+
+ +

@payment.Invoice.AmountPaid.ToString("C")

+
+
+ +

+ @payment.Invoice.BalanceDue.ToString("C") +

+
+
+
+
+ +

@payment.Invoice.InvoicedOn.ToString("MMM dd, yyyy")

+
+
+ +

+ @payment.Invoice.DueOn.ToString("MMM dd, yyyy") + @if (payment.Invoice.IsOverdue) + { + @payment.Invoice.DaysOverdue days overdue + } +

+
+
+ @if (!string.IsNullOrWhiteSpace(payment.Invoice.Description)) + { +
+
+ +

@payment.Invoice.Description

+
+
+ } + } +
+
+ + @if (payment.Invoice?.Lease != null) + { +
+
+
Lease & Property Information
+
+
+ +
+
+ +

@payment.Invoice.Lease.MonthlyRent.ToString("C")

+
+
+ +

+ @if (payment.Invoice.Lease.Status == "Active") + { + @payment.Invoice.Lease.Status + } + else if (payment.Invoice.Lease.Status == "Expired") + { + @payment.Invoice.Lease.Status + } + else + { + @payment.Invoice.Lease.Status + } +

+
+
+
+
+ +

@payment.Invoice.Lease.StartDate.ToString("MMM dd, yyyy")

+
+
+ +

@payment.Invoice.Lease.EndDate.ToString("MMM dd, yyyy")

+
+
+
+
+ } +
+ +
+
+
+
Quick Actions
+
+
+
+ + @if (payment.DocumentId == null) + { + + } + else + { + + + } + + View Invoice + + @if (payment.Invoice?.Lease != null) + { + + View Lease + + + View Property + + + View Tenant + + } +
+
+
+ +
+
+
Metadata
+
+
+
+ +

@payment.CreatedOn.ToString("g")

+ @if (!string.IsNullOrEmpty(payment.CreatedBy)) + { + by @payment.CreatedBy + } +
+ @if (payment.LastModifiedOn.HasValue) + { +
+ +

@payment.LastModifiedOn.Value.ToString("g")

+ @if (!string.IsNullOrEmpty(payment.LastModifiedBy)) + { + by @payment.LastModifiedBy + } +
+ } +
+
+
+
+} + +@code { + [Parameter] + public Guid PaymentId { get; set; } + + private Payment? payment; + private bool isGenerating = false; + private Document? document = null; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + payment = await PaymentService.GetByIdAsync(PaymentId); + + if (payment == null) + { + Navigation.NavigateTo("/propertymanagement/payments"); + } + else if (payment.DocumentId != null) + { + // Load the document if it exists + document = await DocumentService.GetByIdAsync(payment.DocumentId.Value); + } + } + + private void EditPayment() + { + Navigation.NavigateTo($"/propertymanagement/payments/edit/{PaymentId}"); + } + + private void GoBack() + { + Navigation.NavigateTo("/propertymanagement/payments"); + } + + private async Task ViewDocument() + { + if (document != null) + { + var base64Data = Convert.ToBase64String(document.FileData); + await JSRuntime.InvokeVoidAsync("viewFile", base64Data, document.FileType); + } + } + + private async Task DownloadDocument() + { + if (document != null) + { + var fileName = document.FileName; + var fileData = document.FileData; + var mimeType = document.FileType; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(fileData), mimeType); + } + } + + private async Task GeneratePaymentReceipt() + { + isGenerating = true; + StateHasChanged(); + + try + { + // Generate the PDF receipt + byte[] pdfBytes = Aquiis.Professional.Application.Services.PdfGenerators.PaymentPdfGenerator.GeneratePaymentReceipt(payment!); + + // Create the document entity + var document = new Document + { + FileName = $"Receipt_{payment!.PaidOn:yyyyMMdd}_{DateTime.Now:HHmmss}.pdf", + FileExtension = ".pdf", + FileData = pdfBytes, + FileSize = pdfBytes.Length, + FileType = "application/pdf", + ContentType = "application/pdf", + DocumentType = "Payment Receipt", + Description = $"Payment receipt for {payment.Amount:C} on {payment.PaidOn:MMM dd, yyyy}", + LeaseId = payment.Invoice?.LeaseId, + PropertyId = payment.Invoice?.Lease?.PropertyId, + TenantId = payment.Invoice?.Lease?.TenantId, + InvoiceId = payment.InvoiceId, + IsDeleted = false + }; + + // Save to database + await DocumentService.CreateAsync(document); + + // Update payment with DocumentId + payment.DocumentId = document.Id; + + await PaymentService.UpdateAsync(payment); + + // Reload payment and document + this.document = document; + StateHasChanged(); + + await JSRuntime.InvokeVoidAsync("alert", "Payment receipt generated successfully!"); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"Error generating payment receipt: {ex.Message}"); + } + finally + { + isGenerating = false; + StateHasChanged(); + } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/_Imports.razor b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/_Imports.razor new file mode 100644 index 0000000..f940988 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Aquiis.Professional +@using Aquiis.Professional.Infrastructure.Data +@using Aquiis.Professional.Core.Entities diff --git a/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Create.razor b/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Create.razor new file mode 100644 index 0000000..d1a54b8 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Create.razor @@ -0,0 +1,260 @@ +@page "/propertymanagement/properties/create" +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations +@inject NavigationManager Navigation +@inject PropertyService PropertyService + +@rendermode InteractiveServer + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + +
+
+
+
+

Add New Property

+
+
+ + + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + +
+
+ + + +
+
+ +
+
+ + + +
+ @*
+ + + +
*@ +
+ +
+
+ + + +
+
+ + + + @foreach (var state in States.StatesArray()) + { + + } + + +
+
+ + + +
+
+ +
+
+ + + + + + + + + + + + +
+
+ + + +
+
+ +
+
+ + + + + + + + + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+ +@code { + private PropertyModel propertyModel = new(); + private bool isSubmitting = false; + private string errorMessage = string.Empty; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + private async Task SaveProperty() + { + isSubmitting = true; + errorMessage = string.Empty; + + var property = new Property + { + Address = propertyModel.Address, + UnitNumber = propertyModel.UnitNumber, + City = propertyModel.City, + State = propertyModel.State, + ZipCode = propertyModel.ZipCode, + PropertyType = propertyModel.PropertyType, + MonthlyRent = propertyModel.MonthlyRent, + Bedrooms = propertyModel.Bedrooms, + Bathrooms = propertyModel.Bathrooms, + SquareFeet = propertyModel.SquareFeet, + Description = propertyModel.Description, + Status = propertyModel.Status, + IsAvailable = propertyModel.IsAvailable, + }; + + // Save the property using a service or API call + await PropertyService.CreateAsync(property); + + isSubmitting = false; + // Redirect to the properties list page after successful addition + Navigation.NavigateTo("/propertymanagement/properties"); + } + + private void Cancel() + { + Navigation.NavigateTo("/propertymanagement/properties"); + } + + + public class PropertyModel + { + [Required(ErrorMessage = "Address is required")] + [StringLength(200, ErrorMessage = "Address cannot exceed 200 characters")] + public string Address { get; set; } = string.Empty; + + [StringLength(50, ErrorMessage = "Unit number cannot exceed 50 characters")] + public string? UnitNumber { get; set; } + + [StringLength(100, ErrorMessage = "City cannot exceed 100 characters")] + public string City { get; set; } = string.Empty; + + [StringLength(50, ErrorMessage = "State cannot exceed 50 characters")] + public string State { get; set; } = string.Empty; + + [StringLength(10, ErrorMessage = "Zip Code cannot exceed 10 characters")] + [DataType(DataType.PostalCode)] + [RegularExpression(@"^\d{5}(-\d{4})?$", ErrorMessage = "Invalid Zip Code format.")] + [MaxLength(10, ErrorMessage = "Zip Code cannot exceed 10 characters.")] + [Display(Name = "Postal Code", Description = "Postal Code of the property", + Prompt = "e.g., 12345 or 12345-6789", ShortName = "ZIP")] + public string ZipCode { get; set; } = string.Empty; + + [Required(ErrorMessage = "Property type is required")] + [StringLength(50, ErrorMessage = "Property type cannot exceed 50 characters")] + public string PropertyType { get; set; } = string.Empty; + + [Required(ErrorMessage = "Monthly rent is required")] + [Range(0.01, double.MaxValue, ErrorMessage = "Monthly rent must be greater than 0")] + public decimal MonthlyRent { get; set; } + + [Range(0, int.MaxValue, ErrorMessage = "Bedrooms cannot be negative")] + public int Bedrooms { get; set; } + + [Range(0.0, double.MaxValue, ErrorMessage = "Bathrooms cannot be negative")] + public decimal Bathrooms { get; set; } + + [Range(0, int.MaxValue, ErrorMessage = "Square feet cannot be negative")] + public int SquareFeet { get; set; } + + [StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")] + public string Description { get; set; } = string.Empty; + + [Required(ErrorMessage = "Status is required")] + [StringLength(50, ErrorMessage = "Status cannot exceed 50 characters")] + public string Status { get; set; } = ApplicationConstants.PropertyStatuses.Available; + + public bool IsAvailable { get; set; } = true; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Edit.razor b/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Edit.razor new file mode 100644 index 0000000..4ef965c --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Edit.razor @@ -0,0 +1,399 @@ +@page "/propertymanagement/properties/edit/{PropertyId:guid}" + +@using System.ComponentModel.DataAnnotations +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Components.Authorization +@using System.Security.Claims +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Authorization + +@rendermode InteractiveServer +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject PropertyService PropertyService +@inject NavigationManager NavigationManager + +@if (property == null) +{ +
+
+ Loading... +
+
+} +else if (!isAuthorized) +{ +
+

Access Denied

+

You don't have permission to edit this property.

+ Back to Properties +
+} +else +{ +
+
+
+
+

Edit Property

+
+
+ + + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + @foreach (var state in States.StatesArray()) + { + + } + + +
+
+ +
+
+ + + + + + + + + + + + +
+
+ + + +
+
+ +
+
+ + + + + + + + + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+
+ + +
+
+
+ +
+ + + +
+
+
+
+
+ +
+
+
+
Property Actions
+
+
+
+ + +
+
+
+ +
+
+
Property Information
+
+
+ + Created: @property.CreatedOn.ToString("MMMM dd, yyyy") +
+ @if (property.LastModifiedOn.HasValue) + { + Last Modified: @property.LastModifiedOn.Value.ToString("MMMM dd, yyyy") + } +
+
+
+
+
+} + + +@code { + [Parameter] + public Guid PropertyId { get; set; } + + private string currentUserId = string.Empty; + private string errorMessage = string.Empty; + + private Property? property; + private PropertyModel propertyModel = new(); + private bool isSubmitting = false; + private bool isAuthorized = true; + private string successMessage = string.Empty; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadPropertyAsync(); + } + + private async Task LoadPropertyAsync() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + isAuthorized = false; + return; + } + + property = await PropertyService.GetByIdAsync(PropertyId); + + if (property == null) + { + isAuthorized = false; + return; + } + + // Map property to model + propertyModel = new PropertyModel + { + Address = property.Address, + UnitNumber = property.UnitNumber, + City = property.City, + State = property.State, + ZipCode = property.ZipCode, + PropertyType = property.PropertyType, + MonthlyRent = property.MonthlyRent, + Bedrooms = property.Bedrooms, + Bathrooms = property.Bathrooms, + SquareFeet = property.SquareFeet, + Description = property.Description, + Status = property.Status, + IsAvailable = property.IsAvailable + }; + } + + private async Task SavePropertyAsync() + { + if (property != null) + { + await PropertyService.UpdateAsync(property); + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + } + + private async Task DeleteProperty() + { + if (property != null) + { + await PropertyService.DeleteAsync(property.Id); + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + } + + private void ViewProperty() + { + if (property != null) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/view/{property.Id}"); + } + } + + private async Task UpdatePropertyAsync() + { + + if (property != null) + { + try { + isSubmitting = true; + errorMessage = string.Empty; + successMessage = string.Empty; + + // Update property with form data + property!.Address = propertyModel.Address; + property.UnitNumber = propertyModel.UnitNumber; + property.City = propertyModel.City; + property.State = propertyModel.State; + property.ZipCode = propertyModel.ZipCode; + property.PropertyType = propertyModel.PropertyType; + property.MonthlyRent = propertyModel.MonthlyRent; + property.Bedrooms = propertyModel.Bedrooms; + property.Bathrooms = propertyModel.Bathrooms; + property.SquareFeet = propertyModel.SquareFeet; + property.Description = propertyModel.Description; + property.Status = propertyModel.Status; + property.IsAvailable = propertyModel.IsAvailable; + + await PropertyService.UpdateAsync(property); + } catch (Exception ex) + { + errorMessage = $"An error occurred while updating the property: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + + public class PropertyModel + { + [Required(ErrorMessage = "Address is required")] + [StringLength(200, ErrorMessage = "Address cannot exceed 200 characters")] + public string Address { get; set; } = string.Empty; + + [StringLength(50, ErrorMessage = "Unit number cannot exceed 50 characters")] + public string? UnitNumber { get; set; } + + [StringLength(100, ErrorMessage = "City cannot exceed 100 characters")] + public string City { get; set; } = string.Empty; + + [StringLength(50, ErrorMessage = "State cannot exceed 50 characters")] + public string State { get; set; } = string.Empty; + + [StringLength(20, ErrorMessage = "Zip Code cannot exceed 20 characters")] + public string ZipCode { get; set; } = string.Empty; + + [Required(ErrorMessage = "Property type is required")] + [StringLength(50, ErrorMessage = "Property type cannot exceed 50 characters")] + public string PropertyType { get; set; } = string.Empty; + + [Required(ErrorMessage = "Monthly rent is required")] + [Range(0.01, double.MaxValue, ErrorMessage = "Monthly rent must be greater than 0")] + public decimal MonthlyRent { get; set; } + + [Range(0, int.MaxValue, ErrorMessage = "Bedrooms cannot be negative")] + public int Bedrooms { get; set; } + + [Range(0.0, double.MaxValue, ErrorMessage = "Bathrooms cannot be negative")] + public decimal Bathrooms { get; set; } + + [Range(0, int.MaxValue, ErrorMessage = "Square feet cannot be negative")] + public int SquareFeet { get; set; } + + [StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")] + public string Description { get; set; } = string.Empty; + + [Required(ErrorMessage = "Status is required")] + [StringLength(50, ErrorMessage = "Status cannot exceed 50 characters")] + public string Status { get; set; } = ApplicationConstants.PropertyStatuses.Available; + + public bool IsAvailable { get; set; } = true; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Index.razor b/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Index.razor new file mode 100644 index 0000000..3747e97 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Index.razor @@ -0,0 +1,558 @@ +@page "/propertymanagement/properties" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@inject NavigationManager Navigation +@inject PropertyService PropertyService +@inject IJSRuntime JSRuntime +@inject UserContextService UserContext + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager", "User")] +@rendermode InteractiveServer + +
+

Properties

+
+
+ + +
+ @if (!isReadOnlyUser) + { + + } +
+
+ +@if (properties == null) +{ +
+
+ Loading... +
+
+} +else if (!properties.Any()) +{ +
+

No Properties Found

+

Get started by adding your first property to the system.

+ +
+} +else +{ +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
Available
+

@availableCount

+
+
+
+
+
+
+
Pending Lease
+

@pendingCount

+
+
+
+
+
+
+
Occupied
+

@occupiedCount

+
+
+
+ @*
+
+
+
Total Properties
+

@filteredProperties.Count

+
+
+
*@ +
+
+
+
Total Rent/Month
+

@totalMonthlyRent.ToString("C")

+
+
+
+
+ + @if (isGridView) + { + +
+ @foreach (var property in filteredProperties) + { +
+
+
+
+
@property.Address @(!string.IsNullOrWhiteSpace(property.UnitNumber) ? $", {property.UnitNumber}" : "")
+ + @property.Status + +
+

@property.City, @property.State @property.ZipCode

+

@property.Description

+
+
+ Bedrooms +
@property.Bedrooms
+
+
+ Bathrooms +
@property.Bathrooms
+
+
+ Sq Ft +
@property.SquareFeet.ToString("N0")
+
+
+
+ @property.MonthlyRent.ToString("C") + /month +
+
+ +
+
+ } +
+ } + else + { + +
+
+
+ + + + + + + + + + + + + + + + @foreach (var property in pagedProperties) + { + + + + + + + + + + + + } + +
+ Address + @if (sortColumn == nameof(Property.Address)) + { + + } + + City + @if (sortColumn == nameof(Property.City)) + { + + } + + Type + @if (sortColumn == nameof(Property.PropertyType)) + { + + } + BedsBaths + Sq Ft + @if (sortColumn == nameof(Property.SquareFeet)) + { + + } + + Status + @if (sortColumn == nameof(Property.Status)) + { + + } + + Rent + @if (sortColumn == nameof(Property.MonthlyRent)) + { + + } + Actions
+ @property.Address @(!string.IsNullOrWhiteSpace(property.UnitNumber) ? $", {property.UnitNumber}" : "") +
+ @property.State @property.ZipCode +
@property.City@property.PropertyType@property.Bedrooms@property.Bathrooms@property.SquareFeet.ToString("N0") + + @FormatPropertyStatus(property.Status) + + + @property.MonthlyRent.ToString("C") + +
+ + @if (!isReadOnlyUser) + { + + + } +
+
+
+
+ @if (totalPages > 1) + { + + } +
+ } +} + +@code { + private List properties = new(); + private List filteredProperties = new(); + private List sortedProperties = new(); + private List pagedProperties = new(); + private string searchTerm = string.Empty; + private string selectedPropertyStatus = string.Empty; + private int availableCount = 0; + private int pendingCount = 0; + private int occupiedCount = 0; + private decimal totalMonthlyRent = 0; + private bool isGridView = false; + + // Sorting + private string sortColumn = nameof(Property.Address); + private bool sortAscending = true; + + // Pagination + private int currentPage = 1; + private int pageSize = 25; + private int totalPages = 1; + private int totalRecords = 0; + + [Parameter] + [SupplyParameterFromQuery] + public int? PropertyId { get; set; } + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + private string? currentUserRole; + private bool isReadOnlyUser => currentUserRole == ApplicationConstants.OrganizationRoles.User; + + protected override async Task OnInitializedAsync() + { + // Get current user's role + currentUserRole = await UserContext.GetCurrentOrganizationRoleAsync(); + + // Load properties from API or service + await LoadProperties(); + FilterProperties(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && PropertyId.HasValue) + { + await JSRuntime.InvokeVoidAsync("scrollToElement", $"property-{PropertyId.Value}"); + } + } + + private async Task LoadProperties() + { + var authState = await AuthenticationStateTask; + var userId = authState.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + if(string.IsNullOrEmpty(userId)){ + properties = new List(); + return; + } + + var allProperties = await PropertyService.GetAllAsync(); + properties = allProperties.Where(p=>p.IsDeleted==false).ToList(); + } + + private void FilterProperties() + { + if (properties == null) + { + filteredProperties = new(); + return; + } + + filteredProperties = properties.Where(p => + (string.IsNullOrEmpty(searchTerm) || + p.Address.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + p.City.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + p.State.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + p.ZipCode.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + p.Description.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + p.PropertyType.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) && + (string.IsNullOrEmpty(selectedPropertyStatus) || p.Status.ToString() == selectedPropertyStatus) + ).ToList(); + + CalculateMetrics(); + SortAndPaginateProperties(); + } + + private void CalculateMetrics(){ + if (filteredProperties != null) + { + availableCount = filteredProperties.Count(p => p.Status == ApplicationConstants.PropertyStatuses.Available); + pendingCount = filteredProperties.Count(p => p.Status == ApplicationConstants.PropertyStatuses.ApplicationPending || p.Status == ApplicationConstants.PropertyStatuses.LeasePending); + occupiedCount = filteredProperties.Count(p => p.Status == ApplicationConstants.PropertyStatuses.Occupied); + totalMonthlyRent = filteredProperties.Sum(p => p.MonthlyRent); + } + } + + private void CreateProperty(){ + Navigation.NavigateTo("/propertymanagement/properties/create"); + } + + private void ViewProperty(Guid propertyId) + { + Navigation.NavigateTo($"/propertymanagement/properties/view/{propertyId}"); + } + + private void EditProperty(Guid propertyId) + { + Navigation.NavigateTo($"/propertymanagement/properties/edit/{propertyId}"); + } + + private async Task DeleteProperty(Guid propertyId) + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + return; + + await PropertyService.DeleteAsync(propertyId); + + // Add confirmation dialog in a real application + await LoadProperties(); + FilterProperties(); + CalculateMetrics(); + } + + private void ClearFilters() + { + searchTerm = string.Empty; + selectedPropertyStatus = string.Empty; + FilterProperties(); + } + + private void SetViewMode(bool gridView) + { + isGridView = gridView; + } + + private void SortTable(string column) + { + if (sortColumn == column) + { + sortAscending = !sortAscending; + } + else + { + sortColumn = column; + sortAscending = true; + } + SortAndPaginateProperties(); + } + + private void SortAndPaginateProperties() + { + // Sort + sortedProperties = sortColumn switch + { + nameof(Property.Address) => sortAscending + ? filteredProperties.OrderBy(p => p.Address).ToList() + : filteredProperties.OrderByDescending(p => p.Address).ToList(), + nameof(Property.City) => sortAscending + ? filteredProperties.OrderBy(p => p.City).ToList() + : filteredProperties.OrderByDescending(p => p.City).ToList(), + nameof(Property.PropertyType) => sortAscending + ? filteredProperties.OrderBy(p => p.PropertyType).ToList() + : filteredProperties.OrderByDescending(p => p.PropertyType).ToList(), + nameof(Property.SquareFeet) => sortAscending + ? filteredProperties.OrderBy(p => p.SquareFeet).ToList() + : filteredProperties.OrderByDescending(p => p.SquareFeet).ToList(), + nameof(Property.Status) => sortAscending + ? filteredProperties.OrderBy(p => p.Status).ToList() + : filteredProperties.OrderByDescending(p => p.Status).ToList(), + nameof(Property.MonthlyRent) => sortAscending + ? filteredProperties.OrderBy(p => p.MonthlyRent).ToList() + : filteredProperties.OrderByDescending(p => p.MonthlyRent).ToList(), + _ => filteredProperties.OrderBy(p => p.Address).ToList() + }; + + // Paginate + totalRecords = sortedProperties.Count; + totalPages = (int)Math.Ceiling(totalRecords / (double)pageSize); + currentPage = Math.Max(1, Math.Min(currentPage, totalPages)); + + pagedProperties = sortedProperties + .Skip((currentPage - 1) * pageSize) + .Take(pageSize) + .ToList(); + } + + private void UpdatePagination() + { + currentPage = 1; + SortAndPaginateProperties(); + } + + private void FirstPage() => GoToPage(1); + private void LastPage() => GoToPage(totalPages); + private void NextPage() => GoToPage(currentPage + 1); + private void PreviousPage() => GoToPage(currentPage - 1); + + private void GoToPage(int page) + { + currentPage = Math.Max(1, Math.Min(page, totalPages)); + SortAndPaginateProperties(); + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + var s when s == ApplicationConstants.PropertyStatuses.Available => "bg-success", + var s when s == ApplicationConstants.PropertyStatuses.ApplicationPending => "bg-info", + var s when s == ApplicationConstants.PropertyStatuses.LeasePending => "bg-warning", + var s when s == ApplicationConstants.PropertyStatuses.Occupied => "bg-danger", + var s when s == ApplicationConstants.PropertyStatuses.UnderRenovation => "bg-secondary", + var s when s == ApplicationConstants.PropertyStatuses.OffMarket => "bg-dark", + _ => "bg-secondary" + }; + } + + private string FormatPropertyStatus(string status) + { + return status switch + { + var s when s == ApplicationConstants.PropertyStatuses.ApplicationPending => "Application Pending", + var s when s == ApplicationConstants.PropertyStatuses.LeasePending => "Lease Pending", + var s when s == ApplicationConstants.PropertyStatuses.UnderRenovation => "Under Renovation", + var s when s == ApplicationConstants.PropertyStatuses.OffMarket => "Off Market", + _ => status + }; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor b/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor new file mode 100644 index 0000000..0d994bf --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor @@ -0,0 +1,626 @@ +@page "/propertymanagement/properties/view/{PropertyId:guid}" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization + +@inject PropertyService PropertyService +@inject LeaseService LeaseService +@inject MaintenanceService MaintenanceService +@inject InspectionService InspectionService +@inject Application.Services.DocumentService DocumentService +@inject ChecklistService ChecklistService +@inject NavigationManager NavigationManager +@inject IJSRuntime JSRuntime +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +@if (property == null) +{ +
+
+ Loading... +
+
+} +else if (!isAuthorized) +{ +
+

Access Denied

+

You don't have permission to view this property.

+ Back to Properties +
+} +else +{ +
+

Property Details

+
+ + +
+
+ +
+
+
+
+
Property Information
+ + @(property.IsAvailable ? "Available" : "Occupied") + +
+
+
+
+ Address: +

@property.Address @(!string.IsNullOrWhiteSpace(property.UnitNumber) ? $", {property.UnitNumber}" : "")

+ @property.City, @property.State @property.ZipCode +
+
+ +
+
+ Property Type: +

@property.PropertyType

+
+
+ Monthly Rent: +

@property.MonthlyRent.ToString("C")

+
+
+ +
+
+ Bedrooms: +

@property.Bedrooms

+
+
+ Bathrooms: +

@property.Bathrooms

+
+
+ Square Feet: +

@property.SquareFeet.ToString("N0")

+
+
+ + @if (!string.IsNullOrEmpty(property.Description)) + { +
+
+ Description: +

@property.Description

+
+
+ } + +
+
+ Created: +

@property.CreatedOn.ToString("MMMM dd, yyyy")

+
+ @if (property.LastModifiedOn.HasValue) + { +
+ Last Modified: +

@property.LastModifiedOn.Value.ToString("MMMM dd, yyyy")

+
+ } +
+
+
+ +
+
+
Maintenance Requests
+ +
+
+ @if (maintenanceRequests.Any()) + { +
+ @foreach (var request in maintenanceRequests.OrderByDescending(r => r.RequestedOn).Take(5)) + { +
+
+
+
+ @request.Title + @request.Priority + @request.Status + @if (request.IsOverdue) + { + + } +
+ @request.RequestType + + Requested: @request.RequestedOn.ToString("MMM dd, yyyy") + @if (request.ScheduledOn.HasValue) + { + | Scheduled: @request.ScheduledOn.Value.ToString("MMM dd, yyyy") + } + +
+ +
+
+ } +
+ @if (maintenanceRequests.Count > 5) + { +
+ Showing 5 of @maintenanceRequests.Count requests +
+ } +
+ +
+ } + else + { +
+ +

No maintenance requests for this property

+ +
+ } +
+
+ + + @if (propertyDocuments.Any()) + { +
+
+
Documents
+ @propertyDocuments.Count +
+
+
+ @foreach (var doc in propertyDocuments.OrderByDescending(d => d.CreatedOn)) + { +
+
+
+
+ + @doc.FileName +
+ @if (!string.IsNullOrEmpty(doc.Description)) + { + @doc.Description + } + + @doc.DocumentType + @doc.FileSizeFormatted | @doc.CreatedOn.ToString("MMM dd, yyyy") + +
+
+ + +
+
+
+ } +
+
+ +
+
+
+ } +
+ +
+
+
+
Quick Actions
+
+
+
+ + @if (property.IsAvailable) + { + + } + else + { + + } + + + +
+
+
+ + +
+
+
Routine Inspection
+
+
+ @if (property.LastRoutineInspectionDate.HasValue) + { +
+ Last Routine Inspection: +

@property.LastRoutineInspectionDate.Value.ToString("MMM dd, yyyy")

+ @if (propertyInspections.Any()) + { + var lastInspection = propertyInspections.Where(i => i.CompletedOn == property.LastRoutineInspectionDate.Value && i.InspectionType == "Routine").FirstOrDefault(); + + + View Last Routine Inspection + + + } +
+ } + + @if (property.NextRoutineInspectionDueDate.HasValue) + { +
+ Next Routine Inspection Due: +

@property.NextRoutineInspectionDueDate.Value.ToString("MMM dd, yyyy")

+
+ +
+ Status: +

+ + @property.InspectionStatus + +

+
+ + @if (property.IsInspectionOverdue) + { +
+ + + Overdue by @property.DaysOverdue days + +
+ } + else if (property.DaysUntilInspectionDue <= 30) + { +
+ + + Due in @property.DaysUntilInspectionDue days + +
+ } + } + else + { +
+ No inspection scheduled +
+ } + +
+ +
+
+
+ + @if (activeLeases.Any()) + { +
+
+
Active Leases
+
+
+ @foreach (var lease in activeLeases) + { +
+ @lease.Tenant?.FullName +
+ + @lease.StartDate.ToString("MMM dd, yyyy") - @lease.EndDate.ToString("MMM dd, yyyy") + +
+ @lease.MonthlyRent.ToString("C")/month +
+ } +
+
+ } + + +
+
+
Completed Checklists
+ +
+
+ @if (propertyChecklists.Any()) + { +
+ @foreach (var checklist in propertyChecklists.OrderByDescending(c => c.CompletedOn ?? c.CreatedOn).Take(5)) + { +
+
+
+
+ @checklist.Name + @checklist.Status +
+ @checklist.ChecklistType + + @if (checklist.CompletedOn.HasValue) + { + Completed: @checklist.CompletedOn.Value.ToString("MMM dd, yyyy") + } + else + { + Created: @checklist.CreatedOn.ToString("MMM dd, yyyy") + } + +
+
+ + @if (checklist.Status != ApplicationConstants.ChecklistStatuses.Completed) + { + + } +
+
+
+ } +
+ @if (propertyChecklists.Count > 5) + { +
+ Showing 5 of @propertyChecklists.Count checklists +
+ } + } + else + { +
+ +

No checklists for this property

+ +
+ } +
+
+ + + + +
+
+} +@code { + [Parameter] + public Guid PropertyId { get; set; } + + public Guid LeaseId { get; set; } + + List activeLeases = new(); + List propertyDocuments = new(); + List maintenanceRequests = new(); + List propertyInspections = new(); + List propertyChecklists = new(); + + private bool isAuthorized = true; + + private Property? property; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadProperty(); + } + + private async Task LoadProperty() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + isAuthorized = false; + return; + } + property = await PropertyService.GetByIdAsync(PropertyId); + if (property == null) + { + isAuthorized = false; + return; + } + activeLeases = await LeaseService.GetActiveLeasesByPropertyIdAsync(PropertyId); + + Lease? lease = activeLeases.FirstOrDefault(); + if (lease != null) + { + LeaseId = lease.Id; + } + + // Load documents for this property + propertyDocuments = await DocumentService.GetDocumentsByPropertyIdAsync(PropertyId); + propertyDocuments = propertyDocuments + .Where(d => !d.IsDeleted) + .ToList(); + + // Load maintenance requests for this property + maintenanceRequests = await MaintenanceService.GetMaintenanceRequestsByPropertyAsync(PropertyId); + // Load inspections for this property + propertyInspections = await InspectionService.GetByPropertyIdAsync(PropertyId); + + // Load checklists for this property + var allChecklists = await ChecklistService.GetChecklistsAsync(includeArchived: false); + propertyChecklists = allChecklists + .Where(c => c.PropertyId == PropertyId) + .OrderByDescending(c => c.CompletedOn ?? c.CreatedOn) + .ToList(); + } + + private void EditProperty() + { + NavigationManager.NavigateTo($"/propertymanagement/properties/edit/{PropertyId}"); + } + + private void CreateLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/create/?propertyid={PropertyId}"); + } + + private void ViewLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/view/{LeaseId}"); + } + + private void ViewDocuments() + { + NavigationManager.NavigateTo($"/propertymanagement/documents/?propertyid={PropertyId}"); + } + + private void CreateInspection() + { + NavigationManager.NavigateTo($"/propertymanagement/inspections/create/{PropertyId}"); + } + + private void CreateMaintenanceRequest() + { + NavigationManager.NavigateTo($"/propertymanagement/maintenance/create?PropertyId={PropertyId}"); + } + + private void ViewMaintenanceRequest(Guid requestId) + { + NavigationManager.NavigateTo($"/propertymanagement/maintenance/view/{requestId}"); + } + + private void ViewAllMaintenanceRequests() + { + NavigationManager.NavigateTo($"/propertymanagement/maintenance?propertyId={PropertyId}"); + } + + private void BackToList() + { + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + + private async Task ViewDocument(Document doc) + { + var base64Data = Convert.ToBase64String(doc.FileData); + await JSRuntime.InvokeVoidAsync("viewFile", base64Data, doc.FileType); + } + + private async Task DownloadDocument(Document doc) + { + var fileName = doc.FileName; + var fileData = doc.FileData; + var mimeType = doc.FileType; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(fileData), mimeType); + } + + private string GetFileIcon(string extension) + { + return extension.ToLower() switch + { + ".pdf" => "bi-file-pdf text-danger", + ".doc" or ".docx" => "bi-file-word text-primary", + ".jpg" or ".jpeg" or ".png" => "bi-file-image text-success", + ".txt" => "bi-file-text", + _ => "bi-file-earmark" + }; + } + + private string GetDocumentTypeBadge(string documentType) + { + return documentType switch + { + "Lease Agreement" => "bg-primary", + "Invoice" => "bg-warning", + "Payment Receipt" => "bg-success", + "Inspection Report" => "bg-info", + "Addendum" => "bg-secondary", + _ => "bg-secondary" + }; + } + + private string GetInspectionStatusBadge(string status) + { + return status switch + { + "Overdue" => "bg-danger", + "Due Soon" => "bg-warning", + "Scheduled" => "bg-success", + "Not Scheduled" => "bg-secondary", + _ => "bg-secondary" + }; + } + + private string GetChecklistStatusBadge(string status) + { + return status switch + { + "Completed" => "bg-success", + "In Progress" => "bg-warning", + "Draft" => "bg-secondary", + _ => "bg-secondary" + }; + } + + private void CreateChecklist() + { + NavigationManager.NavigateTo("/propertymanagement/checklists"); + } + + private void ViewChecklist(Guid checklistId) + { + NavigationManager.NavigateTo($"/propertymanagement/checklists/view/{checklistId}"); + } + + private void CompleteChecklist(Guid checklistId) + { + NavigationManager.NavigateTo($"/propertymanagement/checklists/complete/{checklistId}"); + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor new file mode 100644 index 0000000..751eb7b --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor @@ -0,0 +1,240 @@ +@page "/reports/income-statement" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject FinancialReportService FinancialReportService +@inject PropertyService PropertyService +@inject FinancialReportPdfGenerator PdfGenerator +@inject AuthenticationStateProvider AuthenticationStateProvider + +@inject UserContextService UserContextService + +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +Income Statement - Aquiis + +
+
+
+

Income Statement

+

View income and expenses for a specific period

+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+

Generating report...

+
+ } + else if (statement != null) + { +
+
+
+ @if (statement.PropertyId.HasValue) + { + @statement.PropertyName + } + else + { + All Properties + } + - Income Statement +
+ +
+
+
+
+ Period: @statement.StartDate.ToString("MMM dd, yyyy") - @statement.EndDate.ToString("MMM dd, yyyy") +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryAmount
INCOME
Rent Income@statement.TotalRentIncome.ToString("C")
Other Income@statement.TotalOtherIncome.ToString("C")
Total Income@statement.TotalIncome.ToString("C")
EXPENSES
Maintenance & Repairs@statement.MaintenanceExpenses.ToString("C")
Utilities@statement.UtilityExpenses.ToString("C")
Insurance@statement.InsuranceExpenses.ToString("C")
Property Taxes@statement.TaxExpenses.ToString("C")
Management Fees@statement.ManagementFees.ToString("C")
Other Expenses@statement.OtherExpenses.ToString("C")
Total Expenses@statement.TotalExpenses.ToString("C")
NET INCOME@statement.NetIncome.ToString("C")
Profit Margin@statement.ProfitMargin.ToString("F2")%
+
+
+ } +
+ +@code { + private DateTime startDate = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + private DateTime endDate = DateTime.Now; + private Guid? selectedPropertyId; + private List properties = new(); + private IncomeStatement? statement; + private bool isLoading = false; + + private Guid? organizationId = Guid.Empty; + + [CascadingParameter] + private Task? AuthenticationStateTask { get; set; } + + protected override async Task OnInitializedAsync() + { + if (AuthenticationStateTask == null) return; + + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + organizationId = await UserContextService.GetActiveOrganizationIdAsync(); + + if (organizationId.HasValue) + { + properties = await PropertyService.GetAllAsync(); + } + } + + private async Task GenerateReport() + { + if (AuthenticationStateTask == null) return; + + isLoading = true; + StateHasChanged(); + + try + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (organizationId.HasValue) + { + @* Guid? propertyId = null; + if (selectedPropertyId.HasValue && Guid.TryParse(selectedPropertyId, out Guid pid)) + { + propertyId = pid; + } *@ + + statement = await FinancialReportService.GenerateIncomeStatementAsync( + organizationId.Value, startDate, endDate, selectedPropertyId); + } + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + + private async Task ExportToPdf() + { + if (statement == null) return; + + try + { + var pdfBytes = PdfGenerator.GenerateIncomeStatementPdf(statement); + var fileName = $"IncomeStatement_{statement.StartDate:yyyyMMdd}_{statement.EndDate:yyyyMMdd}.pdf"; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(pdfBytes), "application/pdf"); + } + catch (Exception ex) + { + // Handle error + Console.WriteLine($"Error generating PDF: {ex.Message}"); + } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor new file mode 100644 index 0000000..d197ff8 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor @@ -0,0 +1,259 @@ +@page "/reports/property-performance" +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject FinancialReportService FinancialReportService +@inject FinancialReportPdfGenerator PdfGenerator +@inject AuthenticationStateProvider AuthenticationStateProvider + +@inject UserContextService UserContextService + +@inject ToastService ToastService +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +Property Performance - Aquiis + +
+
+
+

Property Performance Report

+

Compare income, expenses, and ROI across all properties

+
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+

Generating report...

+
+ } + else if (performanceItems.Any()) + { +
+
+
Property Performance: @startDate.ToString("MMM dd, yyyy") - @endDate.ToString("MMM dd, yyyy")
+ +
+
+
+ + + + + + + + + + + + + + @foreach (var item in performanceItems) + { + + + + + + + + + + } + + + + + + + + + + + +
PropertyAddressTotal IncomeTotal ExpensesNet IncomeROI %Occupancy Rate
@item.PropertyName@item.PropertyAddress@item.TotalIncome.ToString("C")@item.TotalExpenses.ToString("C") + + @item.NetIncome.ToString("C") + + + + @item.ROI.ToString("F2")% + + +
+
+
+
+
+ @item.OccupancyRate.ToString("F1")% +
+
TOTALS@performanceItems?.Sum(p => p.TotalIncome).ToString("C")@performanceItems?.Sum(p => p.TotalExpenses).ToString("C") + + @performanceItems?.Sum(p => p.NetIncome).ToString("C") + + + @{ + var avgROI = performanceItems?.Any() == true ? performanceItems.Average(p => p.ROI) : 0; + } + @avgROI.ToString("F2")% + + @{ + var avgOccupancy = performanceItems?.Any() == true ? performanceItems.Average(p => p.OccupancyRate) : 0; + } + @avgOccupancy.ToString("F1")% +
+
+
+
+ +
+
+
+
+
Top Performing Properties (by Net Income)
+
+
+
    + @if (performanceItems != null) + { + @foreach (var property in performanceItems.OrderByDescending(p => p.NetIncome).Take(5)) + { +
  1. + @property.PropertyName - + @property.NetIncome.ToString("C") + (@property.ROI.ToString("F2")% ROI) +
  2. + } + } +
+
+
+
+
+
+
+
Highest Occupancy
+
+
+
    + @if (performanceItems != null) + { + @foreach (var property in performanceItems.OrderByDescending(p => p.OccupancyRate).Take(5)) + { +
  1. + @property.PropertyName - + @property.OccupancyRate.ToString("F1")% + (@property.OccupancyDays of @property.TotalDays days) +
  2. + } + } +
+
+
+
+
+ } +
+ +@code { + private DateTime startDate = new DateTime(DateTime.Now.Year, 1, 1); + private DateTime endDate = DateTime.Now; + private List performanceItems = new(); + private bool isLoading = false; + + [CascadingParameter] + private Task? AuthenticationStateTask { get; set; } + + private async Task GenerateReport() + { + if (AuthenticationStateTask == null) return; + + isLoading = true; + StateHasChanged(); + + try + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + var organizationId = await UserContextService.GetActiveOrganizationIdAsync(); + + if (organizationId.HasValue) + { + performanceItems = await FinancialReportService.GeneratePropertyPerformanceAsync( + organizationId.Value, startDate, endDate); + } + else { + ToastService.ShowError("Unable to determine active organization."); + } + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + + private string GetROIClass(decimal roi) + { + if (roi >= 10) return "text-success fw-bold"; + if (roi >= 5) return "text-success"; + if (roi >= 0) return "text-warning"; + return "text-danger"; + } + + private string GetOccupancyClass(decimal rate) + { + if (rate >= 90) return "bg-success"; + if (rate >= 70) return "bg-info"; + if (rate >= 50) return "bg-warning"; + return "bg-danger"; + } + + private async Task ExportToPdf() + { + if (!performanceItems.Any()) return; + + try + { + var pdfBytes = PdfGenerator.GeneratePropertyPerformancePdf(performanceItems, startDate, endDate); + var fileName = $"PropertyPerformance_{startDate:yyyyMMdd}_{endDate:yyyyMMdd}.pdf"; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(pdfBytes), "application/pdf"); + } + catch (Exception ex) + { + Console.WriteLine($"Error generating PDF: {ex.Message}"); + } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/RentRollReport.razor b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/RentRollReport.razor new file mode 100644 index 0000000..15e5e7a --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/RentRollReport.razor @@ -0,0 +1,243 @@ +@page "/reports/rentroll" +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject FinancialReportService FinancialReportService +@inject FinancialReportPdfGenerator PdfGenerator +@inject AuthenticationStateProvider AuthenticationStateProvider + +@inject UserContextService UserContextService + +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +Rent Roll - Aquiis + +
+
+
+

Rent Roll Report

+

Current tenant and rent status across all properties

+
+
+ +
+
+ + +
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+

Generating report...

+
+ } + else if (rentRollItems.Any()) + { +
+
+
Rent Roll as of @asOfDate.ToString("MMM dd, yyyy")
+ +
+
+
+ + + + + + + + + + + + + + + + + + @foreach (var item in rentRollItems) + { + + + + + + + + + + + + + + } + + + + + + + + + + + + +
PropertyAddressTenantLease StatusLease PeriodMonthly RentSecurity DepositTotal PaidTotal DueBalanceStatus
@item.PropertyName@item.PropertyAddress@item.TenantName + + @item.LeaseStatus + + + @if (item.LeaseStartDate.HasValue) + { + @item.LeaseStartDate.Value.ToString("MM/dd/yyyy") + } + @if (item.LeaseEndDate.HasValue) + { + - @item.LeaseEndDate.Value.ToString("MM/dd/yyyy") + } + @item.MonthlyRent.ToString("C")@item.SecurityDeposit.ToString("C")@item.TotalPaid.ToString("C")@item.TotalDue.ToString("C") + + @item.Balance.ToString("C") + + + + @item.PaymentStatus + +
TOTALS@rentRollItems.Sum(r => r.MonthlyRent).ToString("C")@rentRollItems.Sum(r => r.SecurityDeposit).ToString("C")@rentRollItems.Sum(r => r.TotalPaid).ToString("C")@rentRollItems.Sum(r => r.TotalDue).ToString("C") + + @rentRollItems.Sum(r => r.Balance).ToString("C") + +
+
+
+
+ +
+
+
+
+
Total Properties
+

@rentRollItems.Select(r => r.PropertyId).Distinct().Count()

+
+
+
+
+
+
+
Active Leases
+

@rentRollItems.Count(r => r.LeaseStatus == "Active")

+
+
+
+
+
+
+
Monthly Revenue
+

@rentRollItems.Sum(r => r.MonthlyRent).ToString("C")

+
+
+
+
+
+
+
Outstanding Balance
+

+ @rentRollItems.Sum(r => r.Balance).ToString("C") +

+
+
+
+
+ } +
+ +@code { + private DateTime asOfDate = DateTime.Now; + private List rentRollItems = new(); + private bool isLoading = false; + + [CascadingParameter] + private Task? AuthenticationStateTask { get; set; } + + private Guid? organizationId = Guid.Empty; + + private async Task GenerateReport() + { + if (AuthenticationStateTask == null) return; + + isLoading = true; + StateHasChanged(); + + try + { + organizationId = await UserContextService.GetActiveOrganizationIdAsync(); + + if (organizationId.HasValue) + { + rentRollItems = await FinancialReportService.GenerateRentRollAsync(organizationId.Value, asOfDate); + } + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + + private string GetLeaseStatusClass(string status) + { + return status?.ToLower() switch + { + "active" => "bg-success", + "expired" => "bg-danger", + "pending" => "bg-warning", + _ => "bg-secondary" + }; + } + + private string GetPaymentStatusClass(string status) + { + return status?.ToLower() switch + { + "current" => "bg-success", + "outstanding" => "bg-danger", + _ => "bg-secondary" + }; + } + + private async Task ExportToPdf() + { + if (!rentRollItems.Any()) return; + + try + { + var pdfBytes = PdfGenerator.GenerateRentRollPdf(rentRollItems, asOfDate); + var fileName = $"RentRoll_{asOfDate:yyyyMMdd}.pdf"; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(pdfBytes), "application/pdf"); + } + catch (Exception ex) + { + Console.WriteLine($"Error generating PDF: {ex.Message}"); + } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/Reports.razor b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/Reports.razor new file mode 100644 index 0000000..e653420 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/Reports.razor @@ -0,0 +1,278 @@ +@page "/reports" +@using Microsoft.AspNetCore.Authorization +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants + +@inject ApplicationService ApplicationService +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + + +Financial Reports - Aquiis + + +
+

Daily Payment Report

+ +
+ +@if (isLoading) +{ +
+
+ Loading... +
+
+} +else +{ +
+
+
+
+
Today's Total
+

$@todayTotal.ToString("N2")

+ @DateTime.Today.ToString("MMM dd, yyyy") +
+
+
+
+
+
+
This Week
+

$@weekTotal.ToString("N2")

+ Last 7 days +
+
+
+
+
+
+
This Month
+

$@monthTotal.ToString("N2")

+ @DateTime.Today.ToString("MMM yyyy") +
+
+
+
+
+
+
Expiring Leases
+

@expiringLeases

+ Next 30 days +
+
+
+
+ + @if (statistics != null) + { +
+
+
Payment Statistics
+
+
+
+
+

Period: @statistics.StartDate.ToString("MMM dd, yyyy") - @statistics.EndDate.ToString("MMM dd, yyyy")

+

Total Payments: @statistics.PaymentCount

+

Average Payment: $@statistics.AveragePayment.ToString("N2")

+
+
+
Payment Methods
+ @if (statistics.PaymentsByMethod.Any()) + { +
    + @foreach (var method in statistics.PaymentsByMethod) + { +
  • + @method.Key: $@method.Value.ToString("N2") +
  • + } +
+ } + else + { +

No payment methods recorded

+ } +
+
+
+
+ } +} + + +
+
+
+

Financial Reports

+

Generate comprehensive financial reports for your properties

+
+
+ +
+
+
+
+
+ +
+
Income Statement
+

+ View income and expenses for a specific period with detailed breakdowns +

+ + Generate + +
+
+
+ +
+
+
+
+ +
+
Rent Roll
+

+ Current tenant status, rent amounts, and payment details across all properties +

+ + Generate + +
+
+
+ +
+
+
+
+ +
+
Property Performance
+

+ Compare income, expenses, ROI, and occupancy rates across all properties +

+ + Generate + +
+
+
+ +
+
+
+
+ +
+
Tax Report
+

+ Schedule E data for tax filing with detailed expense categorization +

+ + Generate + +
+
+
+
+ +
+
+
+
+
Report Features
+
+
+
+
+
Available Features
+
    +
  • Customizable date ranges
  • +
  • Property-specific or portfolio-wide reports
  • +
  • Export to PDF for record keeping
  • +
  • Real-time data from your database
  • +
  • Professional formatting for tax purposes
  • +
  • Detailed expense categorization
  • +
+
+
+
Tips
+
    +
  • Generate reports regularly for better tracking
  • +
  • Use income statements for monthly reviews
  • +
  • Rent roll helps identify payment issues
  • +
  • Property performance guides investment decisions
  • +
  • Tax reports simplify year-end filing
  • +
  • Keep PDF copies for audit trail
  • +
+
+
+
+
+
+
+
+ + + +@code { + private bool isLoading = true; + private decimal todayTotal = 0; + private decimal weekTotal = 0; + private decimal monthTotal = 0; + private int expiringLeases = 0; + private PaymentStatistics? statistics; + + protected override async Task OnInitializedAsync() + { + await LoadReport(); + } + + private async Task LoadReport() + { + isLoading = true; + try + { + var today = DateTime.Today; + var weekStart = today.AddDays(-7); + var monthStart = new DateTime(today.Year, today.Month, 1); + + // Get payment totals + todayTotal = await ApplicationService.GetTodayPaymentTotalAsync(); + weekTotal = await ApplicationService.GetPaymentTotalForRangeAsync(weekStart, today); + monthTotal = await ApplicationService.GetPaymentTotalForRangeAsync(monthStart, today); + + // Get expiring leases count + expiringLeases = await ApplicationService.GetLeasesExpiringCountAsync(30); + + // Get detailed statistics for this month + statistics = await ApplicationService.GetPaymentStatisticsAsync(monthStart, today); + } + finally + { + isLoading = false; + } + } + + private async Task RefreshReport() + { + await LoadReport(); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/TaxReport.razor b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/TaxReport.razor new file mode 100644 index 0000000..d2118c2 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/TaxReport.razor @@ -0,0 +1,287 @@ +@page "/reports/tax-report" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject FinancialReportService FinancialReportService +@inject PropertyService PropertyService +@inject FinancialReportPdfGenerator PdfGenerator +@inject AuthenticationStateProvider AuthenticationStateProvider + +@inject UserContextService UserContextService +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +Tax Report - Aquiis + +
+
+
+

Tax Report (Schedule E)

+

IRS Schedule E - Supplemental Income and Loss from rental real estate

+
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+

Generating report...

+
+ } + else if (taxReports.Any()) + { +
+ + Note: This report provides estimated tax information for Schedule E. + Please consult with a tax professional for accurate filing. Depreciation is calculated using simplified residential rental property method (27.5 years). +
+ + @foreach (var report in taxReports) + { +
+
+
@report.PropertyName - Tax Year @report.Year
+ +
+
+
+
+
INCOME
+ + + + + +
3. Rents received@report.TotalRentIncome.ToString("C")
+
+
+ +
+
+
EXPENSES
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
5. Advertising@report.Advertising.ToString("C")
6. Auto and travel@report.Auto.ToString("C")
7. Cleaning and maintenance@report.Cleaning.ToString("C")
9. Insurance@report.Insurance.ToString("C")
11. Legal and other professional fees@report.Legal.ToString("C")
12. Management fees@report.Management.ToString("C")
13. Mortgage interest paid to banks, etc.@report.MortgageInterest.ToString("C")
14. Repairs@report.Repairs.ToString("C")
15. Supplies@report.Supplies.ToString("C")
16. Taxes@report.Taxes.ToString("C")
17. Utilities@report.Utilities.ToString("C")
18. Depreciation expense@report.DepreciationAmount.ToString("C")
19. Other (specify)@report.Other.ToString("C")
20. Total expenses@report.TotalExpenses.ToString("C")
+
+
+ +
+
+
SUMMARY
+ + + + + + + + + + + + + +
Total Income@report.TotalRentIncome.ToString("C")
Total Expenses (including depreciation)@((report.TotalExpenses + report.DepreciationAmount).ToString("C"))
21. Net rental income or (loss) + + @report.TaxableIncome.ToString("C") + +
+
+
+
+
+ } + + @if (taxReports.Count > 1) + { +
+
+
All Properties Summary - Tax Year @taxYear
+
+
+ + + + + + + + + + + + + + + + + +
Total Rental Income (All Properties)@taxReports.Sum(r => r.TotalRentIncome).ToString("C")
Total Expenses (All Properties)@taxReports.Sum(r => r.TotalExpenses).ToString("C")
Total Depreciation@taxReports.Sum(r => r.DepreciationAmount).ToString("C")
Net Rental Income or (Loss) + + @taxReports.Sum(r => r.TaxableIncome).ToString("C") + +
+
+
+ } + } +
+ +@code { + private int taxYear = DateTime.Now.Month >= 11 ? DateTime.Now.Year : DateTime.Now.Year - 1; + private Guid? selectedPropertyId; + private List properties = new(); + private List taxReports = new(); + private bool isLoading = false; + + private Guid? organizationId = Guid.Empty; + + [CascadingParameter] + private Task? AuthenticationStateTask { get; set; } + + protected override async Task OnInitializedAsync() + { + if (AuthenticationStateTask == null) return; + + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + organizationId = await UserContextService.GetActiveOrganizationIdAsync(); + + if (!string.IsNullOrEmpty(userId)) + { + properties = await PropertyService.GetAllAsync(); + } + } + + private async Task GenerateReport() + { + if (AuthenticationStateTask == null) return; + + isLoading = true; + StateHasChanged(); + + try + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (!string.IsNullOrEmpty(userId) && organizationId.HasValue) + { + + taxReports = await FinancialReportService.GenerateTaxReportAsync(organizationId.Value, taxYear, selectedPropertyId); + } + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + + private async Task ExportToPdf(TaxReportData report) + { + if (!taxReports.Any()) return; + + try + { + // Export single property or all + var reportsToExport = report != null ? new List { report } : taxReports; + var pdfBytes = PdfGenerator.GenerateTaxReportPdf(reportsToExport); + var fileName = report != null + ? $"TaxReport_{report.Year}_{report.PropertyName?.Replace(" ", "_")}.pdf" + : $"TaxReport_{taxYear}_AllProperties.pdf"; + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, Convert.ToBase64String(pdfBytes), "application/pdf"); + } + catch (Exception ex) + { + Console.WriteLine($"Error generating PDF: {ex.Message}"); + } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Components/SampleFeatureCompnentOne.razor b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Components/SampleFeatureCompnentOne.razor new file mode 100644 index 0000000..e69de29 diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Create.razor b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Create.razor new file mode 100644 index 0000000..e69de29 diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Edit.razor b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Edit.razor new file mode 100644 index 0000000..e69de29 diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Index.razor b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Index.razor new file mode 100644 index 0000000..e69de29 diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/View.razor b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/View.razor new file mode 100644 index 0000000..e69de29 diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs new file mode 100644 index 0000000..16ab919 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore; +public class SampleFeatureContext : DbContext +{ + // This class can be used to hold context-specific information + public SampleFeatureContext(DbContextOptions options) + : base(options) + { + } + +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs new file mode 100644 index 0000000..524121f --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs @@ -0,0 +1,25 @@ + +public abstract class SampleFeatureEntity : BaseEntity, ISampleFeatureEntity +{ + public string Name { get; set; } = string.Empty; +} + +public interface ISampleFeatureEntity +{ + Guid Id { get; set; } + string Name { get; set; } + + +} + +public abstract class BaseEntity +{ + // Base properties and methods for all entities can be defined here + public Guid Id { get; set; } + + public string CreatedBy { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public string UpdatedBy { get; set; } = string.Empty; + public DateTime? UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs new file mode 100644 index 0000000..4054c33 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs @@ -0,0 +1,20 @@ + +using Microsoft.EntityFrameworkCore; + +public class SampleFeatureService +{ + private readonly SampleFeatureContext _context; + + public SampleFeatureService(SampleFeatureContext context) + { + _context = context; + } + + public async Task GetSampleFeatureByIdAsync(Guid id) + { + return await _context.Set() + .FirstOrDefaultAsync(e => e.Id == id); + } + + // Sample methods for the service can be added here +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor new file mode 100644 index 0000000..4d4a7c4 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor @@ -0,0 +1,337 @@ +@page "/property-management/security-deposits/calculate-dividends/{PoolId:guid}" +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@inject SecurityDepositService SecurityDepositService +@inject NavigationManager NavigationManager +@inject ToastService ToastService +@rendermode InteractiveServer +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + + +Calculate Dividends - @(pool?.Year ?? 0) + +
+ @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (pool == null) + { +
+ + Investment pool not found. +
+ } + else + { +
+
+ +

Calculate Dividends for @pool.Year

+

Review and confirm dividend calculations for all active leases

+
+
+ + @if (pool.Status != ApplicationConstants.InvestmentPoolStatuses.Open) + { +
+ + Dividends Already Calculated +

Dividends for this pool have already been calculated. View the details on the pool details page.

+
+ } + else if (!pool.HasEarnings) + { +
+ + No Dividends to Distribute +

+ @if (pool.HasLosses) + { + This pool had losses of @pool.AbsorbedLosses.ToString("C2"), which are absorbed by the organization. No dividends will be distributed. + } + else + { + This pool had no earnings. No dividends will be distributed. + } +

+
+ } + else + { + +
+
+
+
+
Total Earnings
+

@pool.TotalEarnings.ToString("C2")

+
+
+
+
+
+
+
Organization Share
+

@pool.OrganizationShare.ToString("C2")

+ @((pool.OrganizationSharePercentage * 100).ToString("F0"))% +
+
+
+
+
+
+
Tenant Share Total
+

@pool.TenantShareTotal.ToString("C2")

+
+
+
+
+
+
+
Active Leases
+

@pool.ActiveLeaseCount

+ @pool.DividendPerLease.ToString("C2") each +
+
+
+
+ + @if (calculationPreview.Any()) + { +
+
+
+ Dividend Calculation Preview +
+
+
+
+ + + + + + + + + + + + + + @foreach (var calc in calculationPreview.OrderByDescending(c => c.FinalDividend)) + { + + + + + + + + + + } + + + + + + + +
TenantLease IDLease PeriodMonths in PoolBase DividendProrationFinal Dividend
Tenant #@calc.TenantIdLease #@calc.LeaseId + + @calc.LeaseStartDate.ToString("MMM d, yyyy")
+ to @(calc.LeaseEndDate?.ToString("MMM d, yyyy") ?? "Present") +
+
+ @calc.MonthsInPool + @calc.BaseDividend.ToString("C2") + @if (calc.ProrationFactor < 1.0m) + { + + @((calc.ProrationFactor * 100).ToString("F0"))% + + } + else + { + 100% + } + + @calc.FinalDividend.ToString("C2") +
Total Dividends to Distribute: + @calculationPreview.Sum(c => c.FinalDividend).ToString("C2") +
+
+
+
+ +
+
+
Confirm Dividend Calculation
+

+ Review the dividend calculations above. Once confirmed, dividends will be created for each tenant + and tenants can choose to receive their dividend as a lease credit or check. +

+ +
+
What happens next?
+
    +
  • Dividend records will be created for all @calculationPreview.Count active leases
  • +
  • Tenants will be notified to choose their dividend payment method
  • +
  • You can process dividend payments from the pool details page
  • +
  • The pool status will change to "Calculated"
  • +
+
+ +
+ + +
+
+
+ } + else + { +
+ + No Active Leases Found +

There are no active leases in the pool for @pool.Year. Cannot calculate dividends.

+
+ } + } + } +
+ +@code { + [Parameter] + public Guid PoolId { get; set; } + + private SecurityDepositInvestmentPool? pool; + private List calculationPreview = new(); + private bool isLoading = true; + private bool isCalculating = false; + + protected override async Task OnInitializedAsync() + { + await LoadPoolAndPreview(); + } + + private async Task LoadPoolAndPreview() + { + isLoading = true; + try + { + pool = await SecurityDepositService.GetInvestmentPoolByIdAsync(PoolId); + + if (pool != null && pool.HasEarnings && pool.Status == ApplicationConstants.InvestmentPoolStatuses.Open) + { + // Get all security deposits in the pool for this year + var deposits = await SecurityDepositService.GetSecurityDepositsInPoolAsync(pool.Year); + + foreach (var deposit in deposits) + { + // Calculate proration based on months in pool + var leaseStart = deposit.PoolEntryDate ?? deposit.DateReceived; + var yearStart = new DateTime(pool.Year, 1, 1); + var yearEnd = new DateTime(pool.Year, 12, 31); + + var effectiveStart = leaseStart > yearStart ? leaseStart : yearStart; + var effectiveEnd = deposit.PoolExitDate.HasValue && deposit.PoolExitDate.Value < yearEnd + ? deposit.PoolExitDate.Value + : yearEnd; + + var monthsInPool = ((effectiveEnd.Year - effectiveStart.Year) * 12 + effectiveEnd.Month - effectiveStart.Month + 1); + var prorationFactor = monthsInPool / 12.0m; + + calculationPreview.Add(new DividendCalculation + { + TenantId = deposit.TenantId, + LeaseId = deposit.LeaseId, + LeaseStartDate = leaseStart, + LeaseEndDate = deposit.PoolExitDate, + MonthsInPool = monthsInPool, + BaseDividend = pool.DividendPerLease, + ProrationFactor = prorationFactor, + FinalDividend = pool.DividendPerLease * prorationFactor + }); + } + } + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to load dividend preview: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task ConfirmCalculation() + { + isCalculating = true; + try + { + await SecurityDepositService.CalculateDividendsAsync(pool!.Year); + + ToastService.ShowSuccess($"Dividends calculated for {pool.Year}. Tenants can now choose their payment method."); + NavigationManager.NavigateTo($"/property-management/security-deposits/investment-pool/{PoolId}"); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to calculate dividends: {ex.Message}"); + } + finally + { + isCalculating = false; + } + } + + private void Cancel() + { + NavigationManager.NavigateTo($"/property-management/security-deposits/investment-pool/{PoolId}"); + } + + private class DividendCalculation + { + public Guid TenantId { get; set; } + public Guid LeaseId { get; set; } + public DateTime LeaseStartDate { get; set; } + public DateTime? LeaseEndDate { get; set; } + public int MonthsInPool { get; set; } + public decimal BaseDividend { get; set; } + public decimal ProrationFactor { get; set; } + public decimal FinalDividend { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor new file mode 100644 index 0000000..c33f310 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor @@ -0,0 +1,345 @@ +@page "/property-management/security-deposits/investment-pools" +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@inject SecurityDepositService SecurityDepositService +@inject NavigationManager NavigationManager +@inject ToastService ToastService +@rendermode InteractiveServer +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + + +Investment Pools - Security Deposits + +
+
+
+

Security Deposit Investment Pools

+

Manage annual investment performance and dividend distributions

+
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+

Loading investment pools...

+
+ } + else if (investmentPools == null || !investmentPools.Any()) + { +
+ + No Investment Pools Found +

No annual investment performance has been recorded yet. Click "Record Performance" to add the first year's investment results.

+
+ } + else + { +
+
+
+ + + + + + + + + + + + + + + + + + + @foreach (var pool in investmentPools.OrderByDescending(p => p.Year)) + { + var poolStats = GetPoolStats(pool.Year); + + + + + + + + + + + + + + + } + + + + + + + + + + + + + + + + +
YearStarting BalanceDepositsWithdrawalsCurrent BalanceTotal EarningsReturn RateOrganization ShareTenant ShareActive LeasesStatusActions
+ @pool.Year + + @pool.StartingBalance.ToString("C2") + + + @poolStats.Deposits.ToString("C2") + + + @poolStats.Withdrawals.ToString("C2") + + @poolStats.CurrentBalance.ToString("C2") + + @if (pool.HasEarnings) + { + + + @pool.TotalEarnings.ToString("C2") + + } + else if (pool.HasLosses) + { + + + @pool.TotalEarnings.ToString("C2") + + } + else + { + $0.00 + } + + @if (pool.ReturnRate >= 0) + { + @((pool.ReturnRate * 100).ToString("F2"))% + } + else + { + @((pool.ReturnRate * 100).ToString("F2"))% + } + + @pool.OrganizationShare.ToString("C2") + (@((pool.OrganizationSharePercentage * 100).ToString("F0"))%) + + @pool.TenantShareTotal.ToString("C2") + + @pool.ActiveLeaseCount + + @if (pool.Status == ApplicationConstants.InvestmentPoolStatuses.Open) + { + Open + } + else if (pool.Status == ApplicationConstants.InvestmentPoolStatuses.Calculated) + { + Calculated + } + else if (pool.Status == ApplicationConstants.InvestmentPoolStatuses.Distributed) + { + Distributed + } + else if (pool.Status == ApplicationConstants.InvestmentPoolStatuses.Closed) + { + Closed + } + +
+ + @if (pool.Status == ApplicationConstants.InvestmentPoolStatuses.Open) + { + + } + @if (pool.Status != ApplicationConstants.InvestmentPoolStatuses.Closed) + { + + } +
+
Totals@investmentPools.Sum(p => p.StartingBalance).ToString("C2")@allPoolStats.Sum(s => s.Deposits).ToString("C2")@allPoolStats.Sum(s => s.Withdrawals).ToString("C2")@allPoolStats.Sum(s => s.CurrentBalance).ToString("C2") + @if (investmentPools.Sum(p => p.TotalEarnings) >= 0) + { + @investmentPools.Sum(p => p.TotalEarnings).ToString("C2") + } + else + { + @investmentPools.Sum(p => p.TotalEarnings).ToString("C2") + } + + @{ + var avgReturn = investmentPools.Any() ? investmentPools.Average(p => p.ReturnRate) : 0; + } + @((avgReturn * 100).ToString("F2"))% + (avg) + @investmentPools.Sum(p => p.OrganizationShare).ToString("C2")@investmentPools.Sum(p => p.TenantShareTotal).ToString("C2")@investmentPools.Sum(p => p.ActiveLeaseCount)
+
+
+
+ + @if (investmentPools.Any()) + { +
+
+
+
+
Total Investment Pool Value
+

@investmentPools.Sum(p => p.EndingBalance).ToString("C2")

+
+
+
+
+
+
+
Total Dividends Distributed
+

@investmentPools.Sum(p => p.TenantShareTotal).ToString("C2")

+
+
+
+
+
+
+
Organization Revenue
+

@investmentPools.Sum(p => p.OrganizationShare).ToString("C2")

+
+
+
+
+ } + } +
+ +@code { + private List investmentPools = new(); + private List allDeposits = new(); + private List allPoolStats = new(); + private bool isLoading = true; + + protected override async Task OnInitializedAsync() + { + await LoadInvestmentPools(); + } + + private async Task LoadInvestmentPools() + { + isLoading = true; + try + { + investmentPools = await SecurityDepositService.GetInvestmentPoolsAsync(); + allDeposits = await SecurityDepositService.GetSecurityDepositsAsync(); + + // Calculate stats for each pool year + allPoolStats = investmentPools.Select(p => GetPoolStats(p.Year)).ToList(); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to load investment pools: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private PoolStats GetPoolStats(int year) + { + var yearStart = new DateTime(year, 1, 1); + var yearEnd = new DateTime(year, 12, 31, 23, 59, 59); + + // Get the pool to access its starting balance + var pool = investmentPools.FirstOrDefault(p => p.Year == year); + var startingBalance = pool?.StartingBalance ?? 0; + + // Deposits added during the year + var deposits = allDeposits + .Where(d => d.PoolEntryDate.HasValue && + d.PoolEntryDate.Value >= yearStart && + d.PoolEntryDate.Value <= yearEnd) + .Sum(d => d.Amount); + + // Deposits removed during the year + var withdrawals = allDeposits + .Where(d => d.PoolExitDate.HasValue && + d.PoolExitDate.Value >= yearStart && + d.PoolExitDate.Value <= yearEnd) + .Sum(d => d.Amount); + + // Current balance = Starting + Deposits - Withdrawals + var currentBalance = startingBalance + deposits - withdrawals; + + return new PoolStats + { + Deposits = deposits, + Withdrawals = withdrawals, + CurrentBalance = currentBalance + }; + } + + private void CreateNewPool() + { + NavigationManager.NavigateTo("/property-management/security-deposits/record-performance"); + } + + private void ViewPoolDetails(Guid poolId) + { + NavigationManager.NavigateTo($"/property-management/security-deposits/investment-pool/{poolId}"); + } + + private void CalculateDividends(Guid poolId) + { + NavigationManager.NavigateTo($"/property-management/security-deposits/calculate-dividends/{poolId}"); + } + + private async Task ClosePool(Guid poolId, int year) + { + try + { + await SecurityDepositService.CloseInvestmentPoolAsync(poolId); + ToastService.ShowSuccess($"Investment pool for {year} has been closed"); + await LoadInvestmentPools(); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to close pool: {ex.Message}"); + } + } + + private class PoolStats + { + public decimal Deposits { get; set; } + public decimal Withdrawals { get; set; } + public decimal CurrentBalance { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor new file mode 100644 index 0000000..e19b5f0 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor @@ -0,0 +1,359 @@ +@page "/property-management/security-deposits/record-performance" +@page "/property-management/security-deposits/record-performance/{Year:int}" +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Authorization +@inject SecurityDepositService SecurityDepositService +@inject OrganizationService OrganizationService +@inject NavigationManager NavigationManager +@inject ToastService ToastService +@rendermode InteractiveServer +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + + +Record Investment Performance + +
+
+
+ +

Record Annual Investment Performance

+

Enter the investment earnings for the security deposit pool

+
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else + { +
+
+
+
+
Investment Performance Details
+
+
+ + + +
+
+ + + + @if (existingPool != null) + { + Performance already recorded for this year + } +
+
+ +
+ $ + +
+ Total of all deposits currently in pool +
+
+ +
+
+
+ Year-to-Date Summary:
+ Deposits in Pool: @depositsInPoolCount | + Total Balance: @currentPoolBalance.ToString("C2") +
+
+
+ +
+
+ +
+ $ + +
+ + Can be negative for losses (absorbed by organization) +
+
+ +
+ + % +
+ Calculated automatically +
+
+ +
+
+ + +
+
+ +
+ + @if (performanceModel.TotalEarnings > 0) + { +
+
+ Earnings Distribution Preview +
+
+
+ Organization Share (@((organizationSettings?.OrganizationSharePercentage * 100 ?? 20).ToString("F0"))%): +
@((performanceModel.TotalEarnings * (organizationSettings?.OrganizationSharePercentage ?? 0.20m)).ToString("C2"))
+
+
+ Tenant Share Total: +
@((performanceModel.TotalEarnings * (1 - (organizationSettings?.OrganizationSharePercentage ?? 0.20m))).ToString("C2"))
+
+
+
+ } + else if (performanceModel.TotalEarnings < 0) + { +
+
+ Loss Absorption Notice +
+

+ Investment losses of @(Math.Abs(performanceModel.TotalEarnings).ToString("C2")) will be absorbed by the organization. + No dividends will be distributed to tenants, and their security deposits remain unchanged. +

+
+ } + +
+ + +
+
+
+
+
+ +
+
+
+
+ Investment Pool Guidelines +
+
+
+
About Investment Performance
+
    +
  • Record the total investment earnings for the year
  • +
  • Earnings can be positive (gains) or negative (losses)
  • +
  • Organization share is @((organizationSettings?.OrganizationSharePercentage * 100 ?? 20).ToString("F0"))% of positive earnings
  • +
  • Losses are absorbed entirely by the organization
  • +
  • Tenants never see negative dividends
  • +
+ +
Next Steps
+
    +
  • After recording performance, calculate dividends
  • +
  • Dividends are distributed in @(GetMonthName(organizationSettings?.DividendDistributionMonth ?? 1))
  • +
  • Pro-rated for mid-year move-ins
  • +
  • Tenants choose lease credit or check
  • +
+ + @if (recentPools.Any()) + { +
Recent Performance
+
+ + + + + + + + + @foreach (var pool in recentPools.OrderByDescending(p => p.Year).Take(5)) + { + + + + + } + +
YearReturn
@pool.Year + @if (pool.ReturnRate >= 0) + { + @((pool.ReturnRate * 100).ToString("F2"))% + } + else + { + @((pool.ReturnRate * 100).ToString("F2"))% + } +
+
+ } +
+
+
+
+ } +
+ +@code { + [Parameter] + public int? Year { get; set; } + + private PerformanceModel performanceModel = new(); + private SecurityDepositInvestmentPool? existingPool; + private OrganizationSettings? organizationSettings; + private List recentPools = new(); + private bool isLoading = true; + private bool isSaving = false; + + // Current pool stats + private decimal currentPoolBalance = 0; + private int depositsInPoolCount = 0; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + isLoading = true; + try + { + // Set default year if not provided + if (!Year.HasValue || Year.Value == 0) + { + Year = DateTime.Now.Year; // Default to current year + } + + performanceModel.Year = Year.Value; + + // Load organization settings + organizationSettings = await OrganizationService.GetOrganizationSettingsAsync(); + + // Check if pool already exists for this year + existingPool = await SecurityDepositService.GetInvestmentPoolByYearAsync(Year.Value); + + if (existingPool != null) + { + // Populate form with existing data + performanceModel.TotalEarnings = existingPool.TotalEarnings; + performanceModel.ReturnRate = existingPool.ReturnRate; + performanceModel.Notes = existingPool.Notes; + } + else + { + // Create new pool to get starting balance + existingPool = await SecurityDepositService.GetOrCreateInvestmentPoolAsync(Year.Value); + } + + // Load recent pools for reference + recentPools = await SecurityDepositService.GetInvestmentPoolsAsync(); + + // Get current pool balance (all deposits in pool right now) + var allDeposits = await SecurityDepositService.GetSecurityDepositsAsync(); + var depositsInPool = allDeposits.Where(d => d.InInvestmentPool).ToList(); + depositsInPoolCount = depositsInPool.Count; + currentPoolBalance = depositsInPool.Sum(d => d.Amount); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to load data: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private void CalculateReturnRate() + { + if (existingPool != null && existingPool.StartingBalance > 0) + { + performanceModel.ReturnRate = performanceModel.TotalEarnings / existingPool.StartingBalance; + } + } + + private async Task HandleSubmit() + { + isSaving = true; + try + { + var endingBalance = (existingPool?.StartingBalance ?? 0) + performanceModel.TotalEarnings; + + await SecurityDepositService.RecordInvestmentPerformanceAsync( + performanceModel.Year, + existingPool?.StartingBalance ?? 0, + endingBalance, + performanceModel.TotalEarnings + ); + + ToastService.ShowSuccess($"Investment performance recorded for {performanceModel.Year}"); + NavigationManager.NavigateTo("/property-management/security-deposits/investment-pools"); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to record performance: {ex.Message}"); + } + finally + { + isSaving = false; + } + } + + private void Cancel() + { + NavigationManager.NavigateTo("/property-management/security-deposits/investment-pools"); + } + + private string GetMonthName(int month) + { + return new DateTime(2000, month, 1).ToString("MMMM"); + } + + private class PerformanceModel + { + public int Year { get; set; } + public decimal TotalEarnings { get; set; } + public decimal ReturnRate { get; set; } + public string? Notes { get; set; } + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor new file mode 100644 index 0000000..a9ceade --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor @@ -0,0 +1,401 @@ +@page "/property-management/security-deposits" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Core +@inject SecurityDepositService SecurityDepositService +@inject NavigationManager NavigationManager +@inject ToastService ToastService +@rendermode InteractiveServer +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + +Security Deposits + +
+
+
+

Security Deposits

+

Manage security deposits, investment pool, and dividend distributions

+
+ +
+ + @if (isLoading) + { +
+
+ Loading... +
+

Loading security deposits...

+
+ } + else + { + +
+
+
+
+
+ Total Deposits Held +
+

@totalDepositsHeld.ToString("C2")

+ @depositsHeldCount deposits +
+
+
+
+
+
+
+ Current Pool Balance +
+

@currentPoolBalance.ToString("C2")

+ @depositsInPoolCount deposits invested +
+
+
+
+
+
+
+ Released Deposits +
+

@totalReleased.ToString("C2")

+ @releasedCount deposits +
+
+
+
+
+
+
+ Total Refunded +
+

@totalRefunded.ToString("C2")

+ @refundedCount deposits +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + @if (!filteredDeposits.Any()) + { +
+ + @if (!allDeposits.Any()) + { + No Security Deposits Found +

Security deposits are collected when leases are signed and activated.

+ } + else + { + No deposits match your filters. + } +
+ } + else + { +
+
+
+
Security Deposits (@filteredDeposits.Count)
+
+
+
+
+ + + + + + + + + + + + + + + @foreach (var deposit in filteredDeposits.OrderByDescending(d => d.DateReceived)) + { + + + + + + + + + + + } + +
PropertyTenantAmountDate ReceivedPayment MethodStatusIn PoolActions
+ @if (deposit.Lease?.Property != null) + { + + @deposit.Lease.Property.Address
+ @deposit.Lease.Property.City, @deposit.Lease.Property.State +
+ } +
+ @if (deposit.Tenant != null) + { + + @deposit.Tenant.FirstName @deposit.Tenant.LastName + + } + + @deposit.Amount.ToString("C2") + + @deposit.DateReceived.ToString("MMM d, yyyy") + + @deposit.PaymentMethod + + @if (deposit.Status == ApplicationConstants.SecurityDepositStatuses.Held) + { + Held + } + else if (deposit.Status == ApplicationConstants.SecurityDepositStatuses.Released) + { + Released + } + else if (deposit.Status == ApplicationConstants.SecurityDepositStatuses.Refunded) + { + Refunded + } + else if (deposit.Status == ApplicationConstants.SecurityDepositStatuses.Forfeited) + { + Forfeited + } + else if (deposit.Status == ApplicationConstants.SecurityDepositStatuses.Forfeited) + { + Forfeited + } + + @if (deposit.InInvestmentPool) + { + + Yes + + @if (deposit.PoolEntryDate.HasValue) + { +
Since @deposit.PoolEntryDate.Value.ToString("MMM yyyy") + } + } + else + { + + No + + } +
+
+ @if (!deposit.InInvestmentPool && deposit.Status == ApplicationConstants.SecurityDepositStatuses.Held) + { + + } + else if (deposit.InInvestmentPool && deposit.Status == ApplicationConstants.SecurityDepositStatuses.Held) + { + + } + @if (deposit.Status == ApplicationConstants.SecurityDepositStatuses.Held) + { + + } +
+
+
+
+
+ } + } +
+ +@code { + private List allDeposits = new(); + private List filteredDeposits = new(); + private bool isLoading = true; + + private string filterStatus = ""; + private string searchTerm = ""; + + // Summary statistics + private decimal totalDepositsHeld = 0; + private int depositsHeldCount = 0; + private decimal currentPoolBalance = 0; + private int depositsInPoolCount = 0; + private decimal totalReleased = 0; + private int releasedCount = 0; + private decimal totalRefunded = 0; + private int refundedCount = 0; + + protected override async Task OnInitializedAsync() + { + await LoadDeposits(); + } + + private async Task LoadDeposits() + { + isLoading = true; + try + { + allDeposits = await SecurityDepositService.GetSecurityDepositsAsync(); + FilterDeposits(); + CalculateStatistics(); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to load security deposits: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private void FilterDeposits() + { + filteredDeposits = allDeposits.ToList(); + + // Filter by status + if (!string.IsNullOrEmpty(filterStatus)) + { + if (filterStatus == "InPool") + { + filteredDeposits = filteredDeposits.Where(d => d.InInvestmentPool).ToList(); + } + else + { + filteredDeposits = filteredDeposits.Where(d => d.Status == filterStatus).ToList(); + } + } + + // Filter by search term + if (!string.IsNullOrEmpty(searchTerm)) + { + var search = searchTerm.ToLower(); + filteredDeposits = filteredDeposits.Where(d => + (d.Tenant != null && (d.Tenant.FirstName.ToLower().Contains(search) || d.Tenant.LastName.ToLower().Contains(search))) || + (d.Lease?.Property != null && (d.Lease.Property.Address.ToLower().Contains(search) || + d.Lease.Property.City.ToLower().Contains(search))) || + d.LeaseId.ToString().Contains(search) || + (d.TransactionReference != null && d.TransactionReference.ToLower().Contains(search)) + ).ToList(); + } + } + + private void CalculateStatistics() + { + // Deposits held (not refunded) + var heldDeposits = allDeposits.Where(d => d.Status == ApplicationConstants.SecurityDepositStatuses.Held).ToList(); + depositsHeldCount = heldDeposits.Count; + totalDepositsHeld = heldDeposits.Sum(d => d.Amount); + + // Deposits in investment pool + var poolDeposits = allDeposits.Where(d => d.InInvestmentPool).ToList(); + depositsInPoolCount = poolDeposits.Count; + currentPoolBalance = poolDeposits.Sum(d => d.Amount); + + // Released deposits + var released = allDeposits.Where(d => d.Status == ApplicationConstants.SecurityDepositStatuses.Released).ToList(); + releasedCount = released.Count; + totalReleased = released.Sum(d => d.Amount); + + // Refunded + var refunded = allDeposits.Where(d => d.Status == ApplicationConstants.SecurityDepositStatuses.Refunded).ToList(); + refundedCount = refunded.Count; + totalRefunded = refunded.Sum(d => d.Amount); + } + + private void ClearFilters() + { + filterStatus = ""; + searchTerm = ""; + FilterDeposits(); + } + + private async Task AddToPool(Guid depositId) + { + try + { + await SecurityDepositService.AddToInvestmentPoolAsync(depositId); + ToastService.ShowSuccess("Deposit added to investment pool"); + await LoadDeposits(); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to add to pool: {ex.Message}"); + } + } + + private async Task RemoveFromPool(Guid depositId) + { + try + { + await SecurityDepositService.RemoveFromInvestmentPoolAsync(depositId); + ToastService.ShowSuccess("Deposit removed from investment pool"); + await LoadDeposits(); + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to remove from pool: {ex.Message}"); + } + } + + private void InitiateRefund(Guid depositId) + { + // TODO: Navigate to refund workflow page when implemented + ToastService.ShowInfo("Refund workflow coming soon"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor new file mode 100644 index 0000000..4ab7523 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor @@ -0,0 +1,419 @@ +@page "/property-management/security-deposits/investment-pool/{PoolId:guid}" +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Constants +@using Microsoft.AspNetCore.Authorization +@inject SecurityDepositService SecurityDepositService +@inject NavigationManager NavigationManager +@inject ToastService ToastService +@rendermode InteractiveServer + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + + +Investment Pool Details - @(pool?.Year ?? 0) + +
+ @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (pool == null) + { +
+ + Investment pool not found. +
+ } + else + { +
+
+ +
+
+

@pool.Year Investment Pool

+

Detailed performance and dividend information

+
+
+ @if (pool.Status == ApplicationConstants.InvestmentPoolStatuses.Open) + { + + } + else if (pool.Status == ApplicationConstants.InvestmentPoolStatuses.Calculated) + { + Dividends Calculated - Ready to Distribute + } + else if (pool.Status == ApplicationConstants.InvestmentPoolStatuses.Distributed) + { + Dividends Distributed + } +
+
+
+
+ + +
+
+
+
+
Starting Balance
+

@pool.StartingBalance.ToString("C2")

+ @pool.ActiveLeaseCount active leases +
+
+
+
+
+
+
Total Earnings
+

+ @if (pool.HasEarnings) + { + @pool.TotalEarnings.ToString("C2") + } + else if (pool.HasLosses) + { + @pool.TotalEarnings.ToString("C2") + } + else + { + $0.00 + } +

+ + @if (pool.ReturnRate >= 0) + { + @((pool.ReturnRate * 100).ToString("F2"))% return + } + else + { + @((pool.ReturnRate * 100).ToString("F2"))% loss + } + +
+
+
+
+
+
+
Organization Share
+

@pool.OrganizationShare.ToString("C2")

+ @((pool.OrganizationSharePercentage * 100).ToString("F0"))% of earnings +
+
+
+
+
+
+
Tenant Share Total
+

@pool.TenantShareTotal.ToString("C2")

+ + @if (pool.DividendPerLease > 0) + { + @pool.DividendPerLease.ToString("C2") per lease + } + else + { + No dividends + } + +
+
+
+
+ + +
+
+
+
+
Performance Summary
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Year:@pool.Year
Starting Balance:@pool.StartingBalance.ToString("C2")
Ending Balance:@pool.EndingBalance.ToString("C2")
Total Earnings: + @if (pool.HasEarnings) + { + +@pool.TotalEarnings.ToString("C2") + } + else if (pool.HasLosses) + { + @pool.TotalEarnings.ToString("C2") + } + else + { + $0.00 + } +
Return Rate: + @if (pool.ReturnRate >= 0) + { + @((pool.ReturnRate * 100).ToString("F2"))% + } + else + { + @((pool.ReturnRate * 100).ToString("F2"))% + } +
Active Leases:@pool.ActiveLeaseCount
+ + @if (!string.IsNullOrEmpty(pool.Notes)) + { +
+
Notes:
+

@pool.Notes

+ } +
+
+
+ +
+
+
+
Distribution Details
+
+
+ @if (pool.HasEarnings) + { + + + + + + + + + + + + + + + + + + + @if (pool.DividendsCalculatedOn.HasValue) + { + + + + + } + @if (pool.DividendsDistributedOn.HasValue) + { + + + + + } + +
Organization Share %:@((pool.OrganizationSharePercentage * 100).ToString("F0"))%
Organization Amount:@pool.OrganizationShare.ToString("C2")
Tenant Share Total:@pool.TenantShareTotal.ToString("C2")
Dividend Per Lease:@pool.DividendPerLease.ToString("C2")
Calculated On:@pool.DividendsCalculatedOn.Value.ToString("MMM d, yyyy")
Distributed On:@pool.DividendsDistributedOn.Value.ToString("MMM d, yyyy")
+ } + else if (pool.HasLosses) + { +
+
Loss Absorbed by Organization
+

Investment losses of @pool.AbsorbedLosses.ToString("C2") were absorbed by the organization.

+

No dividends were distributed to tenants, and all security deposits remain unchanged.

+
+ } + else + { +
+

No earnings or losses for this period.

+
+ } +
+
+
+
+ + + @if (dividends.Any()) + { +
+
+
Dividend Distributions (@dividends.Count)
+
+
+
+ + + + + + + + + + + + + + @foreach (var dividend in dividends.OrderByDescending(d => d.DividendAmount)) + { + + + + + + + + + + } + + + + + + + + +
TenantLease IDBase DividendProrationFinal AmountPayment MethodStatus
+ Tenant #@dividend.TenantId + Lease #@dividend.LeaseId@dividend.BaseDividendAmount.ToString("C2") + @if (dividend.ProrationFactor < 1.0m) + { + + @((dividend.ProrationFactor * 100).ToString("F0"))% + +
+ @dividend.MonthsInPool mo + } + else + { + 100% +
+ Full year + } +
+ @dividend.DividendAmount.ToString("C2") + + @if (dividend.PaymentMethod == ApplicationConstants.DividendPaymentMethods.Pending) + { + Pending Choice + } + else if (dividend.PaymentMethod == ApplicationConstants.DividendPaymentMethods.LeaseCredit) + { + Lease Credit + } + else if (dividend.PaymentMethod == ApplicationConstants.DividendPaymentMethods.Check) + { + Check + } + + @if (dividend.Status == ApplicationConstants.DividendStatuses.Pending) + { + Pending + } + else if (dividend.Status == ApplicationConstants.DividendStatuses.ChoiceMade) + { + Choice Made + } + else if (dividend.Status == ApplicationConstants.DividendStatuses.Applied) + { + Applied + } + else if (dividend.Status == ApplicationConstants.DividendStatuses.Paid) + { + Paid + } +
Total Dividends:@dividends.Sum(d => d.DividendAmount).ToString("C2")
+
+
+
+ } + else if (pool.HasEarnings) + { +
+ + Dividends Not Yet Calculated +

Click "Calculate Dividends" to generate dividend distributions for all active leases.

+
+ } + } +
+ +@code { + [Parameter] + public Guid PoolId { get; set; } + + private SecurityDepositInvestmentPool? pool; + private List dividends = new(); + private bool isLoading = true; + + protected override async Task OnInitializedAsync() + { + await LoadPoolDetails(); + } + + private async Task LoadPoolDetails() + { + isLoading = true; + try + { + pool = await SecurityDepositService.GetInvestmentPoolByIdAsync(PoolId); + + if (pool != null) + { + dividends = await SecurityDepositService.GetDividendsByYearAsync(pool.Year); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Failed to load pool details: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private void NavigateToCalculateDividends() + { + NavigationManager.NavigateTo($"/property-management/security-deposits/calculate-dividends/{PoolId}"); + } +} diff --git a/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor b/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor new file mode 100644 index 0000000..5065e35 --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor @@ -0,0 +1,217 @@ +@page "/propertymanagement/tenants/create" + +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authorization +@inject TenantService TenantService +@inject NavigationManager NavigationManager +@inject ToastService ToastService +@rendermode InteractiveServer + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +

Create Tenant

+ +
+
+
+
+

Add New Tenant

+
+
+ + + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+ + +
+
+
+
+
+
+ +@code { + private TenantModel tenantModel = new TenantModel(); + private bool isSubmitting = false; + private string errorMessage = string.Empty; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + private async Task SaveTenant() + { + try + { + isSubmitting = true; + errorMessage = string.Empty; + + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + errorMessage = "User not authenticated."; + ToastService.ShowError("User not authenticated. Please log in again."); + return; + } + + // Check for duplicate identification number + if (!string.IsNullOrWhiteSpace(tenantModel.IdentificationNumber)) + { + var existingTenant = await TenantService.GetTenantByIdentificationNumberAsync(tenantModel.IdentificationNumber); + if (existingTenant != null) + { + errorMessage = $"A tenant with identification number {tenantModel.IdentificationNumber} already exists. " + + $"View existing tenant: {existingTenant.FullName}"; + ToastService.ShowWarning($"Duplicate identification number found for {existingTenant.FullName}"); + return; + } + } + + var tenant = new Tenant + { + FirstName = tenantModel.FirstName, + LastName = tenantModel.LastName, + Email = tenantModel.Email, + PhoneNumber = tenantModel.PhoneNumber, + DateOfBirth = tenantModel.DateOfBirth, + EmergencyContactName = tenantModel.EmergencyContactName, + EmergencyContactPhone = tenantModel.EmergencyContactPhone, + Notes = tenantModel.Notes, + IdentificationNumber = tenantModel.IdentificationNumber, + IsActive = true + }; + + await TenantService.CreateAsync(tenant); + + ToastService.ShowSuccess($"Tenant {tenant.FullName} created successfully!"); + NavigationManager.NavigateTo("/propertymanagement/tenants"); + } + catch (Exception ex) + { + errorMessage = $"Error creating tenant: {ex.Message}"; + ToastService.ShowError($"Failed to create tenant: {ex.Message}"); + } + finally + { + isSubmitting = false; + } + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/tenants"); + } + + public class TenantModel + { + [Required(ErrorMessage = "First name is required")] + [StringLength(100, ErrorMessage = "First name cannot exceed 100 characters")] + public string FirstName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Last name is required")] + [StringLength(100, ErrorMessage = "Last name cannot exceed 100 characters")] + public string LastName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Please enter a valid email address")] + [StringLength(255, ErrorMessage = "Email cannot exceed 255 characters")] + public string Email { get; set; } = string.Empty; + + [Phone(ErrorMessage = "Please enter a valid phone number")] + [StringLength(20, ErrorMessage = "Phone number cannot exceed 20 characters")] + public string PhoneNumber { get; set; } = string.Empty; + + public DateTime? DateOfBirth { get; set; } + + [Required(ErrorMessage = "Identification number is required")] + [StringLength(100, ErrorMessage = "Identification number cannot exceed 100 characters")] + public string IdentificationNumber { get; set; } = string.Empty; + + [StringLength(200, ErrorMessage = "Emergency contact name cannot exceed 200 characters")] + public string EmergencyContactName { get; set; } = string.Empty; + + [Phone(ErrorMessage = "Please enter a valid phone number")] + [StringLength(20, ErrorMessage = "Emergency contact phone cannot exceed 20 characters")] + public string EmergencyContactPhone { get; set; } = string.Empty; + + [StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")] + public string Notes { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/EditTenant.razor b/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/EditTenant.razor new file mode 100644 index 0000000..fbae2fb --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/EditTenant.razor @@ -0,0 +1,339 @@ +@page "/propertymanagement/tenants/edit/{Id:guid}" +@using Aquiis.Professional.Core.Entities +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Web + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject NavigationManager NavigationManager +@inject TenantService TenantService +@rendermode InteractiveServer + +@if (tenant == null) +{ +
+
+ Loading... +
+
+} +else if (!isAuthorized) +{ +
+

Access Denied

+

You don't have permission to edit this tenant.

+ Back to Tenants +
+} +else +{ +
+
+
+
+

Edit Tenant

+
+
+ + + + @if (!string.IsNullOrEmpty(errorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(successMessage)) + { + + } + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+
+
+ + Active +
+
+
+
+ + + +
+
+ +
+ + + +
+
+
+
+
+ +
+
+
+
Tenant Actions
+
+
+
+ + + +
+
+
+ +
+
+
Tenant Information
+
+
+ + Added: @tenant.CreatedOn.ToString("MMMM dd, yyyy") +
+ @if (tenant.LastModifiedOn.HasValue) + { + Last Modified: @tenant.LastModifiedOn.Value.ToString("MMMM dd, yyyy") + } +
+
+
+
+
+} + +@code { + [Parameter] public Guid Id { get; set; } + + private Tenant? tenant; + private TenantModel tenantModel = new(); + private bool isSubmitting = false; + private bool isAuthorized = true; + private string errorMessage = string.Empty; + private string successMessage = string.Empty; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadTenant(); + } + + private async Task LoadTenant() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + isAuthorized = false; + return; + } + + tenant = await TenantService.GetByIdAsync(Id); + + if (tenant == null) + { + isAuthorized = false; + return; + } + + // Map tenant to model + tenantModel = new TenantModel + { + FirstName = tenant.FirstName, + LastName = tenant.LastName, + Email = tenant.Email, + PhoneNumber = tenant.PhoneNumber, + DateOfBirth = tenant.DateOfBirth, + IdentificationNumber = tenant.IdentificationNumber, + IsActive = tenant.IsActive, + EmergencyContactName = tenant.EmergencyContactName, + EmergencyContactPhone = tenant.EmergencyContactPhone!, + Notes = tenant.Notes + }; + } + + private async Task UpdateTenant() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + isAuthorized = false; + return; + } + + try + { + isSubmitting = true; + errorMessage = string.Empty; + successMessage = string.Empty; + + // Update tenant with form data + tenant!.FirstName = tenantModel.FirstName; + tenant.LastName = tenantModel.LastName; + tenant.Email = tenantModel.Email; + tenant.PhoneNumber = tenantModel.PhoneNumber; + tenant.DateOfBirth = tenantModel.DateOfBirth; + tenant.IdentificationNumber = tenantModel.IdentificationNumber; + tenant.IsActive = tenantModel.IsActive; + tenant.EmergencyContactName = tenantModel.EmergencyContactName; + tenant.EmergencyContactPhone = tenantModel.EmergencyContactPhone; + tenant.Notes = tenantModel.Notes; + + await TenantService.UpdateAsync(tenant); + successMessage = "Tenant updated successfully!"; + } + catch (Exception ex) + { + errorMessage = $"Error updating tenant: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + + private void ViewTenant() + { + NavigationManager.NavigateTo($"/propertymanagement/tenants/view/{Id}"); + } + + private void CreateLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/create?tenantId={Id}"); + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/tenants"); + } + + private async Task DeleteTenant() + { + if (tenant != null) + { + try + { + await TenantService.DeleteAsync(tenant.Id); + NavigationManager.NavigateTo("/propertymanagement/tenants"); + } + catch (Exception ex) + { + errorMessage = $"Error deleting tenant: {ex.Message}"; + } + } + } + + public class TenantModel + { + [Required(ErrorMessage = "First name is required")] + [StringLength(100, ErrorMessage = "First name cannot exceed 100 characters")] + public string FirstName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Last name is required")] + [StringLength(100, ErrorMessage = "Last name cannot exceed 100 characters")] + public string LastName { get; set; } = string.Empty; + + [Required(ErrorMessage = "Email is required")] + [EmailAddress(ErrorMessage = "Please enter a valid email address")] + [StringLength(255, ErrorMessage = "Email cannot exceed 255 characters")] + public string Email { get; set; } = string.Empty; + + [Phone(ErrorMessage = "Please enter a valid phone number")] + [StringLength(20, ErrorMessage = "Phone number cannot exceed 20 characters")] + public string PhoneNumber { get; set; } = string.Empty; + + public DateTime? DateOfBirth { get; set; } + + [Required(ErrorMessage = "Identification number is required")] + [StringLength(100, ErrorMessage = "Identification number cannot exceed 100 characters")] + public string IdentificationNumber { get; set; } = string.Empty; + + public bool IsActive { get; set; } + + [StringLength(200, ErrorMessage = "Emergency contact name cannot exceed 200 characters")] + public string EmergencyContactName { get; set; } = string.Empty; + + [Phone(ErrorMessage = "Please enter a valid phone number")] + [StringLength(20, ErrorMessage = "Emergency contact phone cannot exceed 20 characters")] + public string EmergencyContactPhone { get; set; } = string.Empty; + + [StringLength(500, ErrorMessage = "Notes cannot exceed 500 characters")] + public string Notes { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Tenants.razor b/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Tenants.razor new file mode 100644 index 0000000..f51e11f --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Tenants.razor @@ -0,0 +1,528 @@ +@page "/propertymanagement/tenants" +@using Aquiis.Professional.Features.PropertyManagement +@using Microsoft.AspNetCore.Authorization +@inject NavigationManager Navigation +@inject TenantService TenantService +@inject IJSRuntime JSRuntime +@inject UserContextService UserContext + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager", "User")] +@rendermode InteractiveServer + +
+

Tenants

+ @if (!isReadOnlyUser) + { + + } +
+ +@if (tenants == null) +{ +
+
+ Loading... +
+
+} +else if (!tenants.Any()) +{ +
+

No Tenants Found

+

Get started by converting a Prospective Tenant to your first tenant in the system.

+ +
+} +else +{ +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
Active Tenants
+

@activeTenantsCount

+
+
+
+
+
+
+
Without Lease
+

@tenantsWithoutLeaseCount

+
+
+
+
+
+
+
Total Tenants
+

@filteredTenants.Count

+
+
+
+
+
+
+
New This Month
+

@newThisMonthCount

+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + @foreach (var tenant in pagedTenants) + { + + + + + + + + + + + } + +
+ + + + + + + + + + + + Lease StatusActions
+
+ @tenant.FullName + @if (!string.IsNullOrEmpty(tenant.Notes)) + { +
+ @tenant.Notes + } +
+
@tenant.Email@tenant.PhoneNumber + @if (tenant.DateOfBirth.HasValue) + { + @tenant.DateOfBirth.Value.ToString("MMM dd, yyyy") + } + else + { + Not provided + } + + @if (tenant.IsActive) + { + Active + } + else + { + Inactive + } + @tenant.CreatedOn.ToString("MMM dd, yyyy") + @{ + var activeLease = tenant.Leases?.FirstOrDefault(l => l.Status == "Active"); + var latestLease = tenant.Leases?.OrderByDescending(l => l.StartDate).FirstOrDefault(); + } + @if (activeLease != null) + { + Active + } + else if (latestLease != null) + { + @latestLease.Status + } + else + { + No Lease + } + +
+ + @if (!isReadOnlyUser) + { + + + } +
+
+
+ + @if (totalPages > 1) + { +
+
+ + Showing @((currentPage - 1) * pageSize + 1) to @Math.Min(currentPage * pageSize, totalRecords) of @totalRecords tenants + +
+ +
+ } +
+
+} + +@code { + private List? tenants; + private List filteredTenants = new(); + private List pagedTenants = new(); + private string searchTerm = string.Empty; + private string selectedLeaseStatus = string.Empty; + + private int selectedTenantStatus = 1; + + private string sortColumn = nameof(Tenant.FirstName); + private bool sortAscending = true; + private int activeTenantsCount = 0; + private int tenantsWithoutLeaseCount = 0; + private int newThisMonthCount = 0; + + // Pagination variables + private int currentPage = 1; + private int pageSize = 20; + private int totalPages = 1; + private int totalRecords = 0; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + private string? currentUserRole; + private bool isReadOnlyUser => currentUserRole == ApplicationConstants.OrganizationRoles.User; + + protected override async Task OnInitializedAsync() + { + // Get current user's role + currentUserRole = await UserContext.GetCurrentOrganizationRoleAsync(); + + await LoadTenants(); + FilterTenants(); + CalculateMetrics(); + } + + private async Task LoadTenants() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + tenants = new List(); + return; + } + + tenants = await TenantService.GetAllAsync(); + } + + private void CreateTenant() + { + Navigation.NavigateTo("/propertymanagement/prospectivetenants"); + } + + private void ViewTenant(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/tenants/view/{id}"); + } + + private void EditTenant(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/tenants/edit/{id}"); + } + + private async Task DeleteTenant(Guid id) + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + return; + + + // Add confirmation dialog in a real application + var tenant = await TenantService.GetByIdAsync(id); + if (tenant != null) + { + + await TenantService.DeleteAsync(tenant.Id); + await LoadTenants(); + FilterTenants(); + CalculateMetrics(); + } + } + + private void FilterTenants() + { + if (tenants == null) + { + filteredTenants = new(); + pagedTenants = new(); + return; + } + + filteredTenants = tenants.Where(t => + (string.IsNullOrEmpty(searchTerm) || + t.FullName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + t.Email.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + t.PhoneNumber.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + t.Notes.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) && + (string.IsNullOrEmpty(selectedLeaseStatus) || GetTenantLeaseStatus(t) == selectedLeaseStatus) && + (selectedTenantStatus == 1 ? t.IsActive : !t.IsActive) + ).ToList(); + + SortTenants(); + UpdatePagination(); + CalculateMetrics(); + } + + private string GetTenantLeaseStatus(Tenant tenant) + { + var activeLease = tenant.Leases?.FirstOrDefault(l => l.Status == "Active"); + if (activeLease != null) return "Active"; + + var latestLease = tenant.Leases?.OrderByDescending(l => l.StartDate).FirstOrDefault(); + if (latestLease != null) return latestLease.Status; + + return "No Lease"; + } + + private void SortBy(string column) + { + if (sortColumn == column) + { + sortAscending = !sortAscending; + } + else + { + sortColumn = column; + sortAscending = true; + } + + SortTenants(); + } + + private void SortTenants() + { + if (filteredTenants == null) return; + + filteredTenants = sortColumn switch + { + nameof(Tenant.FirstName) => sortAscending + ? filteredTenants.OrderBy(t => t.FirstName).ThenBy(t => t.LastName).ToList() + : filteredTenants.OrderByDescending(t => t.FirstName).ThenByDescending(t => t.LastName).ToList(), + nameof(Tenant.Email) => sortAscending + ? filteredTenants.OrderBy(t => t.Email).ToList() + : filteredTenants.OrderByDescending(t => t.Email).ToList(), + nameof(Tenant.PhoneNumber) => sortAscending + ? filteredTenants.OrderBy(t => t.PhoneNumber).ToList() + : filteredTenants.OrderByDescending(t => t.PhoneNumber).ToList(), + nameof(Tenant.DateOfBirth) => sortAscending + ? filteredTenants.OrderBy(t => t.DateOfBirth ?? DateTime.MinValue).ToList() + : filteredTenants.OrderByDescending(t => t.DateOfBirth ?? DateTime.MinValue).ToList(), + nameof(Tenant.IsActive) => sortAscending + ? filteredTenants.OrderBy(t => t.IsActive).ToList() + : filteredTenants.OrderByDescending(t => t.IsActive).ToList(), + nameof(Tenant.CreatedOn) => sortAscending + ? filteredTenants.OrderBy(t => t.CreatedOn).ToList() + : filteredTenants.OrderByDescending(t => t.CreatedOn).ToList(), + _ => filteredTenants + }; + + UpdatePagination(); + } + + private void CalculateMetrics() + { + if (filteredTenants != null) + { + activeTenantsCount = filteredTenants.Count(t => + t.Leases?.Any(l => l.Status == "Active") == true); + + tenantsWithoutLeaseCount = filteredTenants.Count(t => + t.Leases?.Any() != true); + + var now = DateTime.Now; + newThisMonthCount = filteredTenants.Count(t => + t.CreatedOn.Month == now.Month && t.CreatedOn.Year == now.Year); + } + } + + private string GetLeaseStatusClass(string status) + { + return status switch + { + "Active" => "success", + "Expired" => "warning", + "Terminated" => "danger", + "Pending" => "info", + _ => "secondary" + }; + } + + private void ClearFilters() + { + searchTerm = string.Empty; + selectedLeaseStatus = string.Empty; + currentPage = 1; + FilterTenants(); + } + + private void UpdatePagination() + { + totalRecords = filteredTenants?.Count ?? 0; + totalPages = (int)Math.Ceiling((double)totalRecords / pageSize); + + // Ensure current page is valid + if (currentPage > totalPages && totalPages > 0) + { + currentPage = totalPages; + } + else if (currentPage < 1) + { + currentPage = 1; + } + + // Get the current page of data + pagedTenants = filteredTenants? + .Skip((currentPage - 1) * pageSize) + .Take(pageSize) + .ToList() ?? new List(); + } + + private void GoToPage(int page) + { + if (page >= 1 && page <= totalPages && page != currentPage) + { + currentPage = page; + UpdatePagination(); + } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/View.razor b/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/View.razor new file mode 100644 index 0000000..84053fd --- /dev/null +++ b/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/View.razor @@ -0,0 +1,241 @@ +@page "/propertymanagement/tenants/view/{Id:guid}" +@using Aquiis.Professional.Core.Entities +@using Microsoft.AspNetCore.Authorization +@inject NavigationManager NavigationManager +@inject TenantService TenantService +@inject LeaseService LeaseService + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +@if (tenant == null) +{ +
+
+ Loading... +
+
+} +else if (!isAuthorized) +{ +
+

Access Denied

+

You don't have permission to view this tenant.

+ Back to Tenants +
+} +else +{ +
+

Tenant Details

+
+ + +
+
+ +
+
+
+
+
Personal Information
+
+
+
+
+ Full Name: +

@tenant.FullName

+
+
+ Email: +

@tenant.Email

+
+
+ +
+
+ Phone Number: +

@(!string.IsNullOrEmpty(tenant.PhoneNumber) ? tenant.PhoneNumber : "Not provided")

+
+
+ Date of Birth: +

@(tenant.DateOfBirth?.ToString("MMMM dd, yyyy") ?? "Not provided")

+
+
+ +
+
+ Identification Number: +

@(!string.IsNullOrEmpty(tenant.IdentificationNumber) ? tenant.IdentificationNumber : "Not provided")

+
+
+ Status: +

@(tenant.IsActive ? "Active" : "Inactive")

+
+
+ + @if (!string.IsNullOrEmpty(tenant.EmergencyContactName) || !string.IsNullOrEmpty(tenant.EmergencyContactPhone)) + { +
+
Emergency Contact
+
+
+ Contact Name: +

@(!string.IsNullOrEmpty(tenant.EmergencyContactName) ? tenant.EmergencyContactName : "Not provided")

+
+
+ Contact Phone: +

@(!string.IsNullOrEmpty(tenant.EmergencyContactPhone) ? tenant.EmergencyContactPhone : "Not provided")

+
+
+ } + + @if (!string.IsNullOrEmpty(tenant.Notes)) + { +
+
+
+ Notes: +

@tenant.Notes

+
+
+ } + +
+
+
+ Added to System: +

@tenant.CreatedOn.ToString("MMMM dd, yyyy")

+
+ @if (tenant.LastModifiedOn.HasValue) + { +
+ Last Modified: +

@tenant.LastModifiedOn.Value.ToString("MMMM dd, yyyy")

+
+ } +
+
+
+
+ +
+
+
+
Quick Actions
+
+
+
+ + + + +
+
+
+ + @if (tenantLeases.Any()) + { +
+
+
Lease History
+
+
+ @foreach (var lease in tenantLeases.OrderByDescending(l => l.StartDate)) + { +
+ @lease.Property?.Address +
+ + @lease.StartDate.ToString("MMM dd, yyyy") - @lease.EndDate.ToString("MMM dd, yyyy") + +
+ + @lease.Status + + @lease.MonthlyRent.ToString("C")/month +
+ } +
+
+ } +
+
+} + +@code { + [Parameter] + public Guid Id { get; set; } + + private Tenant? tenant; + private List tenantLeases = new(); + private bool isAuthorized = true; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await LoadTenant(); + } + + private async Task LoadTenant() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + if (string.IsNullOrEmpty(userId)) + { + isAuthorized = false; + return; + } + + tenant = await TenantService.GetByIdAsync(Id); + + if (tenant == null) + { + isAuthorized = false; + return; + } + + // Load leases for this tenant + tenantLeases = await LeaseService.GetLeasesByTenantIdAsync(Id); + } + + private void EditTenant() + { + NavigationManager.NavigateTo($"/propertymanagement/tenants/edit/{Id}"); + } + + private void BackToList() + { + NavigationManager.NavigateTo("/propertymanagement/tenants"); + } + + private void CreateLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/create?tenantId={Id}"); + } + + private void ViewLeases() + { + NavigationManager.NavigateTo($"/propertymanagement/leases?tenantId={Id}"); + } + + private void ViewDocuments() + { + NavigationManager.NavigateTo($"/propertymanagement/documents?tenantId={Id}"); + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Features/_Imports.razor b/Aquiis.Professional/Features/_Imports.razor new file mode 100644 index 0000000..1eb3f46 --- /dev/null +++ b/Aquiis.Professional/Features/_Imports.razor @@ -0,0 +1,21 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Aquiis.Professional +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Shared.Layout +@using Aquiis.Professional.Shared.Components +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.Professional.Shared.Authorization +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Constants +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Features.Administration diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Bold.ttf b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..6d65fa7 Binary files /dev/null and b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Bold.ttf differ diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf new file mode 100644 index 0000000..753f2d8 Binary files /dev/null and b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf differ diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf new file mode 100644 index 0000000..b09f32d Binary files /dev/null and b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf differ diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Oblique.ttf b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Oblique.ttf new file mode 100644 index 0000000..999bac7 Binary files /dev/null and b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Oblique.ttf differ diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans.ttf b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans.ttf new file mode 100644 index 0000000..e5f7eec Binary files /dev/null and b/Aquiis.Professional/Fonts/DejaVu/DejaVuSans.ttf differ diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf b/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf new file mode 100644 index 0000000..22987c6 Binary files /dev/null and b/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf differ diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf b/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf new file mode 100644 index 0000000..f5fa0ca Binary files /dev/null and b/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf differ diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf b/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf new file mode 100644 index 0000000..7fde907 Binary files /dev/null and b/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf differ diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed.ttf b/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed.ttf new file mode 100644 index 0000000..3259bc2 Binary files /dev/null and b/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Black.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-Black.ttf new file mode 100755 index 0000000..53a31ac Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-Black.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-BlackItalic.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-BlackItalic.ttf new file mode 100755 index 0000000..b3cee66 Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-BlackItalic.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Bold.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-Bold.ttf new file mode 100755 index 0000000..1d23c70 Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-Bold.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-BoldItalic.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-BoldItalic.ttf new file mode 100755 index 0000000..a3b8e33 Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-BoldItalic.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBold.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBold.ttf new file mode 100755 index 0000000..8612461 Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBold.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf new file mode 100755 index 0000000..516676f Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLight.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLight.ttf new file mode 100755 index 0000000..648e78c Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLight.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLightItalic.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLightItalic.ttf new file mode 100755 index 0000000..569692a Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLightItalic.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Italic.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-Italic.ttf new file mode 100755 index 0000000..70a870f Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-Italic.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Light.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-Light.ttf new file mode 100755 index 0000000..c3f3cb5 Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-Light.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-LightItalic.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-LightItalic.ttf new file mode 100755 index 0000000..9368e06 Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-LightItalic.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Medium.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-Medium.ttf new file mode 100755 index 0000000..0648fb2 Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-Medium.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-MediumItalic.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-MediumItalic.ttf new file mode 100755 index 0000000..af296ab Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-MediumItalic.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Regular.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-Regular.ttf new file mode 100755 index 0000000..0f3d0f8 Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-Regular.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-SemiBold.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-SemiBold.ttf new file mode 100755 index 0000000..3b1bccc Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-SemiBold.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-SemiBoldItalic.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-SemiBoldItalic.ttf new file mode 100755 index 0000000..032b99d Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-SemiBoldItalic.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Thin.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-Thin.ttf new file mode 100755 index 0000000..81167fa Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-Thin.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ThinItalic.ttf b/Aquiis.Professional/Fonts/LatoFont/Lato-ThinItalic.ttf new file mode 100755 index 0000000..339d2dc Binary files /dev/null and b/Aquiis.Professional/Fonts/LatoFont/Lato-ThinItalic.ttf differ diff --git a/Aquiis.Professional/Fonts/LatoFont/OFL.txt b/Aquiis.Professional/Fonts/LatoFont/OFL.txt new file mode 100755 index 0000000..dfca0da --- /dev/null +++ b/Aquiis.Professional/Fonts/LatoFont/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Aquiis.Professional/Infrastructure/Data/ApplicationDbContext.cs b/Aquiis.Professional/Infrastructure/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..bae9991 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/ApplicationDbContext.cs @@ -0,0 +1,742 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Aquiis.Professional.Shared.Components.Account; +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Infrastructure.Data +{ + + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + + // Suppress pending model changes warning - bidirectional Document-Invoice/Payment relationship issue + // TODO: Fix the Document-Invoice and Document-Payment bidirectional relationships properly + optionsBuilder.ConfigureWarnings(warnings => + warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)); + } + + public DbSet Properties { get; set; } + public DbSet Leases { get; set; } + public DbSet LeaseOffers { get; set; } + public DbSet Tenants { get; set; } + public DbSet Invoices { get; set; } + public DbSet Payments { get; set; } + public DbSet Documents { get; set; } + public DbSet Inspections { get; set; } + public DbSet MaintenanceRequests { get; set; } + public DbSet OrganizationSettings { get; set; } + public DbSet SchemaVersions { get; set; } + public DbSet ChecklistTemplates { get; set; } + public DbSet ChecklistTemplateItems { get; set; } + public DbSet Checklists { get; set; } + public DbSet ChecklistItems { get; set; } + public DbSet ProspectiveTenants { get; set; } + public DbSet Tours { get; set; } + public DbSet RentalApplications { get; set; } + public DbSet ApplicationScreenings { get; set; } + public DbSet CalendarEvents { get; set; } + public DbSet CalendarSettings { get; set; } + public DbSet Notes { get; set; } + public DbSet SecurityDeposits { get; set; } + public DbSet SecurityDepositInvestmentPools { get; set; } + public DbSet SecurityDepositDividends { get; set; } + + // Multi-organization support + public DbSet Organizations { get; set; } + public DbSet UserOrganizations { get; set; } + + // Workflow audit logging + public DbSet WorkflowAuditLogs { get; set; } + + + // Notification system + public DbSet Notifications { get; set; } + public DbSet NotificationPreferences { get; set; } + public DbSet OrganizationEmailSettings { get; set; } + public DbSet OrganizationSMSSettings { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure Property entity + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.Address); + entity.Property(e => e.MonthlyRent).HasPrecision(18, 2); + + // Configure relationship with Organization + entity.HasOne() + .WithMany(o => o.Properties) + .HasForeignKey(e => e.OrganizationId) + .OnDelete(DeleteBehavior.NoAction); + }); + + // Configure Tenant entity + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.Email).IsUnique(); + entity.HasIndex(e => e.IdentificationNumber).IsUnique(); + + // Configure relationship with Organization + entity.HasOne() + .WithMany(o => o.Tenants) + .HasForeignKey(e => e.OrganizationId) + .OnDelete(DeleteBehavior.NoAction); + }); + + // Configure Lease entity + modelBuilder.Entity(entity => + { + entity.HasOne(l => l.Property) + .WithMany(p => p.Leases) + .HasForeignKey(l => l.PropertyId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(l => l.Tenant) + .WithMany(t => t.Leases) + .HasForeignKey(l => l.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(l => l.Document) + .WithMany() + .HasForeignKey(l => l.DocumentId) + .OnDelete(DeleteBehavior.SetNull); + + entity.Property(e => e.MonthlyRent).HasPrecision(18, 2); + entity.Property(e => e.SecurityDeposit).HasPrecision(18, 2); + + // Configure relationship with Organization + entity.HasOne() + .WithMany(o => o.Leases) + .HasForeignKey(e => e.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Configure Invoice entity + modelBuilder.Entity(entity => + { + entity.HasOne(i => i.Lease) + .WithMany(l => l.Invoices) + .HasForeignKey(i => i.LeaseId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(i => i.Document) + .WithMany() + .HasForeignKey(i => i.DocumentId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasIndex(e => e.InvoiceNumber).IsUnique(); + entity.Property(e => e.Amount).HasPrecision(18, 2); + entity.Property(e => e.AmountPaid).HasPrecision(18, 2); + + // Configure relationship with User + entity.HasOne() + .WithMany() + .HasForeignKey(e => e.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Configure Payment entity + modelBuilder.Entity(entity => + { + entity.HasOne(p => p.Invoice) + .WithMany(i => i.Payments) + .HasForeignKey(p => p.InvoiceId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(p => p.Document) + .WithMany() + .HasForeignKey(p => p.DocumentId) + .OnDelete(DeleteBehavior.SetNull); + + entity.Property(e => e.Amount).HasPrecision(18, 2); + + // Configure relationship with User + entity.HasOne() + .WithMany() + .HasForeignKey(e => e.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Configure Document entity + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.Property) + .WithMany(p => p.Documents) + .HasForeignKey(d => d.PropertyId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(d => d.Tenant) + .WithMany() + .HasForeignKey(d => d.TenantId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(d => d.Lease) + .WithMany(l => l.Documents) + .HasForeignKey(d => d.LeaseId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(d => d.Invoice) + .WithMany() + .HasForeignKey(d => d.InvoiceId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(d => d.Payment) + .WithMany() + .HasForeignKey(d => d.PaymentId) + .OnDelete(DeleteBehavior.SetNull); + + // FileData is automatically stored as BLOB in SQLite + // No need to specify column type + + // Configure relationship with User + entity.HasOne() + .WithMany() + .HasForeignKey(e => e.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Configure Inspection entity + modelBuilder.Entity(entity => + { + entity.HasOne(i => i.Property) + .WithMany() + .HasForeignKey(i => i.PropertyId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(i => i.Lease) + .WithMany() + .HasForeignKey(i => i.LeaseId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(i => i.Document) + .WithMany() + .HasForeignKey(i => i.DocumentId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasIndex(e => e.PropertyId); + entity.HasIndex(e => e.CompletedOn); + }); + + // Configure MaintenanceRequest entity + modelBuilder.Entity(entity => + { + entity.HasOne(m => m.Property) + .WithMany() + .HasForeignKey(m => m.PropertyId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(m => m.Lease) + .WithMany() + .HasForeignKey(m => m.LeaseId) + .OnDelete(DeleteBehavior.SetNull); + + entity.Property(e => e.EstimatedCost).HasPrecision(18, 2); + entity.Property(e => e.ActualCost).HasPrecision(18, 2); + + entity.HasIndex(e => e.PropertyId); + entity.HasIndex(e => e.Status); + entity.HasIndex(e => e.Priority); + entity.HasIndex(e => e.RequestedOn); + }); + + // Configure OrganizationSettings entity + modelBuilder.Entity(entity => + { + entity.Property(e => e.OrganizationId).HasConversion(); + entity.HasIndex(e => e.OrganizationId).IsUnique(); + entity.Property(e => e.LateFeePercentage).HasPrecision(5, 4); + entity.Property(e => e.MaxLateFeeAmount).HasPrecision(18, 2); + entity.Property(e => e.DefaultApplicationFee).HasPrecision(18, 2); + entity.Property(e => e.OrganizationSharePercentage).HasPrecision(18, 6); + entity.Property(e => e.SecurityDepositMultiplier).HasPrecision(18, 2); + }); + + // Configure ChecklistTemplate entity + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.Category); + }); + + // Configure ChecklistTemplateItem entity + modelBuilder.Entity(entity => + { + entity.HasOne(cti => cti.ChecklistTemplate) + .WithMany(ct => ct.Items) + .HasForeignKey(cti => cti.ChecklistTemplateId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasIndex(e => e.ChecklistTemplateId); + }); + + // Configure Checklist entity + modelBuilder.Entity(entity => + { + entity.HasOne(c => c.Property) + .WithMany() + .HasForeignKey(c => c.PropertyId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(c => c.Lease) + .WithMany() + .HasForeignKey(c => c.LeaseId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(c => c.ChecklistTemplate) + .WithMany(ct => ct.Checklists) + .HasForeignKey(c => c.ChecklistTemplateId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(c => c.Document) + .WithMany() + .HasForeignKey(c => c.DocumentId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasIndex(e => e.PropertyId); + entity.HasIndex(e => e.LeaseId); + entity.HasIndex(e => e.ChecklistType); + entity.HasIndex(e => e.Status); + entity.HasIndex(e => e.CompletedOn); + }); + + // Configure ChecklistItem entity + modelBuilder.Entity(entity => + { + entity.HasOne(ci => ci.Checklist) + .WithMany(c => c.Items) + .HasForeignKey(ci => ci.ChecklistId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasIndex(e => e.ChecklistId); + }); + + // Configure ProspectiveTenant entity + modelBuilder.Entity(entity => + { + entity.HasOne(pt => pt.InterestedProperty) + .WithMany() + .HasForeignKey(pt => pt.InterestedPropertyId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasIndex(e => e.Email); + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.Status); + }); + + // Configure Tour entity + modelBuilder.Entity(entity => + { + entity.HasOne(s => s.ProspectiveTenant) + .WithMany(pt => pt.Tours) + .HasForeignKey(s => s.ProspectiveTenantId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(s => s.Property) + .WithMany() + .HasForeignKey(s => s.PropertyId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.ScheduledOn); + entity.HasIndex(e => e.Status); + }); + + // Configure RentalApplication entity + // A prospect may have multiple applications over time, but only one "active" application at a time. + // Active = not yet disposed (not approved/denied/withdrawn/expired/lease-declined) + modelBuilder.Entity(entity => + { + entity.HasOne(ra => ra.ProspectiveTenant) + .WithMany(pt => pt.Applications) + .HasForeignKey(ra => ra.ProspectiveTenantId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(ra => ra.Property) + .WithMany() + .HasForeignKey(ra => ra.PropertyId) + .OnDelete(DeleteBehavior.Restrict); + + entity.Property(e => e.CurrentRent).HasPrecision(18, 2); + entity.Property(e => e.MonthlyIncome).HasPrecision(18, 2); + entity.Property(e => e.ApplicationFee).HasPrecision(18, 2); + + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.AppliedOn); + entity.HasIndex(e => e.Status); + }); + + // Configure ApplicationScreening entity + modelBuilder.Entity(entity => + { + entity.HasOne(asc => asc.RentalApplication) + .WithOne(ra => ra.Screening) + .HasForeignKey(asc => asc.RentalApplicationId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.OverallResult); + }); + + // Configure CalendarEvent entity + modelBuilder.Entity(entity => + { + entity.HasOne(ce => ce.Property) + .WithMany() + .HasForeignKey(ce => ce.PropertyId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.StartOn); + entity.HasIndex(e => e.EventType); + entity.HasIndex(e => e.SourceEntityId); + entity.HasIndex(e => new { e.SourceEntityType, e.SourceEntityId }); + }); + + // Configure CalendarSettings entity + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => new { e.OrganizationId, e.EntityType }).IsUnique(); + }); + + // Configure SecurityDeposit entity + modelBuilder.Entity(entity => + { + entity.HasOne(sd => sd.Lease) + .WithMany() + .HasForeignKey(sd => sd.LeaseId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(sd => sd.Tenant) + .WithMany() + .HasForeignKey(sd => sd.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + entity.Property(e => e.Amount).HasPrecision(18, 2); + entity.Property(e => e.RefundAmount).HasPrecision(18, 2); + entity.Property(e => e.DeductionsAmount).HasPrecision(18, 2); + + entity.HasIndex(e => e.LeaseId).IsUnique(); + entity.HasIndex(e => e.TenantId); + entity.HasIndex(e => e.Status); + entity.HasIndex(e => e.InInvestmentPool); + }); + + // Configure SecurityDepositInvestmentPool entity + modelBuilder.Entity(entity => + { + entity.Property(e => e.StartingBalance).HasPrecision(18, 2); + entity.Property(e => e.EndingBalance).HasPrecision(18, 2); + entity.Property(e => e.TotalEarnings).HasPrecision(18, 2); + entity.Property(e => e.ReturnRate).HasPrecision(18, 6); + entity.Property(e => e.OrganizationSharePercentage).HasPrecision(18, 6); + entity.Property(e => e.OrganizationShare).HasPrecision(18, 2); + entity.Property(e => e.TenantShareTotal).HasPrecision(18, 2); + entity.Property(e => e.DividendPerLease).HasPrecision(18, 2); + + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.Year).IsUnique(); + entity.HasIndex(e => e.Status); + }); + + // Configure SecurityDepositDividend entity + modelBuilder.Entity(entity => + { + entity.HasOne(sdd => sdd.SecurityDeposit) + .WithMany(sd => sd.Dividends) + .HasForeignKey(sdd => sdd.SecurityDepositId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(sdd => sdd.InvestmentPool) + .WithMany(ip => ip.Dividends) + .HasForeignKey(sdd => sdd.InvestmentPoolId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(sdd => sdd.Lease) + .WithMany() + .HasForeignKey(sdd => sdd.LeaseId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(sdd => sdd.Tenant) + .WithMany() + .HasForeignKey(sdd => sdd.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + entity.Property(e => e.BaseDividendAmount).HasPrecision(18, 2); + entity.Property(e => e.ProrationFactor).HasPrecision(18, 6); + entity.Property(e => e.DividendAmount).HasPrecision(18, 2); + + entity.HasIndex(e => e.SecurityDepositId); + entity.HasIndex(e => e.InvestmentPoolId); + entity.HasIndex(e => e.LeaseId); + entity.HasIndex(e => e.TenantId); + entity.HasIndex(e => e.Year); + entity.HasIndex(e => e.Status); + }); + + // Configure Organization entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.HasIndex(e => e.OwnerId); + entity.HasIndex(e => e.IsActive); + + // Owner relationship + entity.HasOne() + .WithMany() + .HasForeignKey(e => e.OwnerId) + .OnDelete(DeleteBehavior.Restrict); + }); + + // Configure UserOrganization entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.HasOne(uo => uo.Organization) + .WithMany(o => o.UserOrganizations) + .HasForeignKey(uo => uo.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne() + .WithMany() + .HasForeignKey(uo => uo.UserId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne() + .WithMany() + .HasForeignKey(uo => uo.GrantedBy) + .OnDelete(DeleteBehavior.Restrict); + + // Unique constraint: one role per user per organization + entity.HasIndex(e => new { e.UserId, e.OrganizationId }).IsUnique(); + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.Role); + entity.HasIndex(e => e.IsActive); + }); + + // Configure WorkflowAuditLog entity + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.EntityType); + entity.HasIndex(e => e.EntityId); + entity.HasIndex(e => new { e.EntityType, e.EntityId }); + entity.HasIndex(e => e.Action); + entity.HasIndex(e => e.PerformedOn); + entity.HasIndex(e => e.PerformedBy); + }); + + // Configure Notification entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.HasIndex(e => e.RecipientUserId); + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.SentOn); + entity.HasIndex(e => e.IsRead); + entity.HasIndex(e => e.Category); + + // Organization relationship + entity.HasOne(n => n.Organization) + .WithMany() + .HasForeignKey(n => n.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + + // User relationship (RecipientUserId) + entity.HasOne() + .WithMany() + .HasForeignKey(n => n.RecipientUserId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Configure NotificationPreferences entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.HasIndex(e => e.UserId); + entity.HasIndex(e => e.OrganizationId); + + // Unique constraint: one preference record per user per organization + entity.HasIndex(e => new { e.UserId, e.OrganizationId }) + .IsUnique(); + + // Organization relationship + entity.HasOne(np => np.Organization) + .WithMany() + .HasForeignKey(np => np.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + + // User relationship + entity.HasOne() + .WithMany() + .HasForeignKey(np => np.UserId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Configure OrganizationEmailSettings entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.HasIndex(e => e.OrganizationId).IsUnique(); + + // Organization relationship - one settings record per organization + entity.HasOne(es => es.Organization) + .WithMany() + .HasForeignKey(es => es.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + + }); + + // Configure OrganizationSMSSettings entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + entity.HasIndex(e => e.OrganizationId).IsUnique(); + + // Organization relationship - one settings record per organization + entity.HasOne(ss => ss.Organization) + .WithMany() + .HasForeignKey(ss => ss.OrganizationId) + .OnDelete(DeleteBehavior.Cascade); + + // Precision for financial fields + entity.Property(e => e.AccountBalance).HasPrecision(18, 2); + entity.Property(e => e.CostPerSMS).HasPrecision(18, 4); + }); + + // Seed System Checklist Templates + SeedChecklistTemplates(modelBuilder); + } + + private void SeedChecklistTemplates(ModelBuilder modelBuilder) + { + var systemTimestamp = DateTime.Parse("2025-11-30T00:00:00Z").ToUniversalTime(); + + // Fixed GUIDs for system templates (consistent across deployments) + var propertyTourTemplateId = Guid.Parse("00000000-0000-0000-0001-000000000001"); + var moveInTemplateId = Guid.Parse("00000000-0000-0000-0001-000000000002"); + var moveOutTemplateId = Guid.Parse("00000000-0000-0000-0001-000000000003"); + var openHouseTemplateId = Guid.Parse("00000000-0000-0000-0001-000000000004"); + + // Seed ChecklistTemplates + modelBuilder.Entity().HasData( + new ChecklistTemplate + { + Id = propertyTourTemplateId, + Name = "Property Tour", + Description = "Standard property showing checklist", + Category = "Tour", + IsSystemTemplate = true, + OrganizationId = Guid.Empty, + CreatedOn = systemTimestamp, + CreatedBy = string.Empty, + IsDeleted = false + }, + new ChecklistTemplate + { + Id = moveInTemplateId, + Name = "Move-In", + Description = "Move-in inspection checklist", + Category = "MoveIn", + IsSystemTemplate = true, + OrganizationId = Guid.Empty, + CreatedOn = systemTimestamp, + CreatedBy = string.Empty, + IsDeleted = false + }, + new ChecklistTemplate + { + Id = moveOutTemplateId, + Name = "Move-Out", + Description = "Move-out inspection checklist", + Category = "MoveOut", + IsSystemTemplate = true, + OrganizationId = Guid.Empty, + CreatedOn = systemTimestamp, + CreatedBy = string.Empty, + IsDeleted = false + }, + new ChecklistTemplate + { + Id = openHouseTemplateId, + Name = "Open House", + Description = "Open house event checklist", + Category = "Tour", + IsSystemTemplate = true, + OrganizationId = Guid.Empty, + CreatedOn = systemTimestamp, + CreatedBy = string.Empty, + IsDeleted = false + } + ); + + // Seed Property Tour Checklist Items + modelBuilder.Entity().HasData( + // Arrival & Introduction (Section 1) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000001"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Greeted prospect and verified appointment", ItemOrder = 1, CategorySection = "Arrival & Introduction", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000002"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Reviewed property exterior and curb appeal", ItemOrder = 2, CategorySection = "Arrival & Introduction", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000003"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed parking area/garage", ItemOrder = 3, CategorySection = "Arrival & Introduction", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + + // Interior Tour (Section 2) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000004"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Toured living room/common areas", ItemOrder = 4, CategorySection = "Interior Tour", SectionOrder = 2, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000005"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed all bedrooms", ItemOrder = 5, CategorySection = "Interior Tour", SectionOrder = 2, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000006"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed all bathrooms", ItemOrder = 6, CategorySection = "Interior Tour", SectionOrder = 2, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + + // Kitchen & Appliances (Section 3) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000007"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Toured kitchen and demonstrated appliances", ItemOrder = 7, CategorySection = "Kitchen & Appliances", SectionOrder = 3, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000008"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained which appliances are included", ItemOrder = 8, CategorySection = "Kitchen & Appliances", SectionOrder = 3, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + // Utilities & Systems (Section 4) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000009"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained HVAC system and thermostat controls", ItemOrder = 9, CategorySection = "Utilities & Systems", SectionOrder = 4, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000010"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Reviewed utility responsibilities (tenant vs landlord)", ItemOrder = 10, CategorySection = "Utilities & Systems", SectionOrder = 4, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000011"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed water heater location", ItemOrder = 11, CategorySection = "Utilities & Systems", SectionOrder = 4, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + // Storage & Amenities (Section 5) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000012"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed storage areas (closets, attic, basement)", ItemOrder = 12, CategorySection = "Storage & Amenities", SectionOrder = 5, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000013"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed laundry facilities", ItemOrder = 13, CategorySection = "Storage & Amenities", SectionOrder = 5, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000014"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed outdoor space (yard, patio, balcony)", ItemOrder = 14, CategorySection = "Storage & Amenities", SectionOrder = 5, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + // Lease Terms (Section 6) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000015"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Discussed monthly rent amount", ItemOrder = 15, CategorySection = "Lease Terms", SectionOrder = 6, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000016"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained security deposit and move-in costs", ItemOrder = 16, CategorySection = "Lease Terms", SectionOrder = 6, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000017"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Reviewed lease term length and start date", ItemOrder = 17, CategorySection = "Lease Terms", SectionOrder = 6, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000018"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained pet policy", ItemOrder = 18, CategorySection = "Lease Terms", SectionOrder = 6, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + + // Next Steps (Section 7) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000019"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained application process and requirements", ItemOrder = 19, CategorySection = "Next Steps", SectionOrder = 7, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000020"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Reviewed screening process (background, credit check)", ItemOrder = 20, CategorySection = "Next Steps", SectionOrder = 7, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000021"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Answered all prospect questions", ItemOrder = 21, CategorySection = "Next Steps", SectionOrder = 7, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + + // Assessment (Section 8) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000022"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Prospect Interest Level", ItemOrder = 22, CategorySection = "Assessment", SectionOrder = 8, IsRequired = true, RequiresValue = true, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000023"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Overall showing feedback and notes", ItemOrder = 23, CategorySection = "Assessment", SectionOrder = 8, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + // Move-In Checklist Items (Placeholders) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000024"), ChecklistTemplateId = moveInTemplateId, ItemText = "Document property condition", ItemOrder = 1, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000025"), ChecklistTemplateId = moveInTemplateId, ItemText = "Collect keys and access codes", ItemOrder = 2, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000026"), ChecklistTemplateId = moveInTemplateId, ItemText = "Review lease terms with tenant", ItemOrder = 3, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + // Move-Out Checklist Items (Placeholders) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000027"), ChecklistTemplateId = moveOutTemplateId, ItemText = "Inspect property condition", ItemOrder = 1, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000028"), ChecklistTemplateId = moveOutTemplateId, ItemText = "Collect all keys and access devices", ItemOrder = 2, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000029"), ChecklistTemplateId = moveOutTemplateId, ItemText = "Document damages and needed repairs", ItemOrder = 3, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + // Open House Checklist Items (Placeholders) + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000030"), ChecklistTemplateId = openHouseTemplateId, ItemText = "Set up signage and directional markers", ItemOrder = 1, CategorySection = "Preparation", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000031"), ChecklistTemplateId = openHouseTemplateId, ItemText = "Prepare information packets", ItemOrder = 2, CategorySection = "Preparation", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }, + new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000032"), ChecklistTemplateId = openHouseTemplateId, ItemText = "Set up visitor sign-in sheet", ItemOrder = 3, CategorySection = "Preparation", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false } + ); + } + + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251209234246_InitialCreate.Designer.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251209234246_InitialCreate.Designer.cs new file mode 100644 index 0000000..077b9a1 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251209234246_InitialCreate.Designer.cs @@ -0,0 +1,3914 @@ +// +using System; +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251209234246_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.Professional.Application.Services.Workflows.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreditCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreditScore") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallResult") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("ResultNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OverallResult"); + + b.HasIndex("RentalApplicationId") + .IsUnique(); + + b.ToTable("ApplicationScreenings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("EndOn") + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StartOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 11, + ItemText = "Showed water heater location", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 18, + ItemText = "Explained pet policy", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Document property condition", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Inspect property condition", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Prepare information packets", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrantedBy"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Shared.Components.Account.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginIP") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("LoginCount") + .HasColumnType("INTEGER"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PreviousLoginDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Professional.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", "User") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithOne("Application") + .HasForeignKey("Aquiis.Professional.Core.Entities.RentalApplication", "ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("GrantedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany("UserOrganizations") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + + b.Navigation("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Application"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251209234246_InitialCreate.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251209234246_InitialCreate.cs new file mode 100644 index 0000000..11fb45b --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251209234246_InitialCreate.cs @@ -0,0 +1,2076 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Aquiis.Professional.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + ActiveOrganizationId = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + FirstName = table.Column(type: "TEXT", nullable: false), + LastName = table.Column(type: "TEXT", nullable: false), + LastLoginDate = table.Column(type: "TEXT", nullable: true), + PreviousLoginDate = table.Column(type: "TEXT", nullable: true), + LoginCount = table.Column(type: "INTEGER", nullable: false), + LastLoginIP = table.Column(type: "TEXT", nullable: true), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CalendarSettings", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + EntityType = table.Column(type: "TEXT", nullable: false), + AutoCreateEvents = table.Column(type: "INTEGER", nullable: false), + ShowOnCalendar = table.Column(type: "INTEGER", nullable: false), + DefaultColor = table.Column(type: "TEXT", nullable: true), + DefaultIcon = table.Column(type: "TEXT", nullable: true), + DisplayOrder = table.Column(type: "INTEGER", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CalendarSettings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ChecklistTemplates", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Category = table.Column(type: "TEXT", maxLength: 50, nullable: false), + IsSystemTemplate = table.Column(type: "INTEGER", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChecklistTemplates", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OrganizationSettings", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: true), + LateFeeEnabled = table.Column(type: "INTEGER", nullable: false), + LateFeeAutoApply = table.Column(type: "INTEGER", nullable: false), + LateFeeGracePeriodDays = table.Column(type: "INTEGER", nullable: false), + LateFeePercentage = table.Column(type: "TEXT", precision: 5, scale: 4, nullable: false), + MaxLateFeeAmount = table.Column(type: "TEXT", precision: 18, scale: 2, nullable: false), + PaymentReminderEnabled = table.Column(type: "INTEGER", nullable: false), + PaymentReminderDaysBefore = table.Column(type: "INTEGER", nullable: false), + TourNoShowGracePeriodHours = table.Column(type: "INTEGER", nullable: false), + ApplicationFeeEnabled = table.Column(type: "INTEGER", nullable: false), + DefaultApplicationFee = table.Column(type: "TEXT", precision: 18, scale: 2, nullable: false), + ApplicationExpirationDays = table.Column(type: "INTEGER", nullable: false), + SecurityDepositInvestmentEnabled = table.Column(type: "INTEGER", nullable: false), + OrganizationSharePercentage = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + AutoCalculateSecurityDeposit = table.Column(type: "INTEGER", nullable: false), + SecurityDepositMultiplier = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + RefundProcessingDays = table.Column(type: "INTEGER", nullable: false), + DividendDistributionMonth = table.Column(type: "INTEGER", nullable: false), + AllowTenantDividendChoice = table.Column(type: "INTEGER", nullable: false), + DefaultDividendPaymentMethod = table.Column(type: "TEXT", maxLength: 50, nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationSettings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "SchemaVersions", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Version = table.Column(type: "TEXT", maxLength: 50, nullable: false), + AppliedOn = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SchemaVersions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "SecurityDepositInvestmentPools", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Year = table.Column(type: "INTEGER", nullable: false), + StartingBalance = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + EndingBalance = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + TotalEarnings = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + ReturnRate = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + OrganizationSharePercentage = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + OrganizationShare = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + TenantShareTotal = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + ActiveLeaseCount = table.Column(type: "INTEGER", nullable: false), + DividendPerLease = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + DividendsCalculatedOn = table.Column(type: "TEXT", nullable: true), + DividendsDistributedOn = table.Column(type: "TEXT", nullable: true), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Notes = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SecurityDepositInvestmentPools", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "WorkflowAuditLogs", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + EntityType = table.Column(type: "TEXT", nullable: false), + EntityId = table.Column(type: "TEXT", nullable: false), + FromStatus = table.Column(type: "TEXT", nullable: true), + ToStatus = table.Column(type: "TEXT", nullable: false), + Action = table.Column(type: "TEXT", nullable: false), + Reason = table.Column(type: "TEXT", nullable: true), + PerformedBy = table.Column(type: "TEXT", nullable: false), + PerformedOn = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Metadata = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WorkflowAuditLogs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "TEXT", nullable: false), + ProviderKey = table.Column(type: "TEXT", nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Notes", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Content = table.Column(type: "TEXT", maxLength: 5000, nullable: false), + EntityType = table.Column(type: "TEXT", maxLength: 100, nullable: false), + EntityId = table.Column(type: "TEXT", nullable: false), + UserFullName = table.Column(type: "TEXT", maxLength: 100, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Notes", x => x.Id); + table.ForeignKey( + name: "FK_Notes_AspNetUsers_CreatedBy", + column: x => x.CreatedBy, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Organizations", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 100, nullable: false), + OwnerId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: true), + State = table.Column(type: "TEXT", nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false), + CreatedBy = table.Column(type: "TEXT", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + LastModifiedBy = table.Column(type: "TEXT", nullable: true), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Organizations", x => x.Id); + table.ForeignKey( + name: "FK_Organizations_AspNetUsers_OwnerId", + column: x => x.OwnerId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ChecklistTemplateItems", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + ChecklistTemplateId = table.Column(type: "TEXT", nullable: false), + ItemText = table.Column(type: "TEXT", maxLength: 500, nullable: false), + ItemOrder = table.Column(type: "INTEGER", nullable: false), + CategorySection = table.Column(type: "TEXT", maxLength: 100, nullable: true), + SectionOrder = table.Column(type: "INTEGER", nullable: false), + IsRequired = table.Column(type: "INTEGER", nullable: false), + RequiresValue = table.Column(type: "INTEGER", nullable: false), + AllowsNotes = table.Column(type: "INTEGER", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChecklistTemplateItems", x => x.Id); + table.ForeignKey( + name: "FK_ChecklistTemplateItems_ChecklistTemplates_ChecklistTemplateId", + column: x => x.ChecklistTemplateId, + principalTable: "ChecklistTemplates", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Properties", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Address = table.Column(type: "TEXT", maxLength: 200, nullable: false), + UnitNumber = table.Column(type: "TEXT", maxLength: 50, nullable: true), + City = table.Column(type: "TEXT", maxLength: 100, nullable: false), + State = table.Column(type: "TEXT", maxLength: 50, nullable: false), + ZipCode = table.Column(type: "TEXT", maxLength: 10, nullable: false), + PropertyType = table.Column(type: "TEXT", maxLength: 50, nullable: false), + MonthlyRent = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + Bedrooms = table.Column(type: "INTEGER", maxLength: 3, nullable: false), + Bathrooms = table.Column(type: "decimal(3,1)", maxLength: 3, nullable: false), + SquareFeet = table.Column(type: "INTEGER", maxLength: 7, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + IsAvailable = table.Column(type: "INTEGER", nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + LastRoutineInspectionDate = table.Column(type: "TEXT", nullable: true), + NextRoutineInspectionDueDate = table.Column(type: "TEXT", nullable: true), + RoutineInspectionIntervalMonths = table.Column(type: "INTEGER", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Properties", x => x.Id); + table.ForeignKey( + name: "FK_Properties_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Tenants", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + FirstName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + IdentificationNumber = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Email = table.Column(type: "TEXT", maxLength: 255, nullable: false), + PhoneNumber = table.Column(type: "TEXT", maxLength: 20, nullable: false), + DateOfBirth = table.Column(type: "TEXT", nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false), + EmergencyContactName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + EmergencyContactPhone = table.Column(type: "TEXT", maxLength: 20, nullable: true), + Notes = table.Column(type: "TEXT", maxLength: 500, nullable: false), + ProspectiveTenantId = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tenants", x => x.Id); + table.ForeignKey( + name: "FK_Tenants_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "UserOrganizations", + columns: table => new + { + Id = table.Column(type: "TEXT", maxLength: 100, nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Role = table.Column(type: "TEXT", nullable: false), + GrantedBy = table.Column(type: "TEXT", nullable: false), + GrantedOn = table.Column(type: "TEXT", nullable: false), + RevokedOn = table.Column(type: "TEXT", nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false), + CreatedBy = table.Column(type: "TEXT", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + LastModifiedBy = table.Column(type: "TEXT", nullable: true), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserOrganizations", x => x.Id); + table.ForeignKey( + name: "FK_UserOrganizations_AspNetUsers_GrantedBy", + column: x => x.GrantedBy, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_UserOrganizations_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserOrganizations_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CalendarEvents", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), + StartOn = table.Column(type: "TEXT", nullable: false), + EndOn = table.Column(type: "TEXT", nullable: true), + DurationMinutes = table.Column(type: "INTEGER", nullable: false), + EventType = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + PropertyId = table.Column(type: "TEXT", nullable: true), + Location = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Color = table.Column(type: "TEXT", maxLength: 20, nullable: false), + Icon = table.Column(type: "TEXT", maxLength: 50, nullable: false), + SourceEntityId = table.Column(type: "TEXT", nullable: true), + SourceEntityType = table.Column(type: "TEXT", maxLength: 100, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CalendarEvents", x => x.Id); + table.ForeignKey( + name: "FK_CalendarEvents_Properties_PropertyId", + column: x => x.PropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "ProspectiveTenants", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + FirstName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Email = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Phone = table.Column(type: "TEXT", maxLength: 20, nullable: false), + DateOfBirth = table.Column(type: "TEXT", nullable: true), + IdentificationNumber = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IdentificationState = table.Column(type: "TEXT", maxLength: 2, nullable: true), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Source = table.Column(type: "TEXT", maxLength: 100, nullable: true), + Notes = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + InterestedPropertyId = table.Column(type: "TEXT", nullable: true), + DesiredMoveInDate = table.Column(type: "TEXT", nullable: true), + FirstContactedOn = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProspectiveTenants", x => x.Id); + table.ForeignKey( + name: "FK_ProspectiveTenants_Properties_InterestedPropertyId", + column: x => x.InterestedPropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "RentalApplications", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + ProspectiveTenantId = table.Column(type: "TEXT", nullable: false), + PropertyId = table.Column(type: "TEXT", nullable: false), + AppliedOn = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + CurrentAddress = table.Column(type: "TEXT", maxLength: 200, nullable: false), + CurrentCity = table.Column(type: "TEXT", maxLength: 100, nullable: false), + CurrentState = table.Column(type: "TEXT", maxLength: 2, nullable: false), + CurrentZipCode = table.Column(type: "TEXT", maxLength: 10, nullable: false), + CurrentRent = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + LandlordName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + LandlordPhone = table.Column(type: "TEXT", maxLength: 20, nullable: false), + EmployerName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + JobTitle = table.Column(type: "TEXT", maxLength: 100, nullable: false), + MonthlyIncome = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + EmploymentLengthMonths = table.Column(type: "INTEGER", nullable: false), + Reference1Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Reference1Phone = table.Column(type: "TEXT", maxLength: 20, nullable: false), + Reference1Relationship = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Reference2Name = table.Column(type: "TEXT", maxLength: 200, nullable: true), + Reference2Phone = table.Column(type: "TEXT", maxLength: 20, nullable: true), + Reference2Relationship = table.Column(type: "TEXT", maxLength: 100, nullable: true), + ApplicationFee = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + ApplicationFeePaid = table.Column(type: "INTEGER", nullable: false), + ApplicationFeePaidOn = table.Column(type: "TEXT", nullable: true), + ApplicationFeePaymentMethod = table.Column(type: "TEXT", maxLength: 50, nullable: true), + ExpiresOn = table.Column(type: "TEXT", nullable: true), + DenialReason = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + DecidedOn = table.Column(type: "TEXT", nullable: true), + DecisionBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RentalApplications", x => x.Id); + table.ForeignKey( + name: "FK_RentalApplications_Properties_PropertyId", + column: x => x.PropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_RentalApplications_ProspectiveTenants_ProspectiveTenantId", + column: x => x.ProspectiveTenantId, + principalTable: "ProspectiveTenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ApplicationScreenings", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + RentalApplicationId = table.Column(type: "TEXT", nullable: false), + BackgroundCheckRequested = table.Column(type: "INTEGER", nullable: false), + BackgroundCheckRequestedOn = table.Column(type: "TEXT", nullable: true), + BackgroundCheckPassed = table.Column(type: "INTEGER", nullable: true), + BackgroundCheckCompletedOn = table.Column(type: "TEXT", nullable: true), + BackgroundCheckNotes = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + CreditCheckRequested = table.Column(type: "INTEGER", nullable: false), + CreditCheckRequestedOn = table.Column(type: "TEXT", nullable: true), + CreditScore = table.Column(type: "INTEGER", nullable: true), + CreditCheckPassed = table.Column(type: "INTEGER", nullable: true), + CreditCheckCompletedOn = table.Column(type: "TEXT", nullable: true), + CreditCheckNotes = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + OverallResult = table.Column(type: "TEXT", maxLength: 50, nullable: false), + ResultNotes = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationScreenings", x => x.Id); + table.ForeignKey( + name: "FK_ApplicationScreenings_RentalApplications_RentalApplicationId", + column: x => x.RentalApplicationId, + principalTable: "RentalApplications", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "LeaseOffers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + RentalApplicationId = table.Column(type: "TEXT", nullable: false), + PropertyId = table.Column(type: "TEXT", nullable: false), + ProspectiveTenantId = table.Column(type: "TEXT", nullable: false), + StartDate = table.Column(type: "TEXT", nullable: false), + EndDate = table.Column(type: "TEXT", nullable: false), + MonthlyRent = table.Column(type: "decimal(18,2)", nullable: false), + SecurityDeposit = table.Column(type: "decimal(18,2)", nullable: false), + Terms = table.Column(type: "TEXT", maxLength: 2000, nullable: false), + Notes = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + OfferedOn = table.Column(type: "TEXT", nullable: false), + ExpiresOn = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + RespondedOn = table.Column(type: "TEXT", nullable: true), + ResponseNotes = table.Column(type: "TEXT", maxLength: 500, nullable: true), + ConvertedLeaseId = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LeaseOffers", x => x.Id); + table.ForeignKey( + name: "FK_LeaseOffers_Properties_PropertyId", + column: x => x.PropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_LeaseOffers_ProspectiveTenants_ProspectiveTenantId", + column: x => x.ProspectiveTenantId, + principalTable: "ProspectiveTenants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_LeaseOffers_RentalApplications_RentalApplicationId", + column: x => x.RentalApplicationId, + principalTable: "RentalApplications", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ChecklistItems", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + ChecklistId = table.Column(type: "TEXT", nullable: false), + ItemText = table.Column(type: "TEXT", maxLength: 500, nullable: false), + ItemOrder = table.Column(type: "INTEGER", nullable: false), + CategorySection = table.Column(type: "TEXT", maxLength: 100, nullable: true), + SectionOrder = table.Column(type: "INTEGER", nullable: false), + RequiresValue = table.Column(type: "INTEGER", nullable: false), + Value = table.Column(type: "TEXT", maxLength: 200, nullable: true), + Notes = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + PhotoUrl = table.Column(type: "TEXT", maxLength: 500, nullable: true), + IsChecked = table.Column(type: "INTEGER", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChecklistItems", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Checklists", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + PropertyId = table.Column(type: "TEXT", nullable: true), + LeaseId = table.Column(type: "TEXT", nullable: true), + ChecklistTemplateId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + ChecklistType = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + CompletedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + CompletedOn = table.Column(type: "TEXT", nullable: true), + DocumentId = table.Column(type: "TEXT", nullable: true), + GeneralNotes = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Checklists", x => x.Id); + table.ForeignKey( + name: "FK_Checklists_ChecklistTemplates_ChecklistTemplateId", + column: x => x.ChecklistTemplateId, + principalTable: "ChecklistTemplates", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Checklists_Properties_PropertyId", + column: x => x.PropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Tours", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + ProspectiveTenantId = table.Column(type: "TEXT", nullable: false), + PropertyId = table.Column(type: "TEXT", nullable: false), + ScheduledOn = table.Column(type: "TEXT", nullable: false), + DurationMinutes = table.Column(type: "INTEGER", nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Feedback = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + InterestLevel = table.Column(type: "TEXT", maxLength: 50, nullable: true), + ConductedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + ChecklistId = table.Column(type: "TEXT", nullable: true), + CalendarEventId = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tours", x => x.Id); + table.ForeignKey( + name: "FK_Tours_Checklists_ChecklistId", + column: x => x.ChecklistId, + principalTable: "Checklists", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Tours_Properties_PropertyId", + column: x => x.PropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Tours_ProspectiveTenants_ProspectiveTenantId", + column: x => x.ProspectiveTenantId, + principalTable: "ProspectiveTenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Documents", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + FileName = table.Column(type: "TEXT", maxLength: 255, nullable: false), + FileExtension = table.Column(type: "TEXT", maxLength: 10, nullable: false), + FileData = table.Column(type: "BLOB", nullable: false), + FilePath = table.Column(type: "TEXT", maxLength: 255, nullable: false), + ContentType = table.Column(type: "TEXT", maxLength: 500, nullable: false), + FileType = table.Column(type: "TEXT", maxLength: 100, nullable: false), + FileSize = table.Column(type: "INTEGER", nullable: false), + DocumentType = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: false), + PropertyId = table.Column(type: "TEXT", nullable: true), + TenantId = table.Column(type: "TEXT", nullable: true), + LeaseId = table.Column(type: "TEXT", nullable: true), + InvoiceId = table.Column(type: "TEXT", nullable: true), + PaymentId = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Documents", x => x.Id); + table.ForeignKey( + name: "FK_Documents_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Documents_Properties_PropertyId", + column: x => x.PropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Documents_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "Leases", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + PropertyId = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: false), + LeaseOfferId = table.Column(type: "TEXT", nullable: true), + StartDate = table.Column(type: "TEXT", nullable: false), + EndDate = table.Column(type: "TEXT", nullable: false), + MonthlyRent = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + SecurityDeposit = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Terms = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + Notes = table.Column(type: "TEXT", maxLength: 500, nullable: false), + OfferedOn = table.Column(type: "TEXT", nullable: true), + SignedOn = table.Column(type: "TEXT", nullable: true), + DeclinedOn = table.Column(type: "TEXT", nullable: true), + ExpiresOn = table.Column(type: "TEXT", nullable: true), + RenewalNotificationSent = table.Column(type: "INTEGER", nullable: true), + RenewalNotificationSentOn = table.Column(type: "TEXT", nullable: true), + RenewalReminderSentOn = table.Column(type: "TEXT", nullable: true), + RenewalStatus = table.Column(type: "TEXT", maxLength: 50, nullable: true), + RenewalOfferedOn = table.Column(type: "TEXT", nullable: true), + RenewalResponseOn = table.Column(type: "TEXT", nullable: true), + ProposedRenewalRent = table.Column(type: "decimal(18,2)", nullable: true), + RenewalNotes = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + PreviousLeaseId = table.Column(type: "TEXT", nullable: true), + DocumentId = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Leases", x => x.Id); + table.ForeignKey( + name: "FK_Leases_Documents_DocumentId", + column: x => x.DocumentId, + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Leases_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Leases_Properties_PropertyId", + column: x => x.PropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Leases_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Inspections", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + PropertyId = table.Column(type: "TEXT", nullable: false), + CalendarEventId = table.Column(type: "TEXT", nullable: true), + LeaseId = table.Column(type: "TEXT", nullable: true), + CompletedOn = table.Column(type: "TEXT", nullable: false), + InspectionType = table.Column(type: "TEXT", maxLength: 50, nullable: false), + InspectedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + ExteriorRoofGood = table.Column(type: "INTEGER", nullable: false), + ExteriorRoofNotes = table.Column(type: "TEXT", nullable: true), + ExteriorGuttersGood = table.Column(type: "INTEGER", nullable: false), + ExteriorGuttersNotes = table.Column(type: "TEXT", nullable: true), + ExteriorSidingGood = table.Column(type: "INTEGER", nullable: false), + ExteriorSidingNotes = table.Column(type: "TEXT", nullable: true), + ExteriorWindowsGood = table.Column(type: "INTEGER", nullable: false), + ExteriorWindowsNotes = table.Column(type: "TEXT", nullable: true), + ExteriorDoorsGood = table.Column(type: "INTEGER", nullable: false), + ExteriorDoorsNotes = table.Column(type: "TEXT", nullable: true), + ExteriorFoundationGood = table.Column(type: "INTEGER", nullable: false), + ExteriorFoundationNotes = table.Column(type: "TEXT", nullable: true), + LandscapingGood = table.Column(type: "INTEGER", nullable: false), + LandscapingNotes = table.Column(type: "TEXT", nullable: true), + InteriorWallsGood = table.Column(type: "INTEGER", nullable: false), + InteriorWallsNotes = table.Column(type: "TEXT", nullable: true), + InteriorCeilingsGood = table.Column(type: "INTEGER", nullable: false), + InteriorCeilingsNotes = table.Column(type: "TEXT", nullable: true), + InteriorFloorsGood = table.Column(type: "INTEGER", nullable: false), + InteriorFloorsNotes = table.Column(type: "TEXT", nullable: true), + InteriorDoorsGood = table.Column(type: "INTEGER", nullable: false), + InteriorDoorsNotes = table.Column(type: "TEXT", nullable: true), + InteriorWindowsGood = table.Column(type: "INTEGER", nullable: false), + InteriorWindowsNotes = table.Column(type: "TEXT", nullable: true), + KitchenAppliancesGood = table.Column(type: "INTEGER", nullable: false), + KitchenAppliancesNotes = table.Column(type: "TEXT", nullable: true), + KitchenCabinetsGood = table.Column(type: "INTEGER", nullable: false), + KitchenCabinetsNotes = table.Column(type: "TEXT", nullable: true), + KitchenCountersGood = table.Column(type: "INTEGER", nullable: false), + KitchenCountersNotes = table.Column(type: "TEXT", nullable: true), + KitchenSinkPlumbingGood = table.Column(type: "INTEGER", nullable: false), + KitchenSinkPlumbingNotes = table.Column(type: "TEXT", nullable: true), + BathroomToiletGood = table.Column(type: "INTEGER", nullable: false), + BathroomToiletNotes = table.Column(type: "TEXT", nullable: true), + BathroomSinkGood = table.Column(type: "INTEGER", nullable: false), + BathroomSinkNotes = table.Column(type: "TEXT", nullable: true), + BathroomTubShowerGood = table.Column(type: "INTEGER", nullable: false), + BathroomTubShowerNotes = table.Column(type: "TEXT", nullable: true), + BathroomVentilationGood = table.Column(type: "INTEGER", nullable: false), + BathroomVentilationNotes = table.Column(type: "TEXT", nullable: true), + HvacSystemGood = table.Column(type: "INTEGER", nullable: false), + HvacSystemNotes = table.Column(type: "TEXT", nullable: true), + ElectricalSystemGood = table.Column(type: "INTEGER", nullable: false), + ElectricalSystemNotes = table.Column(type: "TEXT", nullable: true), + PlumbingSystemGood = table.Column(type: "INTEGER", nullable: false), + PlumbingSystemNotes = table.Column(type: "TEXT", nullable: true), + SmokeDetectorsGood = table.Column(type: "INTEGER", nullable: false), + SmokeDetectorsNotes = table.Column(type: "TEXT", nullable: true), + CarbonMonoxideDetectorsGood = table.Column(type: "INTEGER", nullable: false), + CarbonMonoxideDetectorsNotes = table.Column(type: "TEXT", nullable: true), + OverallCondition = table.Column(type: "TEXT", maxLength: 20, nullable: false), + GeneralNotes = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + ActionItemsRequired = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + DocumentId = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Inspections", x => x.Id); + table.ForeignKey( + name: "FK_Inspections_Documents_DocumentId", + column: x => x.DocumentId, + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Inspections_Leases_LeaseId", + column: x => x.LeaseId, + principalTable: "Leases", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Inspections_Properties_PropertyId", + column: x => x.PropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Invoices", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LeaseId = table.Column(type: "TEXT", nullable: false), + InvoiceNumber = table.Column(type: "TEXT", maxLength: 50, nullable: false), + InvoicedOn = table.Column(type: "TEXT", nullable: false), + DueOn = table.Column(type: "TEXT", nullable: false), + Amount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + PaidOn = table.Column(type: "TEXT", nullable: true), + AmountPaid = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + Notes = table.Column(type: "TEXT", maxLength: 500, nullable: false), + LateFeeAmount = table.Column(type: "decimal(18,2)", nullable: true), + LateFeeApplied = table.Column(type: "INTEGER", nullable: true), + LateFeeAppliedOn = table.Column(type: "TEXT", nullable: true), + ReminderSent = table.Column(type: "INTEGER", nullable: true), + ReminderSentOn = table.Column(type: "TEXT", nullable: true), + DocumentId = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Invoices", x => x.Id); + table.ForeignKey( + name: "FK_Invoices_Documents_DocumentId", + column: x => x.DocumentId, + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Invoices_Leases_LeaseId", + column: x => x.LeaseId, + principalTable: "Leases", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Invoices_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MaintenanceRequests", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + PropertyId = table.Column(type: "TEXT", nullable: false), + CalendarEventId = table.Column(type: "TEXT", nullable: true), + LeaseId = table.Column(type: "TEXT", nullable: true), + Title = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 2000, nullable: false), + RequestType = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Priority = table.Column(type: "TEXT", maxLength: 20, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 20, nullable: false), + RequestedBy = table.Column(type: "TEXT", maxLength: 500, nullable: false), + RequestedByEmail = table.Column(type: "TEXT", maxLength: 100, nullable: false), + RequestedByPhone = table.Column(type: "TEXT", maxLength: 20, nullable: false), + RequestedOn = table.Column(type: "TEXT", nullable: false), + ScheduledOn = table.Column(type: "TEXT", nullable: true), + CompletedOn = table.Column(type: "TEXT", nullable: true), + EstimatedCost = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + ActualCost = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + AssignedTo = table.Column(type: "TEXT", maxLength: 100, nullable: false), + ResolutionNotes = table.Column(type: "TEXT", maxLength: 2000, nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MaintenanceRequests", x => x.Id); + table.ForeignKey( + name: "FK_MaintenanceRequests_Leases_LeaseId", + column: x => x.LeaseId, + principalTable: "Leases", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_MaintenanceRequests_Properties_PropertyId", + column: x => x.PropertyId, + principalTable: "Properties", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "SecurityDeposits", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LeaseId = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: false), + Amount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + DateReceived = table.Column(type: "TEXT", nullable: false), + PaymentMethod = table.Column(type: "TEXT", maxLength: 50, nullable: false), + TransactionReference = table.Column(type: "TEXT", maxLength: 100, nullable: true), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + InInvestmentPool = table.Column(type: "INTEGER", nullable: false), + PoolEntryDate = table.Column(type: "TEXT", nullable: true), + PoolExitDate = table.Column(type: "TEXT", nullable: true), + RefundProcessedDate = table.Column(type: "TEXT", nullable: true), + RefundAmount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true), + DeductionsAmount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true), + DeductionsReason = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + RefundMethod = table.Column(type: "TEXT", maxLength: 50, nullable: true), + RefundReference = table.Column(type: "TEXT", maxLength: 100, nullable: true), + Notes = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SecurityDeposits", x => x.Id); + table.ForeignKey( + name: "FK_SecurityDeposits_Leases_LeaseId", + column: x => x.LeaseId, + principalTable: "Leases", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SecurityDeposits_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Payments", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + InvoiceId = table.Column(type: "TEXT", nullable: false), + PaidOn = table.Column(type: "TEXT", nullable: false), + Amount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + PaymentMethod = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Notes = table.Column(type: "TEXT", maxLength: 1000, nullable: false), + DocumentId = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Payments", x => x.Id); + table.ForeignKey( + name: "FK_Payments_Documents_DocumentId", + column: x => x.DocumentId, + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Payments_Invoices_InvoiceId", + column: x => x.InvoiceId, + principalTable: "Invoices", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Payments_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SecurityDepositDividends", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + SecurityDepositId = table.Column(type: "TEXT", nullable: false), + InvestmentPoolId = table.Column(type: "TEXT", nullable: false), + LeaseId = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: false), + Year = table.Column(type: "INTEGER", nullable: false), + BaseDividendAmount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + ProrationFactor = table.Column(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false), + DividendAmount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + PaymentMethod = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 50, nullable: false), + ChoiceMadeOn = table.Column(type: "TEXT", nullable: true), + PaymentProcessedOn = table.Column(type: "TEXT", nullable: true), + PaymentReference = table.Column(type: "TEXT", maxLength: 100, nullable: true), + MailingAddress = table.Column(type: "TEXT", maxLength: 500, nullable: true), + MonthsInPool = table.Column(type: "INTEGER", nullable: false), + Notes = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SecurityDepositDividends", x => x.Id); + table.ForeignKey( + name: "FK_SecurityDepositDividends_Leases_LeaseId", + column: x => x.LeaseId, + principalTable: "Leases", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SecurityDepositDividends_SecurityDepositInvestmentPools_InvestmentPoolId", + column: x => x.InvestmentPoolId, + principalTable: "SecurityDepositInvestmentPools", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SecurityDepositDividends_SecurityDeposits_SecurityDepositId", + column: x => x.SecurityDepositId, + principalTable: "SecurityDeposits", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SecurityDepositDividends_Tenants_TenantId", + column: x => x.TenantId, + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.InsertData( + table: "ChecklistTemplates", + columns: new[] { "Id", "Category", "CreatedBy", "CreatedOn", "Description", "IsDeleted", "IsSystemTemplate", "LastModifiedBy", "LastModifiedOn", "Name", "OrganizationId" }, + values: new object[,] + { + { new Guid("00000000-0000-0000-0001-000000000001"), "Tour", "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), "Standard property showing checklist", false, true, "", null, "Property Tour", new Guid("00000000-0000-0000-0000-000000000000") }, + { new Guid("00000000-0000-0000-0001-000000000002"), "MoveIn", "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), "Move-in inspection checklist", false, true, "", null, "Move-In", new Guid("00000000-0000-0000-0000-000000000000") }, + { new Guid("00000000-0000-0000-0001-000000000003"), "MoveOut", "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), "Move-out inspection checklist", false, true, "", null, "Move-Out", new Guid("00000000-0000-0000-0000-000000000000") }, + { new Guid("00000000-0000-0000-0001-000000000004"), "Tour", "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), "Open house event checklist", false, true, "", null, "Open House", new Guid("00000000-0000-0000-0000-000000000000") } + }); + + migrationBuilder.InsertData( + table: "ChecklistTemplateItems", + columns: new[] { "Id", "AllowsNotes", "CategorySection", "ChecklistTemplateId", "CreatedBy", "CreatedOn", "IsDeleted", "IsRequired", "ItemOrder", "ItemText", "LastModifiedBy", "LastModifiedOn", "OrganizationId", "RequiresValue", "SectionOrder" }, + values: new object[,] + { + { new Guid("00000000-0000-0000-0002-000000000001"), true, "Arrival & Introduction", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 1, "Greeted prospect and verified appointment", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000002"), true, "Arrival & Introduction", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 2, "Reviewed property exterior and curb appeal", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000003"), true, "Arrival & Introduction", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 3, "Showed parking area/garage", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000004"), true, "Interior Tour", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 4, "Toured living room/common areas", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 2 }, + { new Guid("00000000-0000-0000-0002-000000000005"), true, "Interior Tour", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 5, "Showed all bedrooms", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 2 }, + { new Guid("00000000-0000-0000-0002-000000000006"), true, "Interior Tour", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 6, "Showed all bathrooms", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 2 }, + { new Guid("00000000-0000-0000-0002-000000000007"), true, "Kitchen & Appliances", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 7, "Toured kitchen and demonstrated appliances", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 3 }, + { new Guid("00000000-0000-0000-0002-000000000008"), true, "Kitchen & Appliances", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 8, "Explained which appliances are included", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 3 }, + { new Guid("00000000-0000-0000-0002-000000000009"), true, "Utilities & Systems", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 9, "Explained HVAC system and thermostat controls", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 4 }, + { new Guid("00000000-0000-0000-0002-000000000010"), true, "Utilities & Systems", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 10, "Reviewed utility responsibilities (tenant vs landlord)", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 4 }, + { new Guid("00000000-0000-0000-0002-000000000011"), true, "Utilities & Systems", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 11, "Showed water heater location", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 4 }, + { new Guid("00000000-0000-0000-0002-000000000012"), true, "Storage & Amenities", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 12, "Showed storage areas (closets, attic, basement)", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 5 }, + { new Guid("00000000-0000-0000-0002-000000000013"), true, "Storage & Amenities", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 13, "Showed laundry facilities", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 5 }, + { new Guid("00000000-0000-0000-0002-000000000014"), true, "Storage & Amenities", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 14, "Showed outdoor space (yard, patio, balcony)", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 5 }, + { new Guid("00000000-0000-0000-0002-000000000015"), true, "Lease Terms", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 15, "Discussed monthly rent amount", "", null, new Guid("00000000-0000-0000-0000-000000000000"), true, 6 }, + { new Guid("00000000-0000-0000-0002-000000000016"), true, "Lease Terms", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 16, "Explained security deposit and move-in costs", "", null, new Guid("00000000-0000-0000-0000-000000000000"), true, 6 }, + { new Guid("00000000-0000-0000-0002-000000000017"), true, "Lease Terms", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 17, "Reviewed lease term length and start date", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 6 }, + { new Guid("00000000-0000-0000-0002-000000000018"), true, "Lease Terms", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 18, "Explained pet policy", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 6 }, + { new Guid("00000000-0000-0000-0002-000000000019"), true, "Next Steps", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 19, "Explained application process and requirements", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 7 }, + { new Guid("00000000-0000-0000-0002-000000000020"), true, "Next Steps", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 20, "Reviewed screening process (background, credit check)", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 7 }, + { new Guid("00000000-0000-0000-0002-000000000021"), true, "Next Steps", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 21, "Answered all prospect questions", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 7 }, + { new Guid("00000000-0000-0000-0002-000000000022"), true, "Assessment", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 22, "Prospect Interest Level", "", null, new Guid("00000000-0000-0000-0000-000000000000"), true, 8 }, + { new Guid("00000000-0000-0000-0002-000000000023"), true, "Assessment", new Guid("00000000-0000-0000-0001-000000000001"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 23, "Overall showing feedback and notes", "", null, new Guid("00000000-0000-0000-0000-000000000000"), true, 8 }, + { new Guid("00000000-0000-0000-0002-000000000024"), true, "General", new Guid("00000000-0000-0000-0001-000000000002"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 1, "Document property condition", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000025"), true, "General", new Guid("00000000-0000-0000-0001-000000000002"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 2, "Collect keys and access codes", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000026"), true, "General", new Guid("00000000-0000-0000-0001-000000000002"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 3, "Review lease terms with tenant", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000027"), true, "General", new Guid("00000000-0000-0000-0001-000000000003"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 1, "Inspect property condition", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000028"), true, "General", new Guid("00000000-0000-0000-0001-000000000003"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 2, "Collect all keys and access devices", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000029"), true, "General", new Guid("00000000-0000-0000-0001-000000000003"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 3, "Document damages and needed repairs", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000030"), true, "Preparation", new Guid("00000000-0000-0000-0001-000000000004"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 1, "Set up signage and directional markers", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000031"), true, "Preparation", new Guid("00000000-0000-0000-0001-000000000004"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 2, "Prepare information packets", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 }, + { new Guid("00000000-0000-0000-0002-000000000032"), true, "Preparation", new Guid("00000000-0000-0000-0001-000000000004"), "", new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), false, true, 3, "Set up visitor sign-in sheet", "", null, new Guid("00000000-0000-0000-0000-000000000000"), false, 1 } + }); + + migrationBuilder.CreateIndex( + name: "IX_ApplicationScreenings_OrganizationId", + table: "ApplicationScreenings", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_ApplicationScreenings_OverallResult", + table: "ApplicationScreenings", + column: "OverallResult"); + + migrationBuilder.CreateIndex( + name: "IX_ApplicationScreenings_RentalApplicationId", + table: "ApplicationScreenings", + column: "RentalApplicationId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CalendarEvents_EventType", + table: "CalendarEvents", + column: "EventType"); + + migrationBuilder.CreateIndex( + name: "IX_CalendarEvents_OrganizationId", + table: "CalendarEvents", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_CalendarEvents_PropertyId", + table: "CalendarEvents", + column: "PropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_CalendarEvents_SourceEntityId", + table: "CalendarEvents", + column: "SourceEntityId"); + + migrationBuilder.CreateIndex( + name: "IX_CalendarEvents_SourceEntityType_SourceEntityId", + table: "CalendarEvents", + columns: new[] { "SourceEntityType", "SourceEntityId" }); + + migrationBuilder.CreateIndex( + name: "IX_CalendarEvents_StartOn", + table: "CalendarEvents", + column: "StartOn"); + + migrationBuilder.CreateIndex( + name: "IX_CalendarSettings_OrganizationId", + table: "CalendarSettings", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_CalendarSettings_OrganizationId_EntityType", + table: "CalendarSettings", + columns: new[] { "OrganizationId", "EntityType" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ChecklistItems_ChecklistId", + table: "ChecklistItems", + column: "ChecklistId"); + + migrationBuilder.CreateIndex( + name: "IX_Checklists_ChecklistTemplateId", + table: "Checklists", + column: "ChecklistTemplateId"); + + migrationBuilder.CreateIndex( + name: "IX_Checklists_ChecklistType", + table: "Checklists", + column: "ChecklistType"); + + migrationBuilder.CreateIndex( + name: "IX_Checklists_CompletedOn", + table: "Checklists", + column: "CompletedOn"); + + migrationBuilder.CreateIndex( + name: "IX_Checklists_DocumentId", + table: "Checklists", + column: "DocumentId"); + + migrationBuilder.CreateIndex( + name: "IX_Checklists_LeaseId", + table: "Checklists", + column: "LeaseId"); + + migrationBuilder.CreateIndex( + name: "IX_Checklists_PropertyId", + table: "Checklists", + column: "PropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_Checklists_Status", + table: "Checklists", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_ChecklistTemplateItems_ChecklistTemplateId", + table: "ChecklistTemplateItems", + column: "ChecklistTemplateId"); + + migrationBuilder.CreateIndex( + name: "IX_ChecklistTemplates_Category", + table: "ChecklistTemplates", + column: "Category"); + + migrationBuilder.CreateIndex( + name: "IX_ChecklistTemplates_OrganizationId", + table: "ChecklistTemplates", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_InvoiceId", + table: "Documents", + column: "InvoiceId"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_LeaseId", + table: "Documents", + column: "LeaseId"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_OrganizationId", + table: "Documents", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_PaymentId", + table: "Documents", + column: "PaymentId"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_PropertyId", + table: "Documents", + column: "PropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_TenantId", + table: "Documents", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Inspections_CompletedOn", + table: "Inspections", + column: "CompletedOn"); + + migrationBuilder.CreateIndex( + name: "IX_Inspections_DocumentId", + table: "Inspections", + column: "DocumentId"); + + migrationBuilder.CreateIndex( + name: "IX_Inspections_LeaseId", + table: "Inspections", + column: "LeaseId"); + + migrationBuilder.CreateIndex( + name: "IX_Inspections_PropertyId", + table: "Inspections", + column: "PropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_Invoices_DocumentId", + table: "Invoices", + column: "DocumentId"); + + migrationBuilder.CreateIndex( + name: "IX_Invoices_InvoiceNumber", + table: "Invoices", + column: "InvoiceNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Invoices_LeaseId", + table: "Invoices", + column: "LeaseId"); + + migrationBuilder.CreateIndex( + name: "IX_Invoices_OrganizationId", + table: "Invoices", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_LeaseOffers_PropertyId", + table: "LeaseOffers", + column: "PropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_LeaseOffers_ProspectiveTenantId", + table: "LeaseOffers", + column: "ProspectiveTenantId"); + + migrationBuilder.CreateIndex( + name: "IX_LeaseOffers_RentalApplicationId", + table: "LeaseOffers", + column: "RentalApplicationId"); + + migrationBuilder.CreateIndex( + name: "IX_Leases_DocumentId", + table: "Leases", + column: "DocumentId"); + + migrationBuilder.CreateIndex( + name: "IX_Leases_OrganizationId", + table: "Leases", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Leases_PropertyId", + table: "Leases", + column: "PropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_Leases_TenantId", + table: "Leases", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_MaintenanceRequests_LeaseId", + table: "MaintenanceRequests", + column: "LeaseId"); + + migrationBuilder.CreateIndex( + name: "IX_MaintenanceRequests_Priority", + table: "MaintenanceRequests", + column: "Priority"); + + migrationBuilder.CreateIndex( + name: "IX_MaintenanceRequests_PropertyId", + table: "MaintenanceRequests", + column: "PropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_MaintenanceRequests_RequestedOn", + table: "MaintenanceRequests", + column: "RequestedOn"); + + migrationBuilder.CreateIndex( + name: "IX_MaintenanceRequests_Status", + table: "MaintenanceRequests", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_Notes_CreatedBy", + table: "Notes", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Organizations_IsActive", + table: "Organizations", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_Organizations_OwnerId", + table: "Organizations", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSettings_OrganizationId", + table: "OrganizationSettings", + column: "OrganizationId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Payments_DocumentId", + table: "Payments", + column: "DocumentId"); + + migrationBuilder.CreateIndex( + name: "IX_Payments_InvoiceId", + table: "Payments", + column: "InvoiceId"); + + migrationBuilder.CreateIndex( + name: "IX_Payments_OrganizationId", + table: "Payments", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Properties_Address", + table: "Properties", + column: "Address"); + + migrationBuilder.CreateIndex( + name: "IX_Properties_OrganizationId", + table: "Properties", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_ProspectiveTenants_Email", + table: "ProspectiveTenants", + column: "Email"); + + migrationBuilder.CreateIndex( + name: "IX_ProspectiveTenants_InterestedPropertyId", + table: "ProspectiveTenants", + column: "InterestedPropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_ProspectiveTenants_OrganizationId", + table: "ProspectiveTenants", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_ProspectiveTenants_Status", + table: "ProspectiveTenants", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_RentalApplications_AppliedOn", + table: "RentalApplications", + column: "AppliedOn"); + + migrationBuilder.CreateIndex( + name: "IX_RentalApplications_OrganizationId", + table: "RentalApplications", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_RentalApplications_PropertyId", + table: "RentalApplications", + column: "PropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_RentalApplications_ProspectiveTenantId", + table: "RentalApplications", + column: "ProspectiveTenantId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RentalApplications_Status", + table: "RentalApplications", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDepositDividends_InvestmentPoolId", + table: "SecurityDepositDividends", + column: "InvestmentPoolId"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDepositDividends_LeaseId", + table: "SecurityDepositDividends", + column: "LeaseId"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDepositDividends_SecurityDepositId", + table: "SecurityDepositDividends", + column: "SecurityDepositId"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDepositDividends_Status", + table: "SecurityDepositDividends", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDepositDividends_TenantId", + table: "SecurityDepositDividends", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDepositDividends_Year", + table: "SecurityDepositDividends", + column: "Year"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDepositInvestmentPools_OrganizationId", + table: "SecurityDepositInvestmentPools", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDepositInvestmentPools_Status", + table: "SecurityDepositInvestmentPools", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDepositInvestmentPools_Year", + table: "SecurityDepositInvestmentPools", + column: "Year", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDeposits_InInvestmentPool", + table: "SecurityDeposits", + column: "InInvestmentPool"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDeposits_LeaseId", + table: "SecurityDeposits", + column: "LeaseId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDeposits_Status", + table: "SecurityDeposits", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_SecurityDeposits_TenantId", + table: "SecurityDeposits", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Tenants_Email", + table: "Tenants", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Tenants_IdentificationNumber", + table: "Tenants", + column: "IdentificationNumber", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Tenants_OrganizationId", + table: "Tenants", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Tours_ChecklistId", + table: "Tours", + column: "ChecklistId"); + + migrationBuilder.CreateIndex( + name: "IX_Tours_OrganizationId", + table: "Tours", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Tours_PropertyId", + table: "Tours", + column: "PropertyId"); + + migrationBuilder.CreateIndex( + name: "IX_Tours_ProspectiveTenantId", + table: "Tours", + column: "ProspectiveTenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Tours_ScheduledOn", + table: "Tours", + column: "ScheduledOn"); + + migrationBuilder.CreateIndex( + name: "IX_Tours_Status", + table: "Tours", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_UserOrganizations_GrantedBy", + table: "UserOrganizations", + column: "GrantedBy"); + + migrationBuilder.CreateIndex( + name: "IX_UserOrganizations_IsActive", + table: "UserOrganizations", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_UserOrganizations_OrganizationId", + table: "UserOrganizations", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_UserOrganizations_Role", + table: "UserOrganizations", + column: "Role"); + + migrationBuilder.CreateIndex( + name: "IX_UserOrganizations_UserId_OrganizationId", + table: "UserOrganizations", + columns: new[] { "UserId", "OrganizationId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_WorkflowAuditLogs_Action", + table: "WorkflowAuditLogs", + column: "Action"); + + migrationBuilder.CreateIndex( + name: "IX_WorkflowAuditLogs_EntityId", + table: "WorkflowAuditLogs", + column: "EntityId"); + + migrationBuilder.CreateIndex( + name: "IX_WorkflowAuditLogs_EntityType", + table: "WorkflowAuditLogs", + column: "EntityType"); + + migrationBuilder.CreateIndex( + name: "IX_WorkflowAuditLogs_EntityType_EntityId", + table: "WorkflowAuditLogs", + columns: new[] { "EntityType", "EntityId" }); + + migrationBuilder.CreateIndex( + name: "IX_WorkflowAuditLogs_OrganizationId", + table: "WorkflowAuditLogs", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_WorkflowAuditLogs_PerformedBy", + table: "WorkflowAuditLogs", + column: "PerformedBy"); + + migrationBuilder.CreateIndex( + name: "IX_WorkflowAuditLogs_PerformedOn", + table: "WorkflowAuditLogs", + column: "PerformedOn"); + + migrationBuilder.AddForeignKey( + name: "FK_ChecklistItems_Checklists_ChecklistId", + table: "ChecklistItems", + column: "ChecklistId", + principalTable: "Checklists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Checklists_Documents_DocumentId", + table: "Checklists", + column: "DocumentId", + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Checklists_Leases_LeaseId", + table: "Checklists", + column: "LeaseId", + principalTable: "Leases", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Documents_Invoices_InvoiceId", + table: "Documents", + column: "InvoiceId", + principalTable: "Invoices", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Documents_Leases_LeaseId", + table: "Documents", + column: "LeaseId", + principalTable: "Leases", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Documents_Payments_PaymentId", + table: "Documents", + column: "PaymentId", + principalTable: "Payments", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Organizations_AspNetUsers_OwnerId", + table: "Organizations"); + + migrationBuilder.DropForeignKey( + name: "FK_Documents_Properties_PropertyId", + table: "Documents"); + + migrationBuilder.DropForeignKey( + name: "FK_Leases_Properties_PropertyId", + table: "Leases"); + + migrationBuilder.DropForeignKey( + name: "FK_Invoices_Documents_DocumentId", + table: "Invoices"); + + migrationBuilder.DropForeignKey( + name: "FK_Leases_Documents_DocumentId", + table: "Leases"); + + migrationBuilder.DropForeignKey( + name: "FK_Payments_Documents_DocumentId", + table: "Payments"); + + migrationBuilder.DropTable( + name: "ApplicationScreenings"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "CalendarEvents"); + + migrationBuilder.DropTable( + name: "CalendarSettings"); + + migrationBuilder.DropTable( + name: "ChecklistItems"); + + migrationBuilder.DropTable( + name: "ChecklistTemplateItems"); + + migrationBuilder.DropTable( + name: "Inspections"); + + migrationBuilder.DropTable( + name: "LeaseOffers"); + + migrationBuilder.DropTable( + name: "MaintenanceRequests"); + + migrationBuilder.DropTable( + name: "Notes"); + + migrationBuilder.DropTable( + name: "OrganizationSettings"); + + migrationBuilder.DropTable( + name: "SchemaVersions"); + + migrationBuilder.DropTable( + name: "SecurityDepositDividends"); + + migrationBuilder.DropTable( + name: "Tours"); + + migrationBuilder.DropTable( + name: "UserOrganizations"); + + migrationBuilder.DropTable( + name: "WorkflowAuditLogs"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "RentalApplications"); + + migrationBuilder.DropTable( + name: "SecurityDepositInvestmentPools"); + + migrationBuilder.DropTable( + name: "SecurityDeposits"); + + migrationBuilder.DropTable( + name: "Checklists"); + + migrationBuilder.DropTable( + name: "ProspectiveTenants"); + + migrationBuilder.DropTable( + name: "ChecklistTemplates"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "Properties"); + + migrationBuilder.DropTable( + name: "Documents"); + + migrationBuilder.DropTable( + name: "Payments"); + + migrationBuilder.DropTable( + name: "Invoices"); + + migrationBuilder.DropTable( + name: "Leases"); + + migrationBuilder.DropTable( + name: "Tenants"); + + migrationBuilder.DropTable( + name: "Organizations"); + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.Designer.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.Designer.cs new file mode 100644 index 0000000..5454978 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.Designer.cs @@ -0,0 +1,3917 @@ +// +using System; +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251211184024_WorkflowAuditLog")] + partial class WorkflowAuditLog + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.Professional.Application.Services.Workflows.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreditCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreditScore") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OverallResult") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("ResultNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OverallResult"); + + b.HasIndex("RentalApplicationId") + .IsUnique(); + + b.ToTable("ApplicationScreenings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("EndOn") + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StartOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 11, + ItemText = "Showed water heater location", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 18, + ItemText = "Explained pet policy", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Document property condition", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Inspect property condition", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Prepare information packets", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrantedBy"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Shared.Components.Account.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginIP") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("LoginCount") + .HasColumnType("INTEGER"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PreviousLoginDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Professional.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", "User") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("GrantedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany("UserOrganizations") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + + b.Navigation("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.cs new file mode 100644 index 0000000..3e6b2dd --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.cs @@ -0,0 +1,90 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + /// + public partial class WorkflowAuditLog : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_RentalApplications_ProspectiveTenantId", + table: "RentalApplications"); + + migrationBuilder.AddColumn( + name: "ActualMoveOutDate", + table: "Leases", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "ExpectedMoveOutDate", + table: "Leases", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "RenewalNumber", + table: "Leases", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "TerminationNoticedOn", + table: "Leases", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "TerminationReason", + table: "Leases", + type: "TEXT", + maxLength: 500, + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_RentalApplications_ProspectiveTenantId", + table: "RentalApplications", + column: "ProspectiveTenantId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_RentalApplications_ProspectiveTenantId", + table: "RentalApplications"); + + migrationBuilder.DropColumn( + name: "ActualMoveOutDate", + table: "Leases"); + + migrationBuilder.DropColumn( + name: "ExpectedMoveOutDate", + table: "Leases"); + + migrationBuilder.DropColumn( + name: "RenewalNumber", + table: "Leases"); + + migrationBuilder.DropColumn( + name: "TerminationNoticedOn", + table: "Leases"); + + migrationBuilder.DropColumn( + name: "TerminationReason", + table: "Leases"); + + migrationBuilder.CreateIndex( + name: "IX_RentalApplications_ProspectiveTenantId", + table: "RentalApplications", + column: "ProspectiveTenantId", + unique: true); + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.Designer.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.Designer.cs new file mode 100644 index 0000000..93f0d03 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.Designer.cs @@ -0,0 +1,3917 @@ +// +using System; +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251211232344_UpdateSeedData")] + partial class UpdateSeedData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.Professional.Application.Services.Workflows.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreditCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreditScore") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OverallResult") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("ResultNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OverallResult"); + + b.HasIndex("RentalApplicationId") + .IsUnique(); + + b.ToTable("ApplicationScreenings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("EndOn") + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StartOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSystemTemplate = true, + LastModifiedBy = "", + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 11, + ItemText = "Showed water heater location", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 18, + ItemText = "Explained pet policy", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Document property condition", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Inspect property condition", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Prepare information packets", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + LastModifiedBy = "", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrantedBy"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Shared.Components.Account.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginIP") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("LoginCount") + .HasColumnType("INTEGER"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PreviousLoginDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Professional.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", "User") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("GrantedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany("UserOrganizations") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + + b.Navigation("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.cs new file mode 100644 index 0000000..dca430e --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + /// + public partial class UpdateSeedData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000015"), + column: "RequiresValue", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000016"), + column: "RequiresValue", + value: false); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000023"), + column: "RequiresValue", + value: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000015"), + column: "RequiresValue", + value: true); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000016"), + column: "RequiresValue", + value: true); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000023"), + column: "RequiresValue", + value: true); + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.Designer.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.Designer.cs new file mode 100644 index 0000000..a94234e --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.Designer.cs @@ -0,0 +1,4123 @@ +// +using System; +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251229235707_AddNotificationInfrastructure")] + partial class AddNotificationInfrastructure + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.Professional.Application.Services.Workflows.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreditCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreditScore") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OverallResult") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("ResultNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OverallResult"); + + b.HasIndex("RentalApplicationId") + .IsUnique(); + + b.ToTable("ApplicationScreenings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("EndOn") + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StartOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 11, + ItemText = "Showed water heater location", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 18, + ItemText = "Explained pet policy", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Document property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Inspect property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Prepare information packets", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrantedBy"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Shared.Components.Account.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginIP") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("LoginCount") + .HasColumnType("INTEGER"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PreviousLoginDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EmailError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EmailSent") + .HasColumnType("INTEGER"); + + b.Property("EmailSentOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRead") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReadOn") + .HasColumnType("TEXT"); + + b.Property("RecipientUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RelatedEntityId") + .HasColumnType("TEXT"); + + b.Property("RelatedEntityType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SMSError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SMSSent") + .HasColumnType("INTEGER"); + + b.Property("SMSSentOn") + .HasColumnType("TEXT"); + + b.Property("SendEmail") + .HasColumnType("INTEGER"); + + b.Property("SendInApp") + .HasColumnType("INTEGER"); + + b.Property("SendSMS") + .HasColumnType("INTEGER"); + + b.Property("SentOn") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsRead"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("SentOn"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("NotificationPreferences", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyDigestTime") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmailApplicationStatusChange") + .HasColumnType("INTEGER"); + + b.Property("EmailInspectionScheduled") + .HasColumnType("INTEGER"); + + b.Property("EmailLeaseExpiring") + .HasColumnType("INTEGER"); + + b.Property("EmailMaintenanceUpdate") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentReceived") + .HasColumnType("INTEGER"); + + b.Property("EnableDailyDigest") + .HasColumnType("INTEGER"); + + b.Property("EnableEmailNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableInAppNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableSMSNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableWeeklyDigest") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SMSLeaseExpiringUrgent") + .HasColumnType("INTEGER"); + + b.Property("SMSMaintenanceEmergency") + .HasColumnType("INTEGER"); + + b.Property("SMSPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WeeklyDigestDay") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("NotificationPreferences"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Professional.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", "User") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("GrantedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany("UserOrganizations") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("RecipientUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("NotificationPreferences", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + + b.Navigation("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.cs new file mode 100644 index 0000000..28b7d0a --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.cs @@ -0,0 +1,666 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + /// + public partial class AddNotificationInfrastructure : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "NotificationPreferences", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + EnableInAppNotifications = table.Column(type: "INTEGER", nullable: false), + EnableEmailNotifications = table.Column(type: "INTEGER", nullable: false), + EmailAddress = table.Column(type: "TEXT", maxLength: 200, nullable: true), + EmailLeaseExpiring = table.Column(type: "INTEGER", nullable: false), + EmailPaymentDue = table.Column(type: "INTEGER", nullable: false), + EmailPaymentReceived = table.Column(type: "INTEGER", nullable: false), + EmailApplicationStatusChange = table.Column(type: "INTEGER", nullable: false), + EmailMaintenanceUpdate = table.Column(type: "INTEGER", nullable: false), + EmailInspectionScheduled = table.Column(type: "INTEGER", nullable: false), + EnableSMSNotifications = table.Column(type: "INTEGER", nullable: false), + PhoneNumber = table.Column(type: "TEXT", maxLength: 20, nullable: true), + SMSPaymentDue = table.Column(type: "INTEGER", nullable: false), + SMSMaintenanceEmergency = table.Column(type: "INTEGER", nullable: false), + SMSLeaseExpiringUrgent = table.Column(type: "INTEGER", nullable: false), + EnableDailyDigest = table.Column(type: "INTEGER", nullable: false), + DailyDigestTime = table.Column(type: "TEXT", nullable: false), + EnableWeeklyDigest = table.Column(type: "INTEGER", nullable: false), + WeeklyDigestDay = table.Column(type: "INTEGER", nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_NotificationPreferences", x => x.Id); + table.ForeignKey( + name: "FK_NotificationPreferences_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_NotificationPreferences_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Notifications", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Message = table.Column(type: "TEXT", maxLength: 2000, nullable: false), + Type = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Category = table.Column(type: "TEXT", maxLength: 50, nullable: false), + RecipientUserId = table.Column(type: "TEXT", nullable: false), + SentOn = table.Column(type: "TEXT", nullable: false), + ReadOn = table.Column(type: "TEXT", nullable: true), + IsRead = table.Column(type: "INTEGER", nullable: false), + RelatedEntityId = table.Column(type: "TEXT", nullable: true), + RelatedEntityType = table.Column(type: "TEXT", maxLength: 50, nullable: true), + SendInApp = table.Column(type: "INTEGER", nullable: false), + SendEmail = table.Column(type: "INTEGER", nullable: false), + SendSMS = table.Column(type: "INTEGER", nullable: false), + EmailSent = table.Column(type: "INTEGER", nullable: false), + EmailSentOn = table.Column(type: "TEXT", nullable: true), + SMSSent = table.Column(type: "INTEGER", nullable: false), + SMSSentOn = table.Column(type: "TEXT", nullable: true), + EmailError = table.Column(type: "TEXT", maxLength: 500, nullable: true), + SMSError = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.Id); + table.ForeignKey( + name: "FK_Notifications_AspNetUsers_RecipientUserId", + column: x => x.RecipientUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Notifications_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000001"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000002"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000003"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000004"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000005"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000006"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000007"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000008"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000009"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000010"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000011"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000012"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000013"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000014"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000015"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000016"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000017"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000018"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000019"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000020"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000021"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000022"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000023"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000024"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000025"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000026"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000027"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000028"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000029"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000030"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000031"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000032"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000001"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000002"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000003"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000004"), + column: "LastModifiedBy", + value: null); + + migrationBuilder.CreateIndex( + name: "IX_NotificationPreferences_OrganizationId", + table: "NotificationPreferences", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_NotificationPreferences_UserId", + table: "NotificationPreferences", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_NotificationPreferences_UserId_OrganizationId", + table: "NotificationPreferences", + columns: new[] { "UserId", "OrganizationId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_Category", + table: "Notifications", + column: "Category"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_IsRead", + table: "Notifications", + column: "IsRead"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_OrganizationId", + table: "Notifications", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_RecipientUserId", + table: "Notifications", + column: "RecipientUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_SentOn", + table: "Notifications", + column: "SentOn"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "NotificationPreferences"); + + migrationBuilder.DropTable( + name: "Notifications"); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000001"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000002"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000003"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000004"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000005"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000006"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000007"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000008"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000009"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000010"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000011"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000012"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000013"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000014"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000015"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000016"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000017"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000018"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000019"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000020"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000021"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000022"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000023"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000024"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000025"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000026"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000027"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000028"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000029"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000030"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000031"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplateItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0002-000000000032"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000001"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000002"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000003"), + column: "LastModifiedBy", + value: ""); + + migrationBuilder.UpdateData( + table: "ChecklistTemplates", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0001-000000000004"), + column: "LastModifiedBy", + value: ""); + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.Designer.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.Designer.cs new file mode 100644 index 0000000..8189662 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.Designer.cs @@ -0,0 +1,4324 @@ +// +using System; +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251230141240_OrganizationEmailSMSSettings")] + partial class OrganizationEmailSMSSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.Professional.Application.Services.Workflows.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreditCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreditScore") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OverallResult") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("ResultNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OverallResult"); + + b.HasIndex("RentalApplicationId") + .IsUnique(); + + b.ToTable("ApplicationScreenings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("EndOn") + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StartOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 11, + ItemText = "Showed water heater location", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 18, + ItemText = "Explained pet policy", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Document property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Inspect property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Prepare information packets", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationEmailSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("DailyLimit") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentToday") + .HasColumnType("INTEGER"); + + b.Property("FromEmail") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FromName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsEmailEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastEmailSentOn") + .HasColumnType("TEXT"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastErrorOn") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyLimit") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SendGridApiKeyEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationEmailSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSMSSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountBalance") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("AccountType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CostPerSMS") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSMSEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastSMSSentOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("SMSSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("SMSSentToday") + .HasColumnType("INTEGER"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("TwilioAccountSidEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioAuthTokenEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioPhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSMSSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrantedBy"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Shared.Components.Account.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginIP") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("LoginCount") + .HasColumnType("INTEGER"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PreviousLoginDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EmailError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EmailSent") + .HasColumnType("INTEGER"); + + b.Property("EmailSentOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRead") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReadOn") + .HasColumnType("TEXT"); + + b.Property("RecipientUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RelatedEntityId") + .HasColumnType("TEXT"); + + b.Property("RelatedEntityType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SMSError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SMSSent") + .HasColumnType("INTEGER"); + + b.Property("SMSSentOn") + .HasColumnType("TEXT"); + + b.Property("SendEmail") + .HasColumnType("INTEGER"); + + b.Property("SendInApp") + .HasColumnType("INTEGER"); + + b.Property("SendSMS") + .HasColumnType("INTEGER"); + + b.Property("SentOn") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsRead"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("SentOn"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("NotificationPreferences", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyDigestTime") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmailApplicationStatusChange") + .HasColumnType("INTEGER"); + + b.Property("EmailInspectionScheduled") + .HasColumnType("INTEGER"); + + b.Property("EmailLeaseExpiring") + .HasColumnType("INTEGER"); + + b.Property("EmailMaintenanceUpdate") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentReceived") + .HasColumnType("INTEGER"); + + b.Property("EnableDailyDigest") + .HasColumnType("INTEGER"); + + b.Property("EnableEmailNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableInAppNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableSMSNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableWeeklyDigest") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SMSLeaseExpiringUrgent") + .HasColumnType("INTEGER"); + + b.Property("SMSMaintenanceEmergency") + .HasColumnType("INTEGER"); + + b.Property("SMSPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WeeklyDigestDay") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("NotificationPreferences"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Professional.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", "User") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationEmailSettings", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSMSSettings", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("GrantedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany("UserOrganizations") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("RecipientUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("NotificationPreferences", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + + b.Navigation("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.cs new file mode 100644 index 0000000..5c10a08 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.cs @@ -0,0 +1,116 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + /// + public partial class OrganizationEmailSMSSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OrganizationEmailSettings", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + IsEmailEnabled = table.Column(type: "INTEGER", nullable: false), + SendGridApiKeyEncrypted = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + FromEmail = table.Column(type: "TEXT", maxLength: 200, nullable: true), + FromName = table.Column(type: "TEXT", maxLength: 200, nullable: true), + EmailsSentToday = table.Column(type: "INTEGER", nullable: false), + EmailsSentThisMonth = table.Column(type: "INTEGER", nullable: false), + LastEmailSentOn = table.Column(type: "TEXT", nullable: true), + StatsLastUpdatedOn = table.Column(type: "TEXT", nullable: true), + DailyCountResetOn = table.Column(type: "TEXT", nullable: true), + MonthlyCountResetOn = table.Column(type: "TEXT", nullable: true), + DailyLimit = table.Column(type: "INTEGER", nullable: true), + MonthlyLimit = table.Column(type: "INTEGER", nullable: true), + PlanType = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsVerified = table.Column(type: "INTEGER", nullable: false), + LastVerifiedOn = table.Column(type: "TEXT", nullable: true), + LastError = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + LastErrorOn = table.Column(type: "TEXT", nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationEmailSettings", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationEmailSettings_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrganizationSMSSettings", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + IsSMSEnabled = table.Column(type: "INTEGER", nullable: false), + TwilioAccountSidEncrypted = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + TwilioAuthTokenEncrypted = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + TwilioPhoneNumber = table.Column(type: "TEXT", maxLength: 20, nullable: true), + SMSSentToday = table.Column(type: "INTEGER", nullable: false), + SMSSentThisMonth = table.Column(type: "INTEGER", nullable: false), + LastSMSSentOn = table.Column(type: "TEXT", nullable: true), + StatsLastUpdatedOn = table.Column(type: "TEXT", nullable: true), + DailyCountResetOn = table.Column(type: "TEXT", nullable: true), + MonthlyCountResetOn = table.Column(type: "TEXT", nullable: true), + AccountBalance = table.Column(type: "TEXT", precision: 18, scale: 2, nullable: true), + CostPerSMS = table.Column(type: "TEXT", precision: 18, scale: 4, nullable: true), + AccountType = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsVerified = table.Column(type: "INTEGER", nullable: false), + LastVerifiedOn = table.Column(type: "TEXT", nullable: true), + LastError = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + CreatedOn = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationSMSSettings", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationSMSSettings_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationEmailSettings_OrganizationId", + table: "OrganizationEmailSettings", + column: "OrganizationId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSMSSettings_OrganizationId", + table: "OrganizationSMSSettings", + column: "OrganizationId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationEmailSettings"); + + migrationBuilder.DropTable( + name: "OrganizationSMSSettings"); + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Aquiis.Professional/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..643a304 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,4321 @@ +// +using System; +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Professional.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.Professional.Application.Services.Workflows.WorkflowAuditLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FromStatus") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PerformedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PerformedOn") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("ToStatus") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("EntityId"); + + b.HasIndex("EntityType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PerformedBy"); + + b.HasIndex("PerformedOn"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("WorkflowAuditLogs"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("BackgroundCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("BackgroundCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckCompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreditCheckNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreditCheckPassed") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequested") + .HasColumnType("INTEGER"); + + b.Property("CreditCheckRequestedOn") + .HasColumnType("TEXT"); + + b.Property("CreditScore") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OverallResult") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("ResultNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OverallResult"); + + b.HasIndex("RentalApplicationId") + .IsUnique(); + + b.ToTable("ApplicationScreenings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("EndOn") + .HasColumnType("TEXT"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityId") + .HasColumnType("TEXT"); + + b.Property("SourceEntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("StartOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EventType"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("SourceEntityId"); + + b.HasIndex("StartOn"); + + b.HasIndex("SourceEntityType", "SourceEntityId"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AutoCreateEvents") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultColor") + .HasColumnType("TEXT"); + + b.Property("DefaultIcon") + .HasColumnType("TEXT"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ShowOnCalendar") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationId", "EntityType") + .IsUnique(); + + b.ToTable("CalendarSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("ChecklistType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CompletedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.HasIndex("ChecklistType"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("Status"); + + b.ToTable("Checklists"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsChecked") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.ToTable("ChecklistItems"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSystemTemplate") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("OrganizationId"); + + b.ToTable("ChecklistTemplates"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0001-000000000001"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard property showing checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Property Tour", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000002"), + Category = "MoveIn", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-in inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-In", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000003"), + Category = "MoveOut", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move-out inspection checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Move-Out", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }, + new + { + Id = new Guid("00000000-0000-0000-0001-000000000004"), + Category = "Tour", + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Open house event checklist", + IsDeleted = false, + IsSystemTemplate = true, + Name = "Open House", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000") + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsNotes") + .HasColumnType("INTEGER"); + + b.Property("CategorySection") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ChecklistTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRequired") + .HasColumnType("INTEGER"); + + b.Property("ItemOrder") + .HasColumnType("INTEGER"); + + b.Property("ItemText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequiresValue") + .HasColumnType("INTEGER"); + + b.Property("SectionOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistTemplateId"); + + b.ToTable("ChecklistTemplateItems"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0002-000000000001"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Greeted prospect and verified appointment", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000002"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Reviewed property exterior and curb appeal", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000003"), + AllowsNotes = true, + CategorySection = "Arrival & Introduction", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Showed parking area/garage", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000004"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 4, + ItemText = "Toured living room/common areas", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000005"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 5, + ItemText = "Showed all bedrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000006"), + AllowsNotes = true, + CategorySection = "Interior Tour", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 6, + ItemText = "Showed all bathrooms", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000007"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 7, + ItemText = "Toured kitchen and demonstrated appliances", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000008"), + AllowsNotes = true, + CategorySection = "Kitchen & Appliances", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 8, + ItemText = "Explained which appliances are included", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000009"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 9, + ItemText = "Explained HVAC system and thermostat controls", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000010"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 10, + ItemText = "Reviewed utility responsibilities (tenant vs landlord)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000011"), + AllowsNotes = true, + CategorySection = "Utilities & Systems", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 11, + ItemText = "Showed water heater location", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000012"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 12, + ItemText = "Showed storage areas (closets, attic, basement)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000013"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 13, + ItemText = "Showed laundry facilities", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000014"), + AllowsNotes = true, + CategorySection = "Storage & Amenities", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 14, + ItemText = "Showed outdoor space (yard, patio, balcony)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000015"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 15, + ItemText = "Discussed monthly rent amount", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000016"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 16, + ItemText = "Explained security deposit and move-in costs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000017"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 17, + ItemText = "Reviewed lease term length and start date", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000018"), + AllowsNotes = true, + CategorySection = "Lease Terms", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 18, + ItemText = "Explained pet policy", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000019"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 19, + ItemText = "Explained application process and requirements", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000020"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 20, + ItemText = "Reviewed screening process (background, credit check)", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000021"), + AllowsNotes = true, + CategorySection = "Next Steps", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 21, + ItemText = "Answered all prospect questions", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000022"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 22, + ItemText = "Prospect Interest Level", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = true, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000023"), + AllowsNotes = true, + CategorySection = "Assessment", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000001"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 23, + ItemText = "Overall showing feedback and notes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000024"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Document property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000025"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect keys and access codes", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000026"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000002"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Review lease terms with tenant", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000027"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Inspect property condition", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000028"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Collect all keys and access devices", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000029"), + AllowsNotes = true, + CategorySection = "General", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000003"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Document damages and needed repairs", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000030"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 1, + ItemText = "Set up signage and directional markers", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000031"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 2, + ItemText = "Prepare information packets", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0002-000000000032"), + AllowsNotes = true, + CategorySection = "Preparation", + ChecklistTemplateId = new Guid("00000000-0000-0000-0001-000000000004"), + CreatedBy = "", + CreatedOn = new DateTime(2025, 11, 30, 0, 0, 0, 0, DateTimeKind.Utc), + IsDeleted = false, + IsRequired = true, + ItemOrder = 3, + ItemText = "Set up visitor sign-in sheet", + OrganizationId = new Guid("00000000-0000-0000-0000-000000000000"), + RequiresValue = false, + SectionOrder = 1 + }); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("FileExtension") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActionItemsRequired") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("BathroomSinkGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomSinkNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomToiletGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomToiletNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomTubShowerGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomTubShowerNotes") + .HasColumnType("TEXT"); + + b.Property("BathroomVentilationGood") + .HasColumnType("INTEGER"); + + b.Property("BathroomVentilationNotes") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CarbonMonoxideDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("CarbonMonoxideDetectorsNotes") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("ElectricalSystemGood") + .HasColumnType("INTEGER"); + + b.Property("ElectricalSystemNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorFoundationGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorFoundationNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorGuttersGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorGuttersNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorRoofGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorRoofNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorSidingGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorSidingNotes") + .HasColumnType("TEXT"); + + b.Property("ExteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("ExteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("GeneralNotes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("HvacSystemGood") + .HasColumnType("INTEGER"); + + b.Property("HvacSystemNotes") + .HasColumnType("TEXT"); + + b.Property("InspectedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("InspectionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InteriorCeilingsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorCeilingsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorDoorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorDoorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorFloorsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorFloorsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWallsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWallsNotes") + .HasColumnType("TEXT"); + + b.Property("InteriorWindowsGood") + .HasColumnType("INTEGER"); + + b.Property("InteriorWindowsNotes") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenAppliancesNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCabinetsGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCabinetsNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenCountersGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenCountersNotes") + .HasColumnType("TEXT"); + + b.Property("KitchenSinkPlumbingGood") + .HasColumnType("INTEGER"); + + b.Property("KitchenSinkPlumbingNotes") + .HasColumnType("TEXT"); + + b.Property("LandscapingGood") + .HasColumnType("INTEGER"); + + b.Property("LandscapingNotes") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OverallCondition") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PlumbingSystemGood") + .HasColumnType("INTEGER"); + + b.Property("PlumbingSystemNotes") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("SmokeDetectorsGood") + .HasColumnType("INTEGER"); + + b.Property("SmokeDetectorsNotes") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("DocumentId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("PropertyId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("DueOn") + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoicedOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("LateFeeApplied") + .HasColumnType("INTEGER"); + + b.Property("LateFeeAppliedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("ReminderSent") + .HasColumnType("INTEGER"); + + b.Property("ReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.HasIndex("LeaseId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DeclinedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpectedMoveOutDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseOfferId") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PreviousLeaseId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProposedRenewalRent") + .HasColumnType("decimal(18,2)"); + + b.Property("RenewalNotes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("RenewalNotificationSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalNumber") + .HasColumnType("INTEGER"); + + b.Property("RenewalOfferedOn") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderSentOn") + .HasColumnType("TEXT"); + + b.Property("RenewalResponseOn") + .HasColumnType("TEXT"); + + b.Property("RenewalStatus") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("SignedOn") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TerminationNoticedOn") + .HasColumnType("TEXT"); + + b.Property("TerminationReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("TenantId"); + + b.ToTable("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConvertedLeaseId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OfferedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("RentalApplicationId") + .HasColumnType("TEXT"); + + b.Property("RespondedOn") + .HasColumnType("TEXT"); + + b.Property("ResponseNotes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SecurityDeposit") + .HasColumnType("decimal(18,2)"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Terms") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("RentalApplicationId"); + + b.ToTable("LeaseOffers"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActualCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AssignedTo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EstimatedCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestedBy") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("RequestedByEmail") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("RequestedByPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RequestedOn") + .HasColumnType("TEXT"); + + b.Property("ResolutionNotes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LeaseId"); + + b.HasIndex("Priority"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RequestedOn"); + + b.HasIndex("Status"); + + b.ToTable("MaintenanceRequests"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("TEXT"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UserFullName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("OwnerId"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationEmailSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("DailyLimit") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("EmailsSentToday") + .HasColumnType("INTEGER"); + + b.Property("FromEmail") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FromName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsEmailEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastEmailSentOn") + .HasColumnType("TEXT"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastErrorOn") + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyLimit") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SendGridApiKeyEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationEmailSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSMSSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountBalance") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("AccountType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CostPerSMS") + .HasPrecision(18, 4) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsSMSEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsVerified") + .HasColumnType("INTEGER"); + + b.Property("LastError") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastSMSSentOn") + .HasColumnType("TEXT"); + + b.Property("LastVerifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyCountResetOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("SMSSentThisMonth") + .HasColumnType("INTEGER"); + + b.Property("SMSSentToday") + .HasColumnType("INTEGER"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("TwilioAccountSidEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioAuthTokenEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("TwilioPhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSMSSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSettings", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowTenantDividendChoice") + .HasColumnType("INTEGER"); + + b.Property("ApplicationExpirationDays") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("AutoCalculateSecurityDeposit") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DefaultApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("DefaultDividendPaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DividendDistributionMonth") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LateFeeAutoApply") + .HasColumnType("INTEGER"); + + b.Property("LateFeeEnabled") + .HasColumnType("INTEGER"); + + b.Property("LateFeeGracePeriodDays") + .HasColumnType("INTEGER"); + + b.Property("LateFeePercentage") + .HasPrecision(5, 4) + .HasColumnType("TEXT"); + + b.Property("MaxLateFeeAmount") + .HasPrecision(18, 2) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PaymentReminderDaysBefore") + .HasColumnType("INTEGER"); + + b.Property("PaymentReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("RefundProcessingDays") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositInvestmentEnabled") + .HasColumnType("INTEGER"); + + b.Property("SecurityDepositMultiplier") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TourNoShowGracePeriodHours") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationSettings"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DocumentId") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaidOn") + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Bathrooms") + .HasMaxLength(3) + .HasColumnType("decimal(3,1)"); + + b.Property("Bedrooms") + .HasMaxLength(3) + .HasColumnType("INTEGER"); + + b.Property("City") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsAvailable") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastRoutineInspectionDate") + .HasColumnType("TEXT"); + + b.Property("MonthlyRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NextRoutineInspectionDueDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoutineInspectionIntervalMonths") + .HasColumnType("INTEGER"); + + b.Property("SquareFeet") + .HasMaxLength(7) + .HasColumnType("INTEGER"); + + b.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UnitNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Properties"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("DesiredMoveInDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("FirstContactedOn") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationState") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("InterestedPropertyId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InterestedPropertyId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.ToTable("ProspectiveTenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationFee") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApplicationFeePaid") + .HasColumnType("INTEGER"); + + b.Property("ApplicationFeePaidOn") + .HasColumnType("TEXT"); + + b.Property("ApplicationFeePaymentMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("CurrentAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CurrentCity") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CurrentRent") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("CurrentZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DecidedOn") + .HasColumnType("TEXT"); + + b.Property("DecisionBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DenialReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("EmployerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmploymentLengthMonths") + .HasColumnType("INTEGER"); + + b.Property("ExpiresOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("JobTitle") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LandlordName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("LandlordPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("MonthlyIncome") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("Reference1Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference1Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference1Relationship") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Reference2Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Reference2Phone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Reference2Relationship") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedOn"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("Status"); + + b.ToTable("RentalApplications"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SchemaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppliedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SchemaVersions"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateReceived") + .HasColumnType("TEXT"); + + b.Property("DeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DeductionsReason") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InInvestmentPool") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PoolEntryDate") + .HasColumnType("TEXT"); + + b.Property("PoolExitDate") + .HasColumnType("TEXT"); + + b.Property("RefundAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RefundMethod") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RefundProcessedDate") + .HasColumnType("TEXT"); + + b.Property("RefundReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("TransactionReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InInvestmentPool"); + + b.HasIndex("LeaseId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.ToTable("SecurityDeposits"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BaseDividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChoiceMadeOn") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InvestmentPoolId") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LeaseId") + .HasColumnType("TEXT"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("MonthsInPool") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PaymentProcessedOn") + .HasColumnType("TEXT"); + + b.Property("PaymentReference") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProrationFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("SecurityDepositId") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvestmentPoolId"); + + b.HasIndex("LeaseId"); + + b.HasIndex("SecurityDepositId"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId"); + + b.HasIndex("Year"); + + b.ToTable("SecurityDepositDividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveLeaseCount") + .HasColumnType("INTEGER"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendPerLease") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DividendsCalculatedOn") + .HasColumnType("TEXT"); + + b.Property("DividendsDistributedOn") + .HasColumnType("TEXT"); + + b.Property("EndingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationShare") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OrganizationSharePercentage") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("ReturnRate") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("StartingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TenantShareTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalEarnings") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Status"); + + b.HasIndex("Year") + .IsUnique(); + + b.ToTable("SecurityDepositInvestmentPools"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IdentificationNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("IdentificationNumber") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CalendarEventId") + .HasColumnType("TEXT"); + + b.Property("ChecklistId") + .HasColumnType("TEXT"); + + b.Property("ConductedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("Feedback") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("InterestLevel") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("ProspectiveTenantId") + .HasColumnType("TEXT"); + + b.Property("ScheduledOn") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChecklistId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("ProspectiveTenantId"); + + b.HasIndex("ScheduledOn"); + + b.HasIndex("Status"); + + b.ToTable("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("GrantedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GrantedOn") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevokedOn") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrantedBy"); + + b.HasIndex("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Shared.Components.Account.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginIP") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("LoginCount") + .HasColumnType("INTEGER"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PreviousLoginDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("EmailError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EmailSent") + .HasColumnType("INTEGER"); + + b.Property("EmailSentOn") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsRead") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReadOn") + .HasColumnType("TEXT"); + + b.Property("RecipientUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RelatedEntityId") + .HasColumnType("TEXT"); + + b.Property("RelatedEntityType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SMSError") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SMSSent") + .HasColumnType("INTEGER"); + + b.Property("SMSSentOn") + .HasColumnType("TEXT"); + + b.Property("SendEmail") + .HasColumnType("INTEGER"); + + b.Property("SendInApp") + .HasColumnType("INTEGER"); + + b.Property("SendSMS") + .HasColumnType("INTEGER"); + + b.Property("SentOn") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("IsRead"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("SentOn"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("NotificationPreferences", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("DailyDigestTime") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EmailApplicationStatusChange") + .HasColumnType("INTEGER"); + + b.Property("EmailInspectionScheduled") + .HasColumnType("INTEGER"); + + b.Property("EmailLeaseExpiring") + .HasColumnType("INTEGER"); + + b.Property("EmailMaintenanceUpdate") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("EmailPaymentReceived") + .HasColumnType("INTEGER"); + + b.Property("EnableDailyDigest") + .HasColumnType("INTEGER"); + + b.Property("EnableEmailNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableInAppNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableSMSNotifications") + .HasColumnType("INTEGER"); + + b.Property("EnableWeeklyDigest") + .HasColumnType("INTEGER"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SMSLeaseExpiringUrgent") + .HasColumnType("INTEGER"); + + b.Property("SMSMaintenanceEmergency") + .HasColumnType("INTEGER"); + + b.Property("SMSPaymentDue") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WeeklyDigestDay") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("NotificationPreferences"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Professional.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ChecklistTemplate"); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Invoice"); + + b.Navigation("Lease"); + + b.Navigation("Payment"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Note", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", "User") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationEmailSettings", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.OrganizationSMSSettings", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Professional.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvestmentPool"); + + b.Navigation("Lease"); + + b.Navigation("SecurityDeposit"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Professional.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.UserOrganization", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("GrantedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany("UserOrganizations") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("RecipientUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("NotificationPreferences", b => + { + b.HasOne("Aquiis.Professional.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Professional.Shared.Components.Account.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + + b.Navigation("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Professional.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aquiis.Professional/Infrastructure/Services/EmailService.cs b/Aquiis.Professional/Infrastructure/Services/EmailService.cs new file mode 100644 index 0000000..c1dfbf4 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Services/EmailService.cs @@ -0,0 +1,41 @@ + +using Aquiis.Professional.Core.Interfaces.Services; + +namespace Aquiis.Professional.Infrastructure.Services; + +public class EmailService : IEmailService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public EmailService(ILogger logger, IConfiguration configuration) + { + _logger = logger; + _configuration = configuration; + } + + public async Task SendEmailAsync(string to, string subject, string body) + { + // TODO: Implement with SendGrid/Mailgun in Task 2.5 + _logger.LogInformation($"[EMAIL] To: {to}, Subject: {subject}, Body: {body}"); + await Task.CompletedTask; + } + + public async Task SendEmailAsync(string to, string subject, string body, string? fromName = null) + { + _logger.LogInformation($"[EMAIL] From: {fromName}, To: {to}, Subject: {subject}"); + await Task.CompletedTask; + } + + public async Task SendTemplateEmailAsync(string to, string templateId, Dictionary templateData) + { + _logger.LogInformation($"[EMAIL TEMPLATE] To: {to}, Template: {templateId}"); + await Task.CompletedTask; + } + + public async Task ValidateEmailAddressAsync(string email) + { + // Basic validation + return await Task.FromResult(!string.IsNullOrWhiteSpace(email) && email.Contains("@")); + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Infrastructure/Services/EntityRouteHelper.cs b/Aquiis.Professional/Infrastructure/Services/EntityRouteHelper.cs new file mode 100644 index 0000000..eb2e9ca --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Services/EntityRouteHelper.cs @@ -0,0 +1,63 @@ +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Infrastructure.Services; + +/// +/// Provides centralized mapping between entity types and their navigation routes. +/// This ensures consistent URL generation across the application when navigating to entity details. +/// +public static class EntityRouteHelper +{ + private static readonly Dictionary RouteMap = new() + { + { "Lease", "/propertymanagement/leases/view" }, + { "Payment", "/propertymanagement/payments/view" }, + { "Invoice", "/propertymanagement/invoices/view" }, + { "Maintenance", "/propertymanagement/maintenance/view" }, + { "Application", "/propertymanagement/applications" }, + { "Property", "/propertymanagement/properties/edit" }, + { "Tenant", "/propertymanagement/tenants/view" }, + { "Prospect", "/PropertyManagement/ProspectiveTenants" } + }; + + /// + /// Gets the full navigation route for a given entity type and ID. + /// + /// The type of entity (e.g., "Lease", "Payment", "Maintenance") + /// The unique identifier of the entity + /// The full route path including the entity ID, or "/" if the entity type is not mapped + public static string GetEntityRoute(string? entityType, Guid entityId) + { + if (string.IsNullOrWhiteSpace(entityType)) + { + return "/"; + } + + if (RouteMap.TryGetValue(entityType, out var route)) + { + return $"{route}/{entityId}"; + } + + // Fallback to home if entity type not found + return "/"; + } + + /// + /// Checks if a route mapping exists for the given entity type. + /// + /// The type of entity to check + /// True if a route mapping exists, false otherwise + public static bool HasRoute(string? entityType) + { + return !string.IsNullOrWhiteSpace(entityType) && RouteMap.ContainsKey(entityType); + } + + /// + /// Gets all supported entity types that have route mappings. + /// + /// A collection of supported entity type names + public static IEnumerable GetSupportedEntityTypes() + { + return RouteMap.Keys; + } +} diff --git a/Aquiis.Professional/Infrastructure/Services/SMSService.cs b/Aquiis.Professional/Infrastructure/Services/SMSService.cs new file mode 100644 index 0000000..d529a23 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Services/SMSService.cs @@ -0,0 +1,29 @@ + +using Aquiis.Professional.Core.Interfaces.Services; + +namespace Aquiis.Professional.Infrastructure.Services; + +public class SMSService : ISMSService +{ + private readonly ILogger _logger; + + public SMSService(ILogger logger) + { + _logger = logger; + } + + public async Task SendSMSAsync(string phoneNumber, string message) + { + // TODO: Implement with Twilio in Task 2.5 + _logger.LogInformation($"[SMS] To: {phoneNumber}, Message: {message}"); + await Task.CompletedTask; + } + + public async Task ValidatePhoneNumberAsync(string phoneNumber) + { + // Basic validation + var digits = new string(phoneNumber.Where(char.IsDigit).ToArray()); + return await Task.FromResult(digits.Length >= 10); + } + +} \ No newline at end of file diff --git a/Aquiis.Professional/Infrastructure/Services/SendGridEmailService.cs b/Aquiis.Professional/Infrastructure/Services/SendGridEmailService.cs new file mode 100644 index 0000000..f7ab9f5 --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Services/SendGridEmailService.cs @@ -0,0 +1,276 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Interfaces.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SendGrid; +using SendGrid.Helpers.Mail; + +namespace Aquiis.Professional.Infrastructure.Services +{ + public class SendGridEmailService : IEmailService + { + private readonly ApplicationDbContext _context; + private readonly UserContextService _userContext; + private readonly ILogger _logger; + private readonly IDataProtectionProvider _dataProtection; + + private const string PROTECTION_PURPOSE = "SendGridApiKey"; + + public SendGridEmailService( + ApplicationDbContext context, + UserContextService userContext, + ILogger logger, + IDataProtectionProvider dataProtection) + { + _context = context; + _userContext = userContext; + _logger = logger; + _dataProtection = dataProtection; + } + + public async Task SendEmailAsync(string to, string subject, string body) + { + var orgId = await _userContext.GetActiveOrganizationIdAsync(); + if (orgId == null) + { + _logger.LogWarning("Cannot send email - no active organization"); + return; + } + + var settings = await GetEmailSettingsAsync(orgId.Value); + + if (!settings.IsEmailEnabled || string.IsNullOrEmpty(settings.SendGridApiKeyEncrypted)) + { + _logger.LogInformation("Email disabled for organization {OrgId}", orgId); + return; // Graceful degradation - don't throw + } + + try + { + var apiKey = DecryptApiKey(settings.SendGridApiKeyEncrypted); + var client = new SendGridClient(apiKey); + + var from = new EmailAddress(settings.FromEmail, settings.FromName); + var toAddress = new EmailAddress(to); + var msg = MailHelper.CreateSingleEmail(from, toAddress, subject, body, body); + + var response = await client.SendEmailAsync(msg); + + if (response.IsSuccessStatusCode) + { + _logger.LogInformation("Email sent successfully to {To}", to); + await UpdateUsageStatsAsync(settings); + } + else + { + var error = await response.Body.ReadAsStringAsync(); + _logger.LogError("SendGrid error {StatusCode}: {Error}", response.StatusCode, error); + settings.LastError = $"HTTP {response.StatusCode}: {error}"; + await _context.SaveChangesAsync(); + throw new Exception($"SendGrid returned {response.StatusCode}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send email via SendGrid for org {OrgId}", orgId); + settings.LastError = ex.Message; + settings.LastErrorOn = DateTime.UtcNow; + await _context.SaveChangesAsync(); + throw; + } + } + + public async Task SendEmailAsync(string to, string subject, string body, string? fromName = null) + { + // Override from name if provided + var orgId = await _userContext.GetActiveOrganizationIdAsync(); + var settings = await GetEmailSettingsAsync(orgId!.Value); + + var originalFromName = settings.FromName; + if (!string.IsNullOrEmpty(fromName)) + { + settings.FromName = fromName; + } + + await SendEmailAsync(to, subject, body); + + settings.FromName = originalFromName; + } + + public async Task SendTemplateEmailAsync(string to, string templateId, Dictionary templateData) + { + var orgId = await _userContext.GetActiveOrganizationIdAsync(); + var settings = await GetEmailSettingsAsync(orgId!.Value); + + if (!settings.IsEmailEnabled) + { + _logger.LogInformation("Email disabled for organization {OrgId}", orgId); + return; + } + + try + { + var apiKey = DecryptApiKey(settings.SendGridApiKeyEncrypted!); + var client = new SendGridClient(apiKey); + + var msg = new SendGridMessage(); + msg.SetFrom(new EmailAddress(settings.FromEmail, settings.FromName)); + msg.AddTo(new EmailAddress(to)); + msg.SetTemplateId(templateId); + msg.SetTemplateData(templateData); + + var response = await client.SendEmailAsync(msg); + + if (response.IsSuccessStatusCode) + { + await UpdateUsageStatsAsync(settings); + } + else + { + var error = await response.Body.ReadAsStringAsync(); + _logger.LogError("SendGrid template error: {Error}", error); + throw new Exception(error); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send template email via SendGrid"); + throw; + } + } + + public async Task ValidateEmailAddressAsync(string email) + { + await Task.CompletedTask; + return !string.IsNullOrWhiteSpace(email) && + email.Contains("@") && + email.Contains("."); + } + + public async Task VerifyApiKeyAsync(string apiKey) + { + try + { + var client = new SendGridClient(apiKey); + + // Test API key by fetching user profile + var response = await client.RequestAsync( + method: SendGridClient.Method.GET, + urlPath: "user/profile"); + + return response.IsSuccessStatusCode; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "SendGrid API key verification failed"); + return false; + } + } + + public async Task GetSendGridStatsAsync() + { + var orgId = await _userContext.GetActiveOrganizationIdAsync(); + var settings = await GetEmailSettingsAsync(orgId!.Value); + + if (!settings.IsEmailEnabled) + { + return new SendGridStats { IsConfigured = false }; + } + + // Optionally refresh stats from SendGrid API + // await RefreshStatsFromSendGridAsync(settings); + + return new SendGridStats + { + IsConfigured = true, + EmailsSentToday = settings.EmailsSentToday, + EmailsSentThisMonth = settings.EmailsSentThisMonth, + DailyLimit = settings.DailyLimit ?? 100, + MonthlyLimit = settings.MonthlyLimit ?? 40000, + LastEmailSentOn = settings.LastEmailSentOn, + LastVerifiedOn = settings.LastVerifiedOn, + PlanType = settings.PlanType ?? "Free", + DailyPercentUsed = settings.DailyLimit.HasValue + ? (int)((settings.EmailsSentToday / (double)settings.DailyLimit.Value) * 100) + : 0, + MonthlyPercentUsed = settings.MonthlyLimit.HasValue + ? (int)((settings.EmailsSentThisMonth / (double)settings.MonthlyLimit.Value) * 100) + : 0 + }; + } + + private async Task GetEmailSettingsAsync(Guid organizationId) + { + var settings = await _context.OrganizationEmailSettings + .FirstOrDefaultAsync(s => s.OrganizationId == organizationId && !s.IsDeleted); + + if (settings == null) + { + throw new InvalidOperationException( + $"Email settings not found for organization {organizationId}"); + } + + return settings; + } + + private async Task UpdateUsageStatsAsync(OrganizationEmailSettings settings) + { + var now = DateTime.UtcNow; + var today = now.Date; + + // Reset daily counter if needed + if (settings.DailyCountResetOn?.Date != today) + { + settings.EmailsSentToday = 0; + settings.DailyCountResetOn = today; + } + + // Reset monthly counter if needed (first of month) + if (settings.MonthlyCountResetOn?.Month != now.Month || + settings.MonthlyCountResetOn?.Year != now.Year) + { + settings.EmailsSentThisMonth = 0; + settings.MonthlyCountResetOn = new DateTime(now.Year, now.Month, 1); + } + + settings.EmailsSentToday++; + settings.EmailsSentThisMonth++; + settings.LastEmailSentOn = now; + settings.StatsLastUpdatedOn = now; + + await _context.SaveChangesAsync(); + } + + private string DecryptApiKey(string encrypted) + { + var protector = _dataProtection.CreateProtector(PROTECTION_PURPOSE); + return protector.Unprotect(encrypted); + } + + public string EncryptApiKey(string apiKey) + { + var protector = _dataProtection.CreateProtector(PROTECTION_PURPOSE); + return protector.Protect(apiKey); + } + } + + public class SendGridStats + { + public bool IsConfigured { get; set; } + public int EmailsSentToday { get; set; } + public int EmailsSentThisMonth { get; set; } + public int DailyLimit { get; set; } + public int MonthlyLimit { get; set; } + public int DailyPercentUsed { get; set; } + public int MonthlyPercentUsed { get; set; } + public DateTime? LastEmailSentOn { get; set; } + public DateTime? LastVerifiedOn { get; set; } + public string? PlanType { get; set; } + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Infrastructure/Services/TwilioSMSService.cs b/Aquiis.Professional/Infrastructure/Services/TwilioSMSService.cs new file mode 100644 index 0000000..9a732bf --- /dev/null +++ b/Aquiis.Professional/Infrastructure/Services/TwilioSMSService.cs @@ -0,0 +1,222 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Interfaces.Services; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Shared.Services; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Twilio; +using Twilio.Rest.Api.V2010.Account; +using Twilio.Types; + +namespace Aquiis.Professional.Infrastructure.Services +{ + public class TwilioSMSService : ISMSService + { + private readonly ApplicationDbContext _context; + private readonly UserContextService _userContext; + private readonly ILogger _logger; + private readonly IDataProtectionProvider _dataProtection; + + private const string ACCOUNT_SID_PURPOSE = "TwilioAccountSid"; + private const string AUTH_TOKEN_PURPOSE = "TwilioAuthToken"; + + public TwilioSMSService( + ApplicationDbContext context, + UserContextService userContext, + ILogger logger, + IDataProtectionProvider dataProtection) + { + _context = context; + _userContext = userContext; + _logger = logger; + _dataProtection = dataProtection; + } + + public async Task SendSMSAsync(string phoneNumber, string message) + { + var orgId = await _userContext.GetActiveOrganizationIdAsync(); + if (orgId == null) + { + _logger.LogWarning("Cannot send SMS - no active organization"); + return; + } + + var settings = await GetSMSSettingsAsync(orgId.Value); + + if (!settings.IsSMSEnabled || + string.IsNullOrEmpty(settings.TwilioAccountSidEncrypted) || + string.IsNullOrEmpty(settings.TwilioAuthTokenEncrypted)) + { + _logger.LogInformation("SMS disabled for organization {OrgId}", orgId); + return; // Graceful degradation + } + + try + { + var accountSid = DecryptAccountSid(settings.TwilioAccountSidEncrypted); + var authToken = DecryptAuthToken(settings.TwilioAuthTokenEncrypted); + + TwilioClient.Init(accountSid, authToken); + + var messageResource = await MessageResource.CreateAsync( + body: message, + from: new PhoneNumber(settings.TwilioPhoneNumber), + to: new PhoneNumber(phoneNumber)); + + if (messageResource.Status == MessageResource.StatusEnum.Queued || + messageResource.Status == MessageResource.StatusEnum.Sent) + { + _logger.LogInformation("SMS sent successfully to {PhoneNumber}", phoneNumber); + await UpdateUsageStatsAsync(settings); + } + else + { + _logger.LogError("Twilio SMS status: {Status}", messageResource.Status); + settings.LastError = $"Status: {messageResource.Status}"; + await _context.SaveChangesAsync(); + throw new Exception($"SMS send failed with status: {messageResource.Status}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send SMS via Twilio for org {OrgId}", orgId); + settings.LastError = ex.Message; + await _context.SaveChangesAsync(); + throw; + } + } + + public async Task ValidatePhoneNumberAsync(string phoneNumber) + { + // Basic validation + var digits = new string(phoneNumber.Where(char.IsDigit).ToArray()); + return await Task.FromResult(digits.Length >= 10); + } + + public async Task VerifyTwilioCredentialsAsync(string accountSid, string authToken, string phoneNumber) + { + try + { + TwilioClient.Init(accountSid, authToken); + + // Verify by fetching the incoming phone number + var incomingPhoneNumber = await IncomingPhoneNumberResource.ReadAsync( + phoneNumber: new PhoneNumber(phoneNumber)); + + return incomingPhoneNumber.Any(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Twilio credentials verification failed"); + return false; + } + } + + public async Task GetTwilioStatsAsync() + { + var orgId = await _userContext.GetActiveOrganizationIdAsync(); + var settings = await GetSMSSettingsAsync(orgId!.Value); + + if (!settings.IsSMSEnabled) + { + return new TwilioStats { IsConfigured = false }; + } + + return new TwilioStats + { + IsConfigured = true, + SMSSentToday = settings.SMSSentToday, + SMSSentThisMonth = settings.SMSSentThisMonth, + AccountBalance = settings.AccountBalance ?? 0, + CostPerSMS = settings.CostPerSMS ?? 0.0075m, + EstimatedMonthlyCost = settings.SMSSentThisMonth * (settings.CostPerSMS ?? 0.0075m), + LastSMSSentOn = settings.LastSMSSentOn, + LastVerifiedOn = settings.LastVerifiedOn, + AccountType = settings.AccountType ?? "Unknown" + }; + } + + private async Task GetSMSSettingsAsync(Guid organizationId) + { + var settings = await _context.OrganizationSMSSettings + .FirstOrDefaultAsync(s => s.OrganizationId == organizationId && !s.IsDeleted); + + if (settings == null) + { + throw new InvalidOperationException( + $"SMS settings not found for organization {organizationId}"); + } + + return settings; + } + + private async Task UpdateUsageStatsAsync(OrganizationSMSSettings settings) + { + var now = DateTime.UtcNow; + var today = now.Date; + + // Reset daily counter if needed + if (settings.DailyCountResetOn?.Date != today) + { + settings.SMSSentToday = 0; + settings.DailyCountResetOn = today; + } + + // Reset monthly counter if needed + if (settings.MonthlyCountResetOn?.Month != now.Month || + settings.MonthlyCountResetOn?.Year != now.Year) + { + settings.SMSSentThisMonth = 0; + settings.MonthlyCountResetOn = new DateTime(now.Year, now.Month, 1); + } + + settings.SMSSentToday++; + settings.SMSSentThisMonth++; + settings.LastSMSSentOn = now; + settings.StatsLastUpdatedOn = now; + + await _context.SaveChangesAsync(); + } + + private string DecryptAccountSid(string encrypted) + { + var protector = _dataProtection.CreateProtector(ACCOUNT_SID_PURPOSE); + return protector.Unprotect(encrypted); + } + + private string DecryptAuthToken(string encrypted) + { + var protector = _dataProtection.CreateProtector(AUTH_TOKEN_PURPOSE); + return protector.Unprotect(encrypted); + } + + public string EncryptAccountSid(string accountSid) + { + var protector = _dataProtection.CreateProtector(ACCOUNT_SID_PURPOSE); + return protector.Protect(accountSid); + } + + public string EncryptAuthToken(string authToken) + { + var protector = _dataProtection.CreateProtector(AUTH_TOKEN_PURPOSE); + return protector.Protect(authToken); + } + } + + public class TwilioStats + { + public bool IsConfigured { get; set; } + public int SMSSentToday { get; set; } + public int SMSSentThisMonth { get; set; } + public decimal AccountBalance { get; set; } + public decimal CostPerSMS { get; set; } + public decimal EstimatedMonthlyCost { get; set; } + public DateTime? LastSMSSentOn { get; set; } + public DateTime? LastVerifiedOn { get; set; } + public string AccountType { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Program.cs b/Aquiis.Professional/Program.cs new file mode 100644 index 0000000..86040ca --- /dev/null +++ b/Aquiis.Professional/Program.cs @@ -0,0 +1,586 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; +using Aquiis.Professional.Shared.Components.Account; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Features.PropertyManagement; +using Aquiis.Professional.Core.Constants; +using Aquiis.Professional.Core.Interfaces; +using Aquiis.Professional.Application.Services; +using Aquiis.Professional.Application.Services.PdfGenerators; +using Aquiis.Professional.Shared.Services; +using Aquiis.Professional.Shared.Authorization; +using ElectronNET.API; +using Microsoft.Extensions.Options; +using Aquiis.Professional.Application.Services.Workflows; +using Aquiis.Professional.Core.Interfaces.Services; +using Aquiis.Professional.Infrastructure.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Configure for Electron +builder.WebHost.UseElectron(args); + +// Configure URLs - use specific port for Electron +if (HybridSupport.IsElectronActive) +{ + builder.WebHost.UseUrls("http://localhost:8888"); +} + + + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + + + //Added for session state +builder.Services.AddDistributedMemoryCache(); + +builder.Services.AddSession(options => +{ + options.IdleTimeout = TimeSpan.FromMinutes(10); + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; +}); + + +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddAuthentication(options => + { + options.DefaultScheme = IdentityConstants.ApplicationScheme; + options.DefaultSignInScheme = IdentityConstants.ExternalScheme; + }) + .AddIdentityCookies(); + +// Get database connection string (uses Electron user data path when running as desktop app) +var connectionString = HybridSupport.IsElectronActive + ? await ElectronPathService.GetConnectionStringAsync(builder.Configuration) + : builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + +builder.Services.AddDbContext(options => + options.UseSqlite(connectionString)); +builder.Services.AddDbContextFactory(options => + options.UseSqlite(connectionString), ServiceLifetime.Scoped); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + +builder.Services.AddIdentityCore(options => { + + // For desktop app, simplify registration (email confirmation can be enabled later via settings) + options.SignIn.RequireConfirmedAccount = !HybridSupport.IsElectronActive; + options.Password.RequireDigit = true; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = true; + options.Password.RequireLowercase = true; + }) + .AddRoles() + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders(); + +// Configure organization-based authorization +builder.Services.AddAuthorization(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +builder.Services.Configure(builder.Configuration.GetSection("ApplicationSettings")); +builder.Services.AddSingleton, IdentityNoOpEmailSender>(); + + + +// Configure cookie authentication +builder.Services.ConfigureApplicationCookie(options => +{ + options.LoginPath = "/Account/Login"; + options.LogoutPath = "/Account/Logout"; + options.AccessDeniedPath = "/Account/AccessDenied"; + + // For Electron desktop app, we can use longer cookie lifetime + if (HybridSupport.IsElectronActive) + { + options.ExpireTimeSpan = TimeSpan.FromDays(30); + options.SlidingExpiration = true; + } + + options.Events.OnSignedIn = async context => + { + // Track user login + if (context.Principal != null) + { + var userManager = context.HttpContext.RequestServices.GetRequiredService>(); + var user = await userManager.GetUserAsync(context.Principal); + if (user != null) + { + user.PreviousLoginDate = user.LastLoginDate; + user.LastLoginDate = DateTime.UtcNow; + user.LoginCount++; + user.LastLoginIP = context.HttpContext.Connection.RemoteIpAddress?.ToString(); + await userManager.UpdateAsync(user); + } + } + }; + options.Events.OnRedirectToAccessDenied = context => + { + // Check if user is locked out and redirect to lockout page + if (context.HttpContext.User.Identity?.IsAuthenticated == true) + { + var userManager = context.HttpContext.RequestServices.GetRequiredService>(); + var user = userManager.GetUserAsync(context.HttpContext.User).Result; + if (user != null && userManager.IsLockedOutAsync(user).Result) + { + context.Response.Redirect("/Account/Lockout"); + return Task.CompletedTask; + } + } + context.Response.Redirect(context.RedirectUri); + return Task.CompletedTask; + }; +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); // New refactored service +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // Concrete class for services that need it +builder.Services.AddScoped(sp => sp.GetRequiredService()); // Interface alias +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +// Add to service registration section +builder.Services.AddScoped(); + +// Phase 2.4: Notification Infrastructure +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Phase 2.5: Email/SMS Integration +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Workflow services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Configure and register session timeout service +builder.Services.AddScoped(sp => +{ + var config = sp.GetRequiredService(); + var service = new SessionTimeoutService(); + + // Load configuration + var timeoutMinutes = config.GetValue("SessionTimeout:InactivityTimeoutMinutes", 30); + var warningMinutes = config.GetValue("SessionTimeout:WarningDurationMinutes", 2); + var enabled = config.GetValue("SessionTimeout:Enabled", true); + + // Disable for Electron in development, or use longer timeout + if (HybridSupport.IsElectronActive) + { + timeoutMinutes = 120; // 2 hours for desktop app + enabled = false; // Typically disabled for desktop + } + + service.InactivityTimeout = TimeSpan.FromMinutes(timeoutMinutes); + service.WarningDuration = TimeSpan.FromMinutes(warningMinutes); + service.IsEnabled = enabled; + + return service; +}); + +// Register background service for scheduled tasks +builder.Services.AddHostedService(); + +var app = builder.Build(); + +// Ensure database is created and migrations are applied +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + var backupService = scope.ServiceProvider.GetRequiredService(); + + // For Electron, handle database initialization and migrations + if (HybridSupport.IsElectronActive) + { + try + { + var pathService = scope.ServiceProvider.GetRequiredService(); + var dbPath = await pathService.GetDatabasePathAsync(); + var stagedRestorePath = $"{dbPath}.restore_pending"; + + // Check if there's a staged restore waiting + if (File.Exists(stagedRestorePath)) + { + app.Logger.LogInformation("Found staged restore file, applying it now"); + + // Backup current database if it exists + if (File.Exists(dbPath)) + { + var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff"); + var beforeRestorePath = $"{dbPath}.beforeRestore.{timestamp}"; + File.Move(dbPath, beforeRestorePath); + app.Logger.LogInformation("Current database backed up to: {Path}", beforeRestorePath); + } + + // Move staged restore into place + File.Move(stagedRestorePath, dbPath); + app.Logger.LogInformation("Staged restore applied successfully"); + } + + var dbExists = File.Exists(dbPath); + + // Check database health if it exists + if (dbExists) + { + var (isHealthy, healthMessage) = await backupService.ValidateDatabaseHealthAsync(); + if (!isHealthy) + { + app.Logger.LogWarning("Database health check failed: {Message}", healthMessage); + app.Logger.LogWarning("Attempting automatic recovery from corruption"); + + var (recovered, recoveryMessage) = await backupService.AutoRecoverFromCorruptionAsync(); + if (recovered) + { + app.Logger.LogInformation("Database recovered successfully: {Message}", recoveryMessage); + } + else + { + app.Logger.LogError("Database recovery failed: {Message}", recoveryMessage); + + // Instead of throwing, rename corrupted database and create new one + var corruptedPath = $"{dbPath}.corrupted.{DateTime.Now:yyyyMMddHHmmss}"; + File.Move(dbPath, corruptedPath); + app.Logger.LogWarning("Corrupted database moved to: {CorruptedPath}", corruptedPath); + app.Logger.LogInformation("Creating new database..."); + + dbExists = false; // Treat as new installation + } + } + } + + if (dbExists) + { + // Existing installation - apply any pending migrations + app.Logger.LogInformation("Checking for migrations on existing database at {DbPath}", dbPath); + + var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); + if (pendingMigrations.Any()) + { + app.Logger.LogInformation("Found {Count} pending migrations", pendingMigrations.Count()); + + // Create backup before migration using the backup service + var backupPath = await backupService.CreatePreMigrationBackupAsync(); + if (backupPath != null) + { + app.Logger.LogInformation("Database backed up to {BackupPath}", backupPath); + } + + try + { + // Apply migrations + await context.Database.MigrateAsync(); + app.Logger.LogInformation("Migrations applied successfully"); + + // Verify database health after migration + var (isHealthy, healthMessage) = await backupService.ValidateDatabaseHealthAsync(); + if (!isHealthy) + { + app.Logger.LogError("Database corrupted after migration: {Message}", healthMessage); + + if (backupPath != null) + { + app.Logger.LogInformation("Rolling back to pre-migration backup"); + await backupService.RestoreFromBackupAsync(backupPath); + } + + throw new Exception($"Migration caused database corruption: {healthMessage}"); + } + } + catch (Exception migrationEx) + { + app.Logger.LogError(migrationEx, "Migration failed, attempting to restore from backup"); + + if (backupPath != null) + { + var restored = await backupService.RestoreFromBackupAsync(backupPath); + if (restored) + { + app.Logger.LogInformation("Database restored from pre-migration backup"); + } + } + + throw; + } + } + else + { + app.Logger.LogInformation("Database is up to date"); + } + } + else + { + // New installation - create database with migrations + app.Logger.LogInformation("Creating new database for Electron app at {DbPath}", dbPath); + await context.Database.MigrateAsync(); + app.Logger.LogInformation("Database created successfully"); + + // Create initial backup after database creation + await backupService.CreateBackupAsync("InitialSetup"); + } + } + catch (Exception ex) + { + app.Logger.LogError(ex, "Failed to initialize database for Electron"); + throw; + } + } + else + { + // Web mode - ensure migrations are applied + try + { + app.Logger.LogInformation("Applying database migrations for web mode"); + + // Get database path for web mode + var webConnectionString = builder.Configuration.GetConnectionString("DefaultConnection"); + if (!string.IsNullOrEmpty(webConnectionString)) + { + var dbPath = webConnectionString + .Replace("Data Source=", "") + .Replace("DataSource=", "") + .Split(';')[0] + .Trim(); + + if (!Path.IsPathRooted(dbPath)) + { + dbPath = Path.Combine(Directory.GetCurrentDirectory(), dbPath); + } + + var stagedRestorePath = $"{dbPath}.restore_pending"; + + // Check if there's a staged restore waiting + if (File.Exists(stagedRestorePath)) + { + app.Logger.LogInformation("Found staged restore file for web mode, applying it now"); + + // Close all database connections + await context.Database.CloseConnectionAsync(); + + // Clear SQLite connection pool + Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); + + // Wait for connections to close + await Task.Delay(500); + + // Backup current database if it exists + if (File.Exists(dbPath)) + { + var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff"); + var beforeRestorePath = $"{dbPath}.beforeRestore.{timestamp}"; + File.Move(dbPath, beforeRestorePath); + app.Logger.LogInformation("Current database backed up to: {Path}", beforeRestorePath); + } + + // Move staged restore into place + File.Move(stagedRestorePath, dbPath); + app.Logger.LogInformation("Staged restore applied successfully for web mode"); + } + } + + // Check if there are pending migrations + var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); + var isNewDatabase = !pendingMigrations.Any() && !(await context.Database.GetAppliedMigrationsAsync()).Any(); + + if (pendingMigrations.Any()) + { + // Create backup before migration + var backupPath = await backupService.CreatePreMigrationBackupAsync(); + if (backupPath != null) + { + app.Logger.LogInformation("Database backed up to {BackupPath}", backupPath); + } + } + + await context.Database.MigrateAsync(); + app.Logger.LogInformation("Database migrations applied successfully"); + + // Create initial backup after creating a new database + if (isNewDatabase) + { + app.Logger.LogInformation("New database created, creating initial backup"); + await backupService.CreateBackupAsync("InitialSetup"); + } + } + catch (Exception ex) + { + app.Logger.LogError(ex, "Failed to apply database migrations"); + throw; + } + } + + // Validate and update schema version + var schemaService = scope.ServiceProvider.GetRequiredService(); + var appSettings = scope.ServiceProvider.GetRequiredService>().Value; + + app.Logger.LogInformation("Checking schema version..."); + var currentDbVersion = await schemaService.GetCurrentSchemaVersionAsync(); + app.Logger.LogInformation("Current database schema version: {Version}", currentDbVersion ?? "null"); + + if (currentDbVersion == null) + { + // New database or table exists but empty - set initial schema version + app.Logger.LogInformation("Setting initial schema version to {Version}", appSettings.SchemaVersion); + await schemaService.UpdateSchemaVersionAsync(appSettings.SchemaVersion, "Initial schema version"); + app.Logger.LogInformation("Schema version initialized successfully"); + } + else if (currentDbVersion != appSettings.SchemaVersion) + { + // Schema version mismatch - log warning but allow startup + app.Logger.LogWarning("Schema version mismatch! Database: {DbVersion}, Application: {AppVersion}", + currentDbVersion, appSettings.SchemaVersion); + } + else + { + app.Logger.LogInformation("Schema version validated: {Version}", currentDbVersion); + } +} + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseMigrationsEndPoint(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseSession(); + +// Only use HTTPS redirection in web mode, not in Electron +if (!HybridSupport.IsElectronActive) +{ + app.UseHttpsRedirection(); +} + +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +// Add additional endpoints required by the Identity /Account Razor components. +app.MapAdditionalIdentityEndpoints(); + +// Add session refresh endpoint for session timeout feature +app.MapPost("/api/session/refresh", async (HttpContext context) => +{ + // Simply accessing the session refreshes it + context.Session.SetString("LastRefresh", DateTime.UtcNow.ToString("O")); + await Task.CompletedTask; + return Results.Ok(new { success = true, timestamp = DateTime.UtcNow }); +}).RequireAuthorization(); + +// Create system service account for background jobs +using (var scope = app.Services.CreateScope()) +{ + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var systemUser = await userManager.FindByIdAsync(ApplicationConstants.SystemUser.Id); + if (systemUser == null) + { + systemUser = new ApplicationUser + { + Id = ApplicationConstants.SystemUser.Id, + UserName = ApplicationConstants.SystemUser.Email, // UserName = Email in this system + NormalizedUserName = ApplicationConstants.SystemUser.Email.ToUpperInvariant(), + Email = ApplicationConstants.SystemUser.Email, + NormalizedEmail = ApplicationConstants.SystemUser.Email.ToUpperInvariant(), + EmailConfirmed = true, + FirstName = ApplicationConstants.SystemUser.FirstName, + LastName = ApplicationConstants.SystemUser.LastName, + LockoutEnabled = true, // CRITICAL: Account is locked by default + LockoutEnd = DateTimeOffset.MaxValue, // Locked until end of time + AccessFailedCount = 0 + }; + + // Create without password - cannot be used for login + var result = await userManager.CreateAsync(systemUser); + + if (!result.Succeeded) + { + throw new Exception($"Failed to create system user: {string.Join(", ", result.Errors.Select(e => e.Description))}"); + } + + // DO NOT assign to any organization - service account is org-agnostic + // DO NOT create UserOrganizations entries + // DO NOT set ActiveOrganizationId + } +} + +// Start the app for Electron +await app.StartAsync(); + +// Open Electron window +if (HybridSupport.IsElectronActive) +{ + var window = await Electron.WindowManager.CreateWindowAsync(new ElectronNET.API.Entities.BrowserWindowOptions + { + Width = 1400, + Height = 900, + MinWidth = 800, + MinHeight = 600, + Show = false + }); + + window.OnReadyToShow += () => window.Show(); + window.SetTitle("Aquiis Property Management"); + + // Open DevTools in development mode for debugging + if (app.Environment.IsDevelopment()) + { + window.WebContents.OpenDevTools(); + app.Logger.LogInformation("DevTools opened for debugging"); + } + + // Gracefully shutdown when window is closed + window.OnClosed += () => + { + app.Logger.LogInformation("Electron window closed, shutting down application"); + Electron.App.Quit(); + }; +} + +await app.WaitForShutdownAsync(); diff --git a/Aquiis.Professional/Properties/launchSettings.json b/Aquiis.Professional/Properties/launchSettings.json new file mode 100644 index 0000000..4e83bb2 --- /dev/null +++ b/Aquiis.Professional/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5105", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7198;http://localhost:5105", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/Aquiis.Professional/README.md b/Aquiis.Professional/README.md new file mode 100644 index 0000000..0522676 --- /dev/null +++ b/Aquiis.Professional/README.md @@ -0,0 +1,513 @@ +# Aquiis - Property Management System + +![.NET 9.0](https://img.shields.io/badge/.NET-9.0-blue) +![ASP.NET Core](https://img.shields.io/badge/ASP.NET%20Core-9.0-blueviolet) +![Blazor Server](https://img.shields.io/badge/Blazor-Server-orange) +![Entity Framework](https://img.shields.io/badge/Entity%20Framework-9.0-green) +![SQLite](https://img.shields.io/badge/Database-SQLite-lightblue) + +A comprehensive web-based property management system built with ASP.NET Core 9.0 and Blazor Server. Aquiis streamlines rental property management for property owners, managers, and tenants with an intuitive interface and robust feature set. + +## 🏢 Overview + +Aquiis is designed to simplify property management operations through a centralized platform that handles everything from property listings and tenant management to lease tracking and document storage. Built with modern web technologies, it provides a responsive, secure, and scalable solution for property management professionals. + +## ✨ Key Features + +### 🏠 Property Management + +- **Property Portfolio** - Comprehensive property listings with detailed information +- **Property Details** - Address, type, rent, bedrooms, bathrooms, square footage +- **Availability Tracking** - Real-time property availability status +- **Property Photos** - Image management and gallery support +- **Search & Filter** - Advanced property search and filtering capabilities +- **Property Analytics** - Dashboard with property performance metrics + +### 👥 Tenant Management + +- **Tenant Profiles** - Complete tenant information management +- **Contact Management** - Phone, email, emergency contacts +- **Tenant History** - Track tenant interactions and lease history +- **Tenant Portal** - Dedicated tenant dashboard and self-service features +- **Communication Tools** - Built-in messaging and notification system +- **Tenant Screening** - Application and background check workflow + +### 📄 Lease Management + +- **Lease Creation** - Digital lease agreement generation +- **Lease Tracking** - Active, pending, expired, and terminated lease monitoring +- **Rent Tracking** - Monthly rent amounts and payment schedules +- **Security Deposits** - Deposit tracking and management +- **Lease Renewals** - Automated renewal notifications and processing +- **Terms Management** - Flexible lease terms and conditions + +### 💰 Financial Management + +- **Payment Tracking** - Rent payment monitoring and history +- **Invoice Generation** - Automated invoice creation and delivery +- **Payment Methods** - Multiple payment option support +- **Financial Reporting** - Revenue and expense reporting +- **Late Fee Management** - Automatic late fee calculation and tracking +- **Security Deposit Tracking** - Deposit handling and return processing + +### 📁 Document Management + +- **File Storage** - Secure document upload and storage +- **Document Categories** - Organized by type (leases, receipts, photos, etc.) +- **Version Control** - Document revision tracking +- **Digital Signatures** - Electronic signature support +- **Document Sharing** - Secure document sharing with tenants +- **Bulk Operations** - Mass document upload and organization + +### 🔐 User Management & Security + +- **Role-Based Access** - Administrator, Property Manager, and Tenant roles +- **Authentication** - Secure login with ASP.NET Core Identity +- **User Profiles** - Comprehensive user account management +- **Permission Management** - Granular access control +- **Activity Tracking** - User login and activity monitoring +- **Data Security** - Encrypted data storage and transmission + +### 🎛️ Administration Features + +- **User Administration** - Complete user account management +- **System Configuration** - Application settings and preferences +- **Application Monitoring** - System health and performance tracking +- **Backup Management** - Data backup and recovery tools +- **Audit Logging** - Comprehensive activity and change tracking + +## 🛠️ Technology Stack + +### Backend + +- **Backend**: ASP.NET Core 9.0 +- **UI Framework**: Blazor Server +- **Database**: SQLite with Entity Framework Core 9.0 +- **Authentication**: ASP.NET Core Identity +- **Architecture**: Clean Architecture with vertical slice organization + +### Frontend + +- **UI Components**: Blazor Server Components +- **Styling**: Bootstrap 5 with custom CSS +- **Icons**: Bootstrap Icons +- **Responsive Design**: Mobile-first responsive layout +- **Real-time Updates**: Blazor Server SignalR integration + +### Development Tools + +- **IDE**: Visual Studio Code with C# extension +- **Database Tools**: Entity Framework Core Tools +- **Version Control**: Git with GitHub integration +- **Package Management**: NuGet +- **Build System**: .NET SDK build system + +## 📋 Prerequisites + +- [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) +- [Git](https://git-scm.com/) +- [Visual Studio Code](https://code.visualstudio.com/) (recommended) or Visual Studio 2022 +- [C# Dev Kit Extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) for VS Code + +## 🚀 Quick Start + +### 1. Clone the Repository + +```bash +git clone https://github.com/xnodeoncode/Aquiis.git +cd Aquiis +``` + +### 2. Build the Application + +```bash +dotnet build +``` + +### 3. Run Database Migrations + +```bash +cd Aquiis.Professional +dotnet ef database update +``` + +### 4. Start the Development Server + +```bash +dotnet run +``` + +### 5. Access the Application + +Open your browser and navigate to: + +- **HTTPS**: https://localhost:7244 +- **HTTP**: http://localhost:5244 + +## 🔧 Development Setup + +### Visual Studio Code Setup + +The project includes pre-configured VS Code settings: + +1. Open the workspace file: `Aquiis.code-workspace` +2. Install recommended extensions when prompted +3. Use **F5** to start debugging +4. Use **Ctrl+Shift+P** → "Tasks: Run Task" for build operations + +### Available Tasks + +- **build** - Debug build (default) +- **build-release** - Release build +- **watch** - Hot reload development +- **publish** - Production publish +- **clean** - Clean build artifacts + +### Database Management + +#### Manual Database Scripts + +SQL scripts for manual database operations are located in: + +```bash +cd Infrastructure/Data/Scripts +# Available scripts: +# 00_InitialSchema.sql - Initial database schema +# updateTenant.sql - Tenant table updates +``` + +#### Entity Framework Commands + +```bash +# Create new migration +dotnet ef migrations add [MigrationName] + +# Update database +dotnet ef database update + +# Remove last migration +dotnet ef migrations remove +``` + +## 📁 Project Structure + +The application follows Clean Architecture principles with clear separation of concerns: + +``` +Aquiis.Professional/ +├── Core/ # Domain Layer (no dependencies) +│ ├── Entities/ # Domain models & business entities +│ │ ├── BaseModel.cs # Base entity with common properties +│ │ ├── Property.cs # Property entity +│ │ ├── Tenant.cs # Tenant entity +│ │ ├── Lease.cs # Lease entity +│ │ ├── SecurityDeposit.cs # Security deposit entity +│ │ └── ... # Other domain entities +│ └── Constants/ # Application constants +│ ├── ApplicationConstants.cs +│ └── ApplicationSettings.cs +│ +├── Infrastructure/ # Infrastructure Layer +│ ├── Data/ # Database & persistence +│ │ ├── ApplicationDbContext.cs # EF Core DbContext +│ │ ├── Migrations/ # EF Core migrations (44 files) +│ │ ├── Scripts/ # SQL scripts for manual operations +│ │ └── Backups/ # Database backups +│ └── Services/ # External service implementations +│ +├── Application/ # Application Layer (business logic) +│ └── Services/ # Domain services +│ ├── PropertyManagementService.cs +│ ├── SecurityDepositService.cs +│ ├── TenantConversionService.cs +│ ├── FinancialReportService.cs +│ ├── ChecklistService.cs +│ ├── CalendarEventService.cs +│ ├── NoteService.cs +│ └── PdfGenerators/ # PDF generation services +│ ├── LeasePdfGenerator.cs +│ ├── InvoicePdfGenerator.cs +│ ├── PaymentPdfGenerator.cs +│ └── ... +│ +├── Features/ # Presentation Layer (Vertical Slices) +│ ├── PropertyManagement/ # Property management features +│ │ ├── Properties/ # Property CRUD & management +│ │ ├── Tenants/ # Tenant management +│ │ ├── Leases/ # Lease management +│ │ ├── SecurityDeposits/ # Security deposit tracking +│ │ ├── Payments/ # Payment processing +│ │ ├── Invoices/ # Invoice management +│ │ ├── Documents/ # Document management +│ │ ├── Inspections/ # Property inspections +│ │ ├── MaintenanceRequests/ # Maintenance tracking +│ │ ├── Applications/ # Rental applications +│ │ ├── Checklists/ # Checklists & templates +│ │ ├── Reports/ # Financial & operational reports +│ │ └── Calendar.razor # Calendar view +│ └── Administration/ # Admin features +│ ├── Application/ # Application screening +│ ├── PropertyManagement/ # Property admin +│ ├── Settings/ # System settings +│ ├── Users/ # User management +│ └── Dashboard.razor +│ +├── Shared/ # Shared UI Layer +│ ├── Layout/ # Layout components +│ │ ├── MainLayout.razor +│ │ └── NavMenu.razor +│ ├── Components/ # Reusable UI components +│ │ ├── Account/ # Authentication components +│ │ ├── Pages/ # Shared pages (Home, About, Error) +│ │ ├── NotesTimeline.razor +│ │ ├── SessionTimeoutModal.razor +│ │ └── ToastContainer.razor +│ └── Services/ # UI-specific services +│ ├── ToastService.cs +│ ├── ThemeService.cs +│ ├── SessionTimeoutService.cs +│ ├── UserContextService.cs +│ └── DocumentService.cs +│ +├── Components/ # Root Blazor components +│ ├── App.razor # App root component +│ ├── Routes.razor # Routing configuration +│ └── _Imports.razor # Global using directives +│ +├── Utilities/ # Helper utilities +│ ├── CalendarEventRouter.cs +│ └── SchedulableEntityRegistry.cs +│ +├── wwwroot/ # Static files +│ ├── assets/ # Images & static assets +│ ├── js/ # JavaScript files +│ └── lib/ # Client libraries +│ +├── Program.cs # Application entry point +├── appsettings.json # Configuration +└── appsettings.Development.json # Development config +``` + +### Architecture Principles + +**Clean Architecture Layers:** + +``` +Features → Application → Core + ↓ +Infrastructure → Core + ↓ +Shared → Core +``` + +**Dependency Rules:** + +- ✅ **Core** has NO dependencies (pure domain logic) +- ✅ **Infrastructure** depends only on Core (data access) +- ✅ **Application** depends only on Core (business logic) +- ✅ **Features** depends on Application + Core (UI features) +- ✅ **Shared** depends on Core (cross-cutting UI) + +**Benefits:** + +- **Separation of Concerns**: Domain, business logic, data access, and UI clearly separated +- **Testability**: Each layer can be tested independently +- **Maintainability**: Easy to locate and modify specific functionality +- **Scalability**: Simple to add new features as vertical slices +- **Reusability**: Domain and application layers can be shared across projects + +## 🔑 Default User Roles + +The system includes three primary user roles: + +### Administrator + +- Full system access +- User management capabilities +- System configuration +- All property management features + +### Property Manager + +- Property portfolio management +- Tenant management +- Lease administration +- Financial tracking +- Document management + +### Tenant + +- Personal dashboard +- Lease information access +- Payment history +- Maintenance requests +- Document viewing + +## 🎯 Key Components + +### Property Management Service + +Core business logic service in the Application layer: + +- Property CRUD operations +- Tenant management workflows +- Lease tracking and renewals +- Document handling and storage +- Financial calculations +- Entity relationship management + +### Authentication & Authorization + +- ASP.NET Core Identity integration +- Role-based access control +- Secure session management +- Password policies +- Account lockout protection + +### Database Architecture + +- Entity Framework Core with SQLite +- Code-first approach with migrations +- Optimized indexing for performance +- Foreign key constraints +- Soft delete patterns + +## 📊 Dashboard Features + +### Property Manager Dashboard + +- Total properties count +- Available properties metrics +- Active lease tracking +- Tenant statistics +- Recent activity feed +- Quick action buttons + +### Administrator Dashboard + +- User account metrics +- System health monitoring +- Application statistics +- Administrative quick actions +- Recent system activity + +### Tenant Dashboard + +- Personal lease information +- Payment history +- Maintenance requests +- Document access +- Communication center + +## 🔧 Configuration + +### Application Settings + +Configuration is managed through: + +- `appsettings.json` - Base configuration +- `appsettings.Development.json` - Development overrides +- Environment variables +- User secrets (for sensitive data) + +### Key Configuration Areas + +- Database connection strings +- Authentication settings +- File storage configuration +- Email service settings +- Application-specific settings + +## 🚀 Deployment + +### Prerequisites for Production + +- Windows/Linux server with .NET 9.0 runtime +- IIS or reverse proxy (nginx/Apache) +- SSL certificate for HTTPS +- Database server (or SQLite for smaller deployments) + +### Build for Production + +```bash +dotnet publish -c Release -o ./publish +``` + +### Environment Variables + +Set the following for production: + +```bash +ASPNETCORE_ENVIRONMENT=Production +ASPNETCORE_URLS=https://+:443;http://+:80 +ConnectionStrings__DefaultConnection=[your-connection-string] +``` + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +### Development Guidelines + +- Follow C# coding conventions +- Use meaningful commit messages +- Update documentation for new features +- Add unit tests for new functionality +- Ensure responsive design compatibility + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 🆘 Support + +### Documentation + +- Check the `REVISIONS.md` file for recent changes +- Review component-specific README files in subdirectories +- Refer to ASP.NET Core and Blazor documentation + +### Common Issues + +1. **Database Connection Issues**: Verify SQLite file permissions and path +2. **Build Errors**: Ensure .NET 9.0 SDK is installed +3. **Authentication Problems**: Check Identity configuration and user roles +4. **Performance Issues**: Review database indexing and query optimization + +### Getting Help + +- Create an issue on GitHub for bugs +- Check existing issues for known problems +- Review the project documentation +- Contact the development team + +## 🏗️ Roadmap + +### Upcoming Features + +- Mobile application support +- Advanced reporting and analytics +- Integration with accounting software +- Automated rent collection +- Multi-language support +- Advanced tenant screening +- IoT device integration +- API for third-party integrations + +### Performance Improvements + +- Database optimization +- Caching implementation +- Background job processing +- File storage optimization +- Search performance enhancements + +--- + +**Aquiis** - Streamlining Property Management for the Modern World + +Built with ❤️ using ASP.NET Core 9.0 and Blazor Server diff --git a/Aquiis.Professional/Shared/App.razor b/Aquiis.Professional/Shared/App.razor new file mode 100644 index 0000000..d3fd5a1 --- /dev/null +++ b/Aquiis.Professional/Shared/App.razor @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Aquiis.Professional/Shared/Authorization/OrganizationAuthorizeAttribute.cs b/Aquiis.Professional/Shared/Authorization/OrganizationAuthorizeAttribute.cs new file mode 100644 index 0000000..95caf80 --- /dev/null +++ b/Aquiis.Professional/Shared/Authorization/OrganizationAuthorizeAttribute.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Aquiis.Professional.Shared.Authorization; + +/// +/// Authorization attribute for organization-based role checking. +/// Replaces [Authorize(Roles = "...")] with organization-scoped roles. +/// When used without roles, allows any authenticated organization member. +/// +public class OrganizationAuthorizeAttribute : AuthorizeAttribute +{ + public OrganizationAuthorizeAttribute(params string[] roles) + { + if (roles == null || roles.Length == 0) + { + Policy = "OrganizationMember"; + } + else + { + Policy = $"OrganizationRole:{string.Join(",", roles)}"; + } + } +} diff --git a/Aquiis.Professional/Shared/Authorization/OrganizationPolicyProvider.cs b/Aquiis.Professional/Shared/Authorization/OrganizationPolicyProvider.cs new file mode 100644 index 0000000..a0f611d --- /dev/null +++ b/Aquiis.Professional/Shared/Authorization/OrganizationPolicyProvider.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; + +namespace Aquiis.Professional.Shared.Authorization; + +/// +/// Custom authorization policy provider for organization roles. +/// Dynamically creates policies based on the OrganizationRole: prefix. +/// +public class OrganizationPolicyProvider : IAuthorizationPolicyProvider +{ + private readonly DefaultAuthorizationPolicyProvider _fallbackPolicyProvider; + private const string POLICY_PREFIX = "OrganizationRole:"; + + public OrganizationPolicyProvider(IOptions options) + { + _fallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); + } + + public Task GetDefaultPolicyAsync() + { + return _fallbackPolicyProvider.GetDefaultPolicyAsync(); + } + + public Task GetFallbackPolicyAsync() + { + return _fallbackPolicyProvider.GetFallbackPolicyAsync(); + } + + public Task GetPolicyAsync(string policyName) + { + if (policyName == "OrganizationMember") + { + var policy = new AuthorizationPolicyBuilder(); + policy.RequireAuthenticatedUser(); + policy.AddRequirements(new OrganizationRoleRequirement(Array.Empty())); + return Task.FromResult(policy.Build()); + } + + if (policyName.StartsWith(POLICY_PREFIX)) + { + var roles = policyName.Substring(POLICY_PREFIX.Length).Split(','); + var policy = new AuthorizationPolicyBuilder(); + policy.RequireAuthenticatedUser(); + policy.AddRequirements(new OrganizationRoleRequirement(roles)); + return Task.FromResult(policy.Build()); + } + + return _fallbackPolicyProvider.GetPolicyAsync(policyName); + } +} diff --git a/Aquiis.Professional/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs b/Aquiis.Professional/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs new file mode 100644 index 0000000..388b785 --- /dev/null +++ b/Aquiis.Professional/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Aquiis.Professional.Shared.Components.Account; +using Aquiis.Professional.Infrastructure.Data; +using Aquiis.Professional.Core.Constants; +using System.Security.Claims; + +namespace Aquiis.Professional.Shared.Authorization; + +/// +/// Authorization handler for organization role requirements. +/// Checks if the user has the required role in their active organization. +/// +public class OrganizationRoleAuthorizationHandler : AuthorizationHandler +{ + private readonly ApplicationDbContext _dbContext; + private readonly UserManager _userManager; + + public OrganizationRoleAuthorizationHandler( + ApplicationDbContext dbContext, + UserManager userManager) + { + _dbContext = dbContext; + _userManager = userManager; + } + + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + OrganizationRoleRequirement requirement) + { + // User must be authenticated + if (!context.User.Identity?.IsAuthenticated ?? true) + { + return; + } + + // Get user ID from claims + var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userId)) + { + return; + } + + // Get user's active organization + var user = await _userManager.FindByIdAsync(userId); + if (user?.ActiveOrganizationId == null) + { + return; + } + + // Get user's role in the active organization + var userOrganization = await _dbContext.UserOrganizations + .Where(uo => uo.UserId == userId + && uo.OrganizationId == user.ActiveOrganizationId + && uo.IsActive + && !uo.IsDeleted) + .FirstOrDefaultAsync(); + + if (userOrganization == null) + { + return; + } + + // Check if user's role is in the allowed roles + // If no roles specified (empty array), allow any authenticated org member + if (requirement.AllowedRoles.Length == 0 || requirement.AllowedRoles.Contains(userOrganization.Role)) + { + context.Succeed(requirement); + } + } +} diff --git a/Aquiis.Professional/Shared/Authorization/OrganizationRoleRequirement.cs b/Aquiis.Professional/Shared/Authorization/OrganizationRoleRequirement.cs new file mode 100644 index 0000000..9a759f9 --- /dev/null +++ b/Aquiis.Professional/Shared/Authorization/OrganizationRoleRequirement.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Aquiis.Professional.Shared.Authorization; + +/// +/// Authorization requirement for organization role checking. +/// +public class OrganizationRoleRequirement : IAuthorizationRequirement +{ + public string[] AllowedRoles { get; } + + public OrganizationRoleRequirement(params string[] allowedRoles) + { + AllowedRoles = allowedRoles; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/AccountConstants.cs b/Aquiis.Professional/Shared/Components/Account/AccountConstants.cs new file mode 100644 index 0000000..6209cca --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/AccountConstants.cs @@ -0,0 +1,13 @@ +namespace Aquiis.Professional.Shared.Components.Account +{ + public static class AccountConstants + { + public static string LoginPath { get; } = "/Account/Login"; + public static string RegisterPath { get; } = "/Account/Register"; + public static string ForgotPasswordPath { get; } = "/Account/ForgotPassword"; + public static string ResetPasswordPath { get; } = "/Account/ResetPassword"; + public static string LogoutPath { get; } = "/Account/Logout"; + public static string LockoutPath { get; } = "/Account/Lockout"; + public static string ProfilePath { get; } = "/Account/Profile"; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Shared/Components/Account/ApplicationUser.cs b/Aquiis.Professional/Shared/Components/Account/ApplicationUser.cs new file mode 100644 index 0000000..3551da0 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/ApplicationUser.cs @@ -0,0 +1,25 @@ +using Aquiis.Professional.Shared.Services; +using Microsoft.AspNetCore.Identity; + +namespace Aquiis.Professional.Shared.Components.Account; + +// Add profile data for application users by adding properties to the ApplicationUser class +public class ApplicationUser : IdentityUser +{ + /// + /// The currently active organization ID for this user session + /// + public Guid ActiveOrganizationId { get; set; } = Guid.Empty; + + // The organization ID this user belongs to + public Guid OrganizationId { get; set; } = Guid.Empty; + + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + + public DateTime? LastLoginDate { get; set; } + public DateTime? PreviousLoginDate { get; set; } + public int LoginCount { get; set; } = 0; + public string? LastLoginIP { get; set; } +} + diff --git a/Aquiis.Professional/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs b/Aquiis.Professional/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..64879ba --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -0,0 +1,112 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Aquiis.Professional.Shared.Components.Account.Pages; +using Aquiis.Professional.Shared.Components.Account.Pages.Manage; +using Aquiis.Professional.Shared.Components.Account; + +namespace Microsoft.AspNetCore.Routing; + +internal static class IdentityComponentsEndpointRouteBuilderExtensions +{ + // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project. + public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var accountGroup = endpoints.MapGroup("/Account"); + + accountGroup.MapPost("/PerformExternalLogin", ( + HttpContext context, + [FromServices] SignInManager signInManager, + [FromForm] string provider, + [FromForm] string returnUrl) => + { + IEnumerable> query = [ + new("ReturnUrl", returnUrl), + new("Action", ExternalLogin.LoginCallbackAction)]; + + var redirectUrl = UriHelper.BuildRelative( + context.Request.PathBase, + "/Account/ExternalLogin", + QueryString.Create(query)); + + var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return TypedResults.Challenge(properties, [provider]); + }); + + accountGroup.MapPost("/Logout", async ( + ClaimsPrincipal user, + [FromServices] SignInManager signInManager, + [FromForm] string returnUrl) => + { + await signInManager.SignOutAsync(); + return TypedResults.LocalRedirect($"~/{returnUrl}"); + }); + + var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization(); + + manageGroup.MapPost("/LinkExternalLogin", async ( + HttpContext context, + [FromServices] SignInManager signInManager, + [FromForm] string provider) => + { + // Clear the existing external cookie to ensure a clean login process + await context.SignOutAsync(IdentityConstants.ExternalScheme); + + var redirectUrl = UriHelper.BuildRelative( + context.Request.PathBase, + "/Account/Manage/ExternalLogins", + QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction)); + + var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User)); + return TypedResults.Challenge(properties, [provider]); + }); + + var loggerFactory = endpoints.ServiceProvider.GetRequiredService(); + var downloadLogger = loggerFactory.CreateLogger("DownloadPersonalData"); + + manageGroup.MapPost("/DownloadPersonalData", async ( + HttpContext context, + [FromServices] UserManager userManager, + [FromServices] AuthenticationStateProvider authenticationStateProvider) => + { + var user = await userManager.GetUserAsync(context.User); + if (user is null) + { + return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); + } + + var userId = await userManager.GetUserIdAsync(user); + downloadLogger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId); + + // Only include personal data for download + var personalData = new Dictionary(); + var personalDataProps = typeof(ApplicationUser).GetProperties().Where( + prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); + foreach (var p in personalDataProps) + { + personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); + } + + var logins = await userManager.GetLoginsAsync(user); + foreach (var l in logins) + { + personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); + } + + personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!); + var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData); + + context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json"); + return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json"); + }); + + return accountGroup; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/IdentityNoOpEmailSender.cs b/Aquiis.Professional/Shared/Components/Account/IdentityNoOpEmailSender.cs new file mode 100644 index 0000000..4b1963a --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/IdentityNoOpEmailSender.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Aquiis.Professional.Infrastructure.Data; + +namespace Aquiis.Professional.Shared.Components.Account; + +// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation. +internal sealed class IdentityNoOpEmailSender : IEmailSender +{ + private readonly IEmailSender emailSender = new NoOpEmailSender(); + + public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) => + emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); + + public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) => + emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); + + public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) => + emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); +} diff --git a/Aquiis.Professional/Shared/Components/Account/IdentityRedirectManager.cs b/Aquiis.Professional/Shared/Components/Account/IdentityRedirectManager.cs new file mode 100644 index 0000000..3a90f23 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/IdentityRedirectManager.cs @@ -0,0 +1,58 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; + +namespace Aquiis.Professional.Shared.Components.Account; + +internal sealed class IdentityRedirectManager(NavigationManager navigationManager) +{ + public const string StatusCookieName = "Identity.StatusMessage"; + + private static readonly CookieBuilder StatusCookieBuilder = new() + { + SameSite = SameSiteMode.Strict, + HttpOnly = true, + IsEssential = true, + MaxAge = TimeSpan.FromSeconds(5), + }; + + [DoesNotReturn] + public void RedirectTo(string? uri) + { + uri ??= ""; + + // Prevent open redirects. + if (!Uri.IsWellFormedUriString(uri, UriKind.Relative)) + { + uri = navigationManager.ToBaseRelativePath(uri); + } + + // During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect. + // So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown. + navigationManager.NavigateTo(uri); + throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering."); + } + + [DoesNotReturn] + public void RedirectTo(string uri, Dictionary queryParameters) + { + var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path); + var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters); + RedirectTo(newUri); + } + + [DoesNotReturn] + public void RedirectToWithStatus(string uri, string message, HttpContext context) + { + context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context)); + RedirectTo(uri); + } + + private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path); + + [DoesNotReturn] + public void RedirectToCurrentPage() => RedirectTo(CurrentPath); + + [DoesNotReturn] + public void RedirectToCurrentPageWithStatus(string message, HttpContext context) + => RedirectToWithStatus(CurrentPath, message, context); +} diff --git a/Aquiis.Professional/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs b/Aquiis.Professional/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs new file mode 100644 index 0000000..29df6cb --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs @@ -0,0 +1,47 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Aquiis.Professional.Infrastructure.Data; + +namespace Aquiis.Professional.Shared.Components.Account; + +// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user +// every 30 minutes an interactive circuit is connected. +internal sealed class IdentityRevalidatingAuthenticationStateProvider( + ILoggerFactory loggerFactory, + IServiceScopeFactory scopeFactory, + IOptions options) + : RevalidatingServerAuthenticationStateProvider(loggerFactory) +{ + protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); + + protected override async Task ValidateAuthenticationStateAsync( + AuthenticationState authenticationState, CancellationToken cancellationToken) + { + // Get the user manager from a new scope to ensure it fetches fresh data + await using var scope = scopeFactory.CreateAsyncScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + return await ValidateSecurityStampAsync(userManager, authenticationState.User); + } + + private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) + { + var user = await userManager.GetUserAsync(principal); + if (user is null) + { + return false; + } + else if (!userManager.SupportsUserSecurityStamp) + { + return true; + } + else + { + var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType); + var userStamp = await userManager.GetSecurityStampAsync(user); + return principalStamp == userStamp; + } + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/IdentityUserAccessor.cs b/Aquiis.Professional/Shared/Components/Account/IdentityUserAccessor.cs new file mode 100644 index 0000000..2ca8618 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/IdentityUserAccessor.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Identity; +using Aquiis.Professional.Infrastructure.Data; + +namespace Aquiis.Professional.Shared.Components.Account; + +internal sealed class IdentityUserAccessor(UserManager userManager, IdentityRedirectManager redirectManager) +{ + public async Task GetRequiredUserAsync(HttpContext context) + { + var user = await userManager.GetUserAsync(context.User); + + if (user is null) + { + redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context); + } + + return user; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/AccessDenied.razor b/Aquiis.Professional/Shared/Components/Account/Pages/AccessDenied.razor new file mode 100644 index 0000000..43c4a2c --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/AccessDenied.razor @@ -0,0 +1,8 @@ +@page "/Account/AccessDenied" + +Access denied + +
+

Access denied

+

You do not have access to this resource.

+
diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmail.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmail.razor new file mode 100644 index 0000000..e8a70ed --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmail.razor @@ -0,0 +1,47 @@ +@page "/Account/ConfirmEmail" + +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject UserManager UserManager +@inject IdentityRedirectManager RedirectManager + +Confirm email + +

Confirm email

+ + +@code { + private string? statusMessage; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromQuery] + private string? UserId { get; set; } + + [SupplyParameterFromQuery] + private string? Code { get; set; } + + protected override async Task OnInitializedAsync() + { + if (UserId is null || Code is null) + { + RedirectManager.RedirectTo(""); + } + + var user = await UserManager.FindByIdAsync(UserId); + if (user is null) + { + HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; + statusMessage = $"Error loading user with ID {UserId}"; + } + else + { + var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); + var result = await UserManager.ConfirmEmailAsync(user, code); + statusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email."; + } + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmailChange.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmailChange.razor new file mode 100644 index 0000000..0edf8ae --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmailChange.razor @@ -0,0 +1,67 @@ +@page "/Account/ConfirmEmailChange" + +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityRedirectManager RedirectManager + +Confirm email change + +

Confirm email change

+ + + +@code { + private string? message; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromQuery] + private string? UserId { get; set; } + + [SupplyParameterFromQuery] + private string? Email { get; set; } + + [SupplyParameterFromQuery] + private string? Code { get; set; } + + protected override async Task OnInitializedAsync() + { + if (UserId is null || Email is null || Code is null) + { + RedirectManager.RedirectToWithStatus( + "Account/Login", "Error: Invalid email change confirmation link.", HttpContext); + } + + var user = await UserManager.FindByIdAsync(UserId); + if (user is null) + { + message = "Unable to find user with Id '{userId}'"; + return; + } + + var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); + var result = await UserManager.ChangeEmailAsync(user, Email, code); + if (!result.Succeeded) + { + message = "Error changing email."; + return; + } + + // In our UI email and user name are one and the same, so when we update the email + // we need to update the user name. + var setUserNameResult = await UserManager.SetUserNameAsync(user, Email); + if (!setUserNameResult.Succeeded) + { + message = "Error changing user name."; + return; + } + + await SignInManager.RefreshSignInAsync(user); + message = "Thank you for confirming your email change."; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ExternalLogin.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ExternalLogin.razor new file mode 100644 index 0000000..b7cd0c2 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/ExternalLogin.razor @@ -0,0 +1,204 @@ +@page "/Account/ExternalLogin" + +@using System.ComponentModel.DataAnnotations +@using System.Security.Claims +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject IUserStore UserStore +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Register + + +

Register

+

Associate your @ProviderDisplayName account.

+
+ +
+ You've successfully authenticated with @ProviderDisplayName. + Please enter an email address for this site below and click the Register button to finish + logging in. +
+ +
+
+ + + +
+ + + +
+ +
+
+
+ +@code { + public const string LoginCallbackAction = "LoginCallback"; + + private string? message; + private ExternalLoginInfo? externalLoginInfo; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? RemoteError { get; set; } + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + [SupplyParameterFromQuery] + private string? Action { get; set; } + + private string? ProviderDisplayName => externalLoginInfo?.ProviderDisplayName; + + protected override async Task OnInitializedAsync() + { + if (RemoteError is not null) + { + RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext); + } + + var info = await SignInManager.GetExternalLoginInfoAsync(); + if (info is null) + { + RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext); + } + + externalLoginInfo = info; + + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + if (Action == LoginCallbackAction) + { + await OnLoginCallbackAsync(); + return; + } + + // We should only reach this page via the login callback, so redirect back to + // the login page if we get here some other way. + RedirectManager.RedirectTo("Account/Login"); + } + } + + private async Task OnLoginCallbackAsync() + { + if (externalLoginInfo is null) + { + RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext); + } + + // Sign in the user with this external login provider if the user already has a login. + var result = await SignInManager.ExternalLoginSignInAsync( + externalLoginInfo.LoginProvider, + externalLoginInfo.ProviderKey, + isPersistent: false, + bypassTwoFactor: true); + + if (result.Succeeded) + { + Logger.LogInformation( + "{Name} logged in with {LoginProvider} provider.", + externalLoginInfo.Principal.Identity?.Name, + externalLoginInfo.LoginProvider); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.IsLockedOut) + { + RedirectManager.RedirectTo("Account/Lockout"); + } + + // If the user does not have an account, then ask the user to create an account. + if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) + { + Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? ""; + } + } + + private async Task OnValidSubmitAsync() + { + if (externalLoginInfo is null) + { + RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information during confirmation.", HttpContext); + } + + var emailStore = GetEmailStore(); + var user = CreateUser(); + + await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); + await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); + + var result = await UserManager.CreateAsync(user); + if (result.Succeeded) + { + result = await UserManager.AddLoginAsync(user, externalLoginInfo); + if (result.Succeeded) + { + Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider); + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code }); + await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + + // If account confirmation is required, we need to show the link if we don't have a real email sender + if (UserManager.Options.SignIn.RequireConfirmedAccount) + { + RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email }); + } + + await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider); + RedirectManager.RedirectTo(ReturnUrl); + } + } + + message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}"; + } + + private ApplicationUser CreateUser() + { + try + { + return Activator.CreateInstance(); + } + catch + { + throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " + + $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor"); + } + } + + private IUserEmailStore GetEmailStore() + { + if (!UserManager.SupportsUserEmail) + { + throw new NotSupportedException("The default UI requires a user store with email support."); + } + return (IUserEmailStore)UserStore; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ForgotPassword.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ForgotPassword.razor new file mode 100644 index 0000000..15b6bb5 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/ForgotPassword.razor @@ -0,0 +1,67 @@ +@page "/Account/ForgotPassword" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Forgot your password? + +

Forgot your password?

+

Enter your email.

+
+
+
+ + + + +
+ + + +
+ +
+
+
+ +@code { + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + private async Task OnValidSubmitAsync() + { + var user = await UserManager.FindByEmailAsync(Input.Email); + if (user is null || !(await UserManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); + } + + // For more information on how to enable account confirmation and password reset please + // visit https://go.microsoft.com/fwlink/?LinkID=532713 + var code = await UserManager.GeneratePasswordResetTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri, + new Dictionary { ["code"] = code }); + + await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + + RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor new file mode 100644 index 0000000..0cebbfe --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor @@ -0,0 +1,8 @@ +@page "/Account/ForgotPasswordConfirmation" + +Forgot password confirmation + +

Forgot password confirmation

+

+ Please check your email to reset your password. +

diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/InvalidPasswordReset.razor b/Aquiis.Professional/Shared/Components/Account/Pages/InvalidPasswordReset.razor new file mode 100644 index 0000000..4e3a4d0 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/InvalidPasswordReset.razor @@ -0,0 +1,8 @@ +@page "/Account/InvalidPasswordReset" + +Invalid password reset + +

Invalid password reset

+

+ The password reset link is invalid. +

diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/InvalidUser.razor b/Aquiis.Professional/Shared/Components/Account/Pages/InvalidUser.razor new file mode 100644 index 0000000..858a7d6 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/InvalidUser.razor @@ -0,0 +1,7 @@ +@page "/Account/InvalidUser" + +Invalid user + +

Invalid user

+ + diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Lockout.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Lockout.razor new file mode 100644 index 0000000..3cbe0fc --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Lockout.razor @@ -0,0 +1,8 @@ +@page "/Account/Lockout" + +Locked out + +
+

Locked out

+ +
diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Login.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Login.razor new file mode 100644 index 0000000..d3c83b4 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Login.razor @@ -0,0 +1,127 @@ +@page "/Account/Login" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity + +@inject SignInManager SignInManager +@inject ILogger Logger +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Log in + +

Log in

+
+
+
+ + + +

Use a local account to log in.

+
+ +
+ + + +
+
+ + + +
+
+ +
+
+ +
+ +
+
+
+
+
+

Use another service to log in.

+
+ +
+
+
+ +@code { + private string? errorMessage; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + } + } + + public async Task LoginUser() + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + Logger.LogInformation("User logged in."); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.RequiresTwoFactor) + { + RedirectManager.RedirectTo( + "Account/LoginWith2fa", + new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); + } + else if (result.IsLockedOut) + { + Logger.LogWarning("User account locked out."); + RedirectManager.RedirectTo("Account/Lockout"); + } + else + { + errorMessage = "Error: Invalid login attempt."; + } + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } = ""; + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/LoginWith2fa.razor b/Aquiis.Professional/Shared/Components/Account/Pages/LoginWith2fa.razor new file mode 100644 index 0000000..e117b0c --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/LoginWith2fa.razor @@ -0,0 +1,100 @@ +@page "/Account/LoginWith2fa" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Two-factor authentication + +

Two-factor authentication

+
+ +

Your login is protected with an authenticator app. Enter your authenticator code below.

+
+
+ + + + + +
+ + + +
+
+ +
+
+ +
+
+
+
+

+ Don't have access to your authenticator device? You can + log in with a recovery code. +

+ +@code { + private string? message; + private ApplicationUser user = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + [SupplyParameterFromQuery] + private bool RememberMe { get; set; } + + protected override async Task OnInitializedAsync() + { + // Ensure the user has gone through the username & password screen first + user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ?? + throw new InvalidOperationException("Unable to load two-factor authentication user."); + } + + private async Task OnValidSubmitAsync() + { + var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty); + var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine); + var userId = await UserManager.GetUserIdAsync(user); + + if (result.Succeeded) + { + Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.IsLockedOut) + { + Logger.LogWarning("User with ID '{UserId}' account locked out.", userId); + RedirectManager.RedirectTo("Account/Lockout"); + } + else + { + Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId); + message = "Error: Invalid authenticator code."; + } + } + + private sealed class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Authenticator code")] + public string? TwoFactorCode { get; set; } + + [Display(Name = "Remember this machine")] + public bool RememberMachine { get; set; } + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor b/Aquiis.Professional/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor new file mode 100644 index 0000000..5759f11 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor @@ -0,0 +1,84 @@ +@page "/Account/LoginWithRecoveryCode" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Recovery code verification + +

Recovery code verification

+
+ +

+ You have requested to log in with a recovery code. This login will not be remembered until you provide + an authenticator app code at log in or disable 2FA and log in again. +

+
+
+ + + +
+ + + +
+ +
+
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + // Ensure the user has gone through the username & password screen first + user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ?? + throw new InvalidOperationException("Unable to load two-factor authentication user."); + } + + private async Task OnValidSubmitAsync() + { + var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); + + var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + + var userId = await UserManager.GetUserIdAsync(user); + + if (result.Succeeded) + { + Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.IsLockedOut) + { + Logger.LogWarning("User account locked out."); + RedirectManager.RedirectTo("Account/Lockout"); + } + else + { + Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId); + message = "Error: Invalid recovery code entered."; + } + } + + private sealed class InputModel + { + [Required] + [DataType(DataType.Text)] + [Display(Name = "Recovery Code")] + public string RecoveryCode { get; set; } = ""; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ChangePassword.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ChangePassword.razor new file mode 100644 index 0000000..c7c0ed0 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ChangePassword.razor @@ -0,0 +1,95 @@ +@page "/Account/Manage/ChangePassword" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Change password + +

Change password

+ +
+
+ + + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + private bool hasPassword; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + hasPassword = await UserManager.HasPasswordAsync(user); + if (!hasPassword) + { + RedirectManager.RedirectTo("Account/Manage/SetPassword"); + } + } + + private async Task OnValidSubmitAsync() + { + var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); + if (!changePasswordResult.Succeeded) + { + message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}"; + return; + } + + await SignInManager.RefreshSignInAsync(user); + Logger.LogInformation("User changed their password successfully."); + + RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed", HttpContext); + } + + private sealed class InputModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } = ""; + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } = ""; + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = ""; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor new file mode 100644 index 0000000..bbb1034 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor @@ -0,0 +1,85 @@ +@page "/Account/Manage/DeletePersonalData" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Delete Personal Data + + + +

Delete Personal Data

+ + + +
+ + + + @if (requirePassword) + { +
+ + + +
+ } + +
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + private bool requirePassword; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + requirePassword = await UserManager.HasPasswordAsync(user); + } + + private async Task OnValidSubmitAsync() + { + if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password)) + { + message = "Error: Incorrect password."; + return; + } + + var result = await UserManager.DeleteAsync(user); + if (!result.Succeeded) + { + throw new InvalidOperationException("Unexpected error occurred deleting user."); + } + + await SignInManager.SignOutAsync(); + + var userId = await UserManager.GetUserIdAsync(user); + Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); + + RedirectManager.RedirectToCurrentPage(); + } + + private sealed class InputModel + { + [DataType(DataType.Password)] + public string Password { get; set; } = ""; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Disable2fa.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Disable2fa.razor new file mode 100644 index 0000000..2ecfb40 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Disable2fa.razor @@ -0,0 +1,62 @@ +@page "/Account/Manage/Disable2fa" + +@using Microsoft.AspNetCore.Identity +@inject UserManager UserManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Disable two-factor authentication (2FA) + + +

Disable two-factor authentication (2FA)

+ + + +
+
+ + + +
+ +@code { + private ApplicationUser user = default!; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + + if (HttpMethods.IsGet(HttpContext.Request.Method) && !await UserManager.GetTwoFactorEnabledAsync(user)) + { + throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled."); + } + } + + private async Task OnSubmitAsync() + { + var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new InvalidOperationException("Unexpected error occurred disabling 2FA."); + } + + var userId = await UserManager.GetUserIdAsync(user); + Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId); + RedirectManager.RedirectToWithStatus( + "Account/Manage/TwoFactorAuthentication", + "2fa has been disabled. You can reenable 2fa when you setup an authenticator app", + HttpContext); + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Email.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Email.razor new file mode 100644 index 0000000..3b93e23 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Email.razor @@ -0,0 +1,122 @@ +@page "/Account/Manage/Email" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject IdentityUserAccessor UserAccessor +@inject NavigationManager NavigationManager + +Manage email + +

Manage email

+ + +
+
+
+ + + + + + @if (isEmailConfirmed) + { +
+ +
+ +
+ +
+ } + else + { +
+ + + +
+ } +
+ + + +
+ +
+
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + private string? email; + private bool isEmailConfirmed; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm(FormName = "change-email")] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + email = await UserManager.GetEmailAsync(user); + isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(user); + + Input.NewEmail ??= email; + } + + private async Task OnValidSubmitAsync() + { + if (Input.NewEmail is null || Input.NewEmail == email) + { + message = "Your email is unchanged."; + return; + } + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code }); + + await EmailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); + + message = "Confirmation link to change email sent. Please check your email."; + } + + private async Task OnSendEmailVerificationAsync() + { + if (email is null) + { + return; + } + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code }); + + await EmailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(callbackUrl)); + + message = "Verification email sent. Please check your email."; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "New email")] + public string? NewEmail { get; set; } + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor new file mode 100644 index 0000000..0084a6f --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor @@ -0,0 +1,171 @@ +@page "/Account/Manage/EnableAuthenticator" + +@using System.ComponentModel.DataAnnotations +@using System.Globalization +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity + +@inject UserManager UserManager +@inject IdentityUserAccessor UserAccessor +@inject UrlEncoder UrlEncoder +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Configure authenticator app + +@if (recoveryCodes is not null) +{ + +} +else +{ + +

Configure authenticator app

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

    Scan the QR Code or enter this key @sharedKey into your two factor authenticator app. Spaces and casing do not matter.

    + +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    + + +
    + + + +
    + + +
    +
    +
    +
  6. +
+
+} + +@code { + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + private string? message; + private ApplicationUser user = default!; + private string? sharedKey; + private string? authenticatorUri; + private IEnumerable? recoveryCodes; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + + await LoadSharedKeyAndQrCodeUriAsync(user); + } + + private async Task OnValidSubmitAsync() + { + // Strip spaces and hyphens + var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync( + user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + message = "Error: Verification code is invalid."; + return; + } + + await UserManager.SetTwoFactorEnabledAsync(user, true); + var userId = await UserManager.GetUserIdAsync(user); + Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); + + message = "Your authenticator app has been verified."; + + if (await UserManager.CountRecoveryCodesAsync(user) == 0) + { + recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + } + else + { + RedirectManager.RedirectToWithStatus("Account/Manage/TwoFactorAuthentication", message, HttpContext); + } + } + + private async ValueTask LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) + { + // Load the authenticator key & QR code URI to display on the form + var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await UserManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user); + } + + sharedKey = FormatKey(unformattedKey!); + + var email = await UserManager.GetEmailAsync(user); + authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!); + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' '); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + CultureInfo.InvariantCulture, + AuthenticatorUriFormat, + UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"), + UrlEncoder.Encode(email), + unformattedKey); + } + + private sealed class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } = ""; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ExternalLogins.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ExternalLogins.razor new file mode 100644 index 0000000..0c109c4 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ExternalLogins.razor @@ -0,0 +1,139 @@ +@page "/Account/Manage/ExternalLogins" + +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IUserStore UserStore +@inject IdentityRedirectManager RedirectManager + +Manage your external logins + + +@if (currentLogins?.Count > 0) +{ +

Registered Logins

+ + + @foreach (var login in currentLogins) + { + + + + + } + +
@login.ProviderDisplayName + @if (showRemoveButton) + { +
+ +
+ + + +
+ + } + else + { + @:   + } +
+} +@if (otherLogins?.Count > 0) +{ +

Add another service to log in.

+
+
+ +
+

+ @foreach (var provider in otherLogins) + { + + } +

+
+ +} + +@code { + public const string LinkLoginCallbackAction = "LinkLoginCallback"; + + private ApplicationUser user = default!; + private IList? currentLogins; + private IList? otherLogins; + private bool showRemoveButton; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private string? LoginProvider { get; set; } + + [SupplyParameterFromForm] + private string? ProviderKey { get; set; } + + [SupplyParameterFromQuery] + private string? Action { get; set; } + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + currentLogins = await UserManager.GetLoginsAsync(user); + otherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => currentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + + string? passwordHash = null; + if (UserStore is IUserPasswordStore userPasswordStore) + { + passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted); + } + + showRemoveButton = passwordHash is not null || currentLogins.Count > 1; + + if (HttpMethods.IsGet(HttpContext.Request.Method) && Action == LinkLoginCallbackAction) + { + await OnGetLinkLoginCallbackAsync(); + } + } + + private async Task OnSubmitAsync() + { + var result = await UserManager.RemoveLoginAsync(user, LoginProvider!, ProviderKey!); + if (!result.Succeeded) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not removed.", HttpContext); + } + + await SignInManager.RefreshSignInAsync(user); + RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed.", HttpContext); + } + + private async Task OnGetLinkLoginCallbackAsync() + { + var userId = await UserManager.GetUserIdAsync(user); + var info = await SignInManager.GetExternalLoginInfoAsync(userId); + if (info is null) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: Could not load external login info.", HttpContext); + } + + var result = await UserManager.AddLoginAsync(user, info); + if (!result.Succeeded) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not added. External logins can only be associated with one account.", HttpContext); + } + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + RedirectManager.RedirectToCurrentPageWithStatus("The external login was added.", HttpContext); + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor new file mode 100644 index 0000000..99f6438 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor @@ -0,0 +1,67 @@ +@page "/Account/Manage/GenerateRecoveryCodes" + +@using Microsoft.AspNetCore.Identity + +@inject UserManager UserManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Generate two-factor authentication (2FA) recovery codes + +@if (recoveryCodes is not null) +{ + +} +else +{ +

Generate two-factor authentication (2FA) recovery codes

+ +
+
+ + + +
+} + +@code { + private string? message; + private ApplicationUser user = default!; + private IEnumerable? recoveryCodes; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + + var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user); + if (!isTwoFactorEnabled) + { + throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled."); + } + } + + private async Task OnSubmitAsync() + { + var userId = await UserManager.GetUserIdAsync(user); + recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + message = "You have generated new recovery codes."; + + Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Index.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Index.razor new file mode 100644 index 0000000..8ff0c8c --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Index.razor @@ -0,0 +1,116 @@ +@page "/Account/Manage" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager + +Profile + +

Profile

+ + +
+
+ + + +
+ + + +
+
+ + + +
+
+ + +
+
+ + + +
+ +
+
+
+ +@code { + private ApplicationUser user = default!; + private string? username; + private string? phoneNumber; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + + // Reload user from database to ensure we have the latest values + var userId = await UserManager.GetUserIdAsync(user); + user = await UserManager.FindByIdAsync(userId) ?? user; + + username = await UserManager.GetUserNameAsync(user); + phoneNumber = await UserManager.GetPhoneNumberAsync(user); + + Input.PhoneNumber ??= phoneNumber; + Input.FirstName ??= user.FirstName; + Input.LastName ??= user.LastName; + } + + private async Task OnValidSubmitAsync() + { + // Reload the user to ensure we have the latest version + var userId = await UserManager.GetUserIdAsync(user); + user = await UserManager.FindByIdAsync(userId) ?? user; + + if (Input.PhoneNumber != phoneNumber) + { + var setPhoneResult = await UserManager.SetPhoneNumberAsync(user, Input.PhoneNumber); + if (!setPhoneResult.Succeeded) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to set phone number.", HttpContext); + return; + } + } + + // Update the user properties + user.FirstName = Input.FirstName ?? string.Empty; + user.LastName = Input.LastName ?? string.Empty; + + var updateResult = await UserManager.UpdateAsync(user); + if (!updateResult.Succeeded) + { + var errors = string.Join(", ", updateResult.Errors.Select(e => e.Description)); + RedirectManager.RedirectToCurrentPageWithStatus($"Error: Failed to update profile. {errors}", HttpContext); + return; + } + + await SignInManager.RefreshSignInAsync(user); + RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext); + } + + private sealed class InputModel + { + [Display(Name = "First name")] + public string? FirstName { get; set; } + + [Display(Name = "Last name")] + public string? LastName { get; set; } + + [Phone] + [Display(Name = "Phone number")] + public string? PhoneNumber { get; set; } + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/PersonalData.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/PersonalData.razor new file mode 100644 index 0000000..3cd179e --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/PersonalData.razor @@ -0,0 +1,34 @@ +@page "/Account/Manage/PersonalData" + +@inject IdentityUserAccessor UserAccessor + +Personal Data + + +

Personal Data

+ +
+
+

Your account contains personal data that you have given us. This page allows you to download or delete that data.

+

+ Deleting this data will permanently remove your account, and this cannot be recovered. +

+
+ + + +

+ Delete +

+
+
+ +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + _ = await UserAccessor.GetRequiredUserAsync(HttpContext); + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor new file mode 100644 index 0000000..0a08d34 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor @@ -0,0 +1,51 @@ +@page "/Account/Manage/ResetAuthenticator" + +@using Microsoft.AspNetCore.Identity + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Reset authenticator key + + +

Reset authenticator key

+ +
+
+ + + +
+ +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + private async Task OnSubmitAsync() + { + var user = await UserAccessor.GetRequiredUserAsync(HttpContext); + await UserManager.SetTwoFactorEnabledAsync(user, false); + await UserManager.ResetAuthenticatorKeyAsync(user); + var userId = await UserManager.GetUserIdAsync(user); + Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId); + + await SignInManager.RefreshSignInAsync(user); + + RedirectManager.RedirectToWithStatus( + "Account/Manage/EnableAuthenticator", + "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.", + HttpContext); + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/SetPassword.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/SetPassword.razor new file mode 100644 index 0000000..307a660 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/SetPassword.razor @@ -0,0 +1,86 @@ +@page "/Account/Manage/SetPassword" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager + +Set password + +

Set your password

+ +

+ You do not have a local username/password for this site. Add a local + account so you can log in without an external login. +

+
+
+ + + +
+ + + +
+
+ + + +
+ +
+
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + + var hasPassword = await UserManager.HasPasswordAsync(user); + if (hasPassword) + { + RedirectManager.RedirectTo("Account/Manage/ChangePassword"); + } + } + + private async Task OnValidSubmitAsync() + { + var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!); + if (!addPasswordResult.Succeeded) + { + message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}"; + return; + } + + await SignInManager.RefreshSignInAsync(user); + RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext); + } + + private sealed class InputModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string? NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string? ConfirmPassword { get; set; } + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor new file mode 100644 index 0000000..75b15eb --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor @@ -0,0 +1,100 @@ +@page "/Account/Manage/TwoFactorAuthentication" + +@using Microsoft.AspNetCore.Http.Features +@using Microsoft.AspNetCore.Identity + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager + +Two-factor authentication (2FA) + + +

Two-factor authentication (2FA)

+@if (canTrack) +{ + if (is2faEnabled) + { + if (recoveryCodesLeft == 0) + { +
+ You have no recovery codes left. +

You must generate a new set of recovery codes before you can log in with a recovery code.

+
+ } + else if (recoveryCodesLeft == 1) + { +
+ You have 1 recovery code left. +

You can generate a new set of recovery codes.

+
+ } + else if (recoveryCodesLeft <= 3) + { +
+ You have @recoveryCodesLeft recovery codes left. +

You should generate a new set of recovery codes.

+
+ } + + if (isMachineRemembered) + { +
+ + + + } + + Disable 2FA + Reset recovery codes + } + +

Authenticator app

+ @if (!hasAuthenticator) + { + Add authenticator app + } + else + { + Set up authenticator app + Reset authenticator app + } +} +else +{ +
+ Privacy and cookie policy have not been accepted. +

You must accept the policy before you can enable two factor authentication.

+
+} + +@code { + private bool canTrack; + private bool hasAuthenticator; + private int recoveryCodesLeft; + private bool is2faEnabled; + private bool isMachineRemembered; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + var user = await UserAccessor.GetRequiredUserAsync(HttpContext); + canTrack = HttpContext.Features.Get()?.CanTrack ?? true; + hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null; + is2faEnabled = await UserManager.GetTwoFactorEnabledAsync(user); + isMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user); + recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user); + } + + private async Task OnSubmitForgetBrowserAsync() + { + await SignInManager.ForgetTwoFactorClientAsync(); + + RedirectManager.RedirectToCurrentPageWithStatus( + "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.", + HttpContext); + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/_Imports.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/_Imports.razor new file mode 100644 index 0000000..74f7744 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Manage/_Imports.razor @@ -0,0 +1,2 @@ +@layout ManageLayout +@attribute [Microsoft.AspNetCore.Authorization.Authorize] diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Register.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Register.razor new file mode 100644 index 0000000..a463067 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/Register.razor @@ -0,0 +1,264 @@ +@page "/Account/Register" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using Aquiis.Professional.Infrastructure.Data +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Constants + +@inject UserManager UserManager +@inject IUserStore UserStore +@inject SignInManager SignInManager +@inject IEmailSender EmailSender +@inject ILogger Logger +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager +@inject ApplicationDbContext DbContext +@inject OrganizationService OrganizationService + +Register + +@if (!_allowRegistration) +{ +

Registration Disabled

+
+
+ +
+
+} +else +{ +

Register

+ +
+
+ + + +

Create your account.

+ @if (_isFirstUser) + { +
+ Welcome! You are creating the first account. You will be the organization owner with full administrative privileges. +
+ } +
+ + @if (_isFirstUser) + { +
+ + + +
+ } +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+
+

Use another service to register.

+
+ +
+
+
+} + +@code { + private IEnumerable? identityErrors; + private bool _isFirstUser = false; + private bool _allowRegistration = false; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; + + + protected override async Task OnInitializedAsync() + { + // Check if this is the first user (excluding system user) + var userCount = await Task.Run(() => DbContext.Users + .Count(u => u.Id != ApplicationConstants.SystemUser.Id)); + _isFirstUser = userCount == 0; + _allowRegistration = _isFirstUser; // Only allow registration if this is the first user + } + + public async Task RegisterUser(EditContext editContext) + { + // Double-check registration is allowed + if (!_allowRegistration) + { + identityErrors = new[] { new IdentityError { Description = "Registration is disabled. Please contact your administrator." } }; + return; + } + + // Validate organization name for first user + if (_isFirstUser && string.IsNullOrWhiteSpace(Input.OrganizationName)) + { + identityErrors = new[] { new IdentityError { Description = "Organization name is required." } }; + return; + } + + var user = CreateUser(); + + await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); + var emailStore = GetEmailStore(); + await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); + var result = await UserManager.CreateAsync(user, Input.Password); + + if (!result.Succeeded) + { + identityErrors = result.Errors; + return; + } + + Logger.LogInformation("User created a new account with password."); + + var userId = await UserManager.GetUserIdAsync(user); + + // First user setup - create organization and grant owner access + if (_isFirstUser) + { + try + { + Logger.LogInformation("Creating organization for first user: {Email}", Input.Email); + + @* var newOrganization = new Organization() + { + Name = Input.OrganizationName!, + DisplayName = Input.OrganizationName!, + OwnerId = userId + }; *@ + + // Create organization + var organization = await OrganizationService.CreateOrganizationAsync( + name: Input.OrganizationName!, + ownerId: userId, + displayName: Input.OrganizationName, + state: null); + + if (organization != null) + { + // Set user's active organization and default organization. + user.ActiveOrganizationId = organization.Id; + user.OrganizationId = organization.Id; + await UserManager.UpdateAsync(user); + + Logger.LogInformation("Organization {OrgName} created successfully for user {Email}", + Input.OrganizationName, Input.Email); + } + else + { + Logger.LogError("Failed to create organization for first user"); + identityErrors = new[] { new IdentityError { Description = "Failed to create organization." } }; + + // Clean up user account + await UserManager.DeleteAsync(user); + return; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error creating organization for first user"); + identityErrors = new[] { new IdentityError { Description = $"Error creating organization: {ex.Message}" } }; + + // Clean up user account + await UserManager.DeleteAsync(user); + return; + } + } + + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); + + await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + + if (UserManager.Options.SignIn.RequireConfirmedAccount) + { + RedirectManager.RedirectTo( + "Account/RegisterConfirmation", + new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl }); + } + + await SignInManager.SignInAsync(user, isPersistent: false); + RedirectManager.RedirectTo(ReturnUrl); + } + + private ApplicationUser CreateUser() + { + try + { + return Activator.CreateInstance(); + } + catch + { + throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " + + $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor."); + } + } + + private IUserEmailStore GetEmailStore() + { + if (!UserManager.SupportsUserEmail) + { + throw new NotSupportedException("The default UI requires a user store with email support."); + } + return (IUserEmailStore)UserStore; + } + + private sealed class InputModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 3)] + [Display(Name = "Organization Name")] + public string? OrganizationName { get; set; } + + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } = ""; + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } = ""; + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = ""; + } + +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor b/Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor new file mode 100644 index 0000000..13430bc --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor @@ -0,0 +1,67 @@ +@page "/Account/RegisterConfirmation" + +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Register confirmation + +

Register confirmation

+ + + +@if (emailConfirmationLink is not null) +{ +

+ This app does not currently have a real email sender registered, see these docs for how to configure a real email sender. + Normally this would be emailed: Click here to confirm your account +

+} +else +{ +

Please check your email to confirm your account.

+} + +@code { + private string? emailConfirmationLink; + private string? statusMessage; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromQuery] + private string? Email { get; set; } + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + if (Email is null) + { + RedirectManager.RedirectTo(""); + } + + var user = await UserManager.FindByEmailAsync(Email); + if (user is null) + { + HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; + statusMessage = "Error finding user for unspecified email"; + } + else if (EmailSender is IdentityNoOpEmailSender) + { + // Once you add a real email sender, you should remove this code that lets you confirm the account + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + emailConfirmationLink = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); + } + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ResendEmailConfirmation.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ResendEmailConfirmation.razor new file mode 100644 index 0000000..b1bd072 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/ResendEmailConfirmation.razor @@ -0,0 +1,67 @@ +@page "/Account/ResendEmailConfirmation" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Resend email confirmation + +

Resend email confirmation

+

Enter your email.

+
+ +
+
+ + + +
+ + + +
+ +
+
+
+ +@code { + private string? message; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + private async Task OnValidSubmitAsync() + { + var user = await UserManager.FindByEmailAsync(Input.Email!); + if (user is null) + { + message = "Verification email sent. Please check your email."; + return; + } + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code }); + await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + + message = "Verification email sent. Please check your email."; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ResetPassword.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ResetPassword.razor new file mode 100644 index 0000000..0503cf3 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/ResetPassword.razor @@ -0,0 +1,102 @@ +@page "/Account/ResetPassword" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities + +@inject IdentityRedirectManager RedirectManager +@inject UserManager UserManager + +Reset password + +

Reset password

+

Reset your password.

+
+
+
+ + + + + + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@code { + private IEnumerable? identityErrors; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? Code { get; set; } + + private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; + + protected override void OnInitialized() + { + if (Code is null) + { + RedirectManager.RedirectTo("Account/InvalidPasswordReset"); + } + + Input.Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); + } + + private async Task OnValidSubmitAsync() + { + var user = await UserManager.FindByEmailAsync(Input.Email); + if (user is null) + { + // Don't reveal that the user does not exist + RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); + } + + var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password); + if (result.Succeeded) + { + RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); + } + + identityErrors = result.Errors; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + public string Password { get; set; } = ""; + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = ""; + + [Required] + public string Code { get; set; } = ""; + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor new file mode 100644 index 0000000..05fd826 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor @@ -0,0 +1,7 @@ +@page "/Account/ResetPasswordConfirmation" +Reset password confirmation + +

Reset password confirmation

+

+ Your password has been reset. Please click here to log in. +

diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/_Imports.razor b/Aquiis.Professional/Shared/Components/Account/Pages/_Imports.razor new file mode 100644 index 0000000..ee4dd58 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Pages/_Imports.razor @@ -0,0 +1,2 @@ +@using Aquiis.Professional.Shared.Components.Account.Shared +@attribute [ExcludeFromInteractiveRouting] diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/ExternalLoginPicker.razor b/Aquiis.Professional/Shared/Components/Account/Shared/ExternalLoginPicker.razor new file mode 100644 index 0000000..ea37330 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Shared/ExternalLoginPicker.razor @@ -0,0 +1,42 @@ +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity + +@inject SignInManager SignInManager +@inject IdentityRedirectManager RedirectManager + +@if (externalLogins.Length == 0) +{ +
+

+ There are no external authentication services configured. See this article + about setting up this ASP.NET application to support logging in via external services. +

+
+} +else +{ +
+
+ + +

+ @foreach (var provider in externalLogins) + { + + } +

+
+
+} + +@code { + private AuthenticationScheme[] externalLogins = []; + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray(); + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/ManageLayout.razor b/Aquiis.Professional/Shared/Components/Account/Shared/ManageLayout.razor new file mode 100644 index 0000000..2b89ded --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Shared/ManageLayout.razor @@ -0,0 +1,17 @@ +@inherits LayoutComponentBase +@layout Aquiis.Professional.Shared.Layout.MainLayout + +

Manage your account

+ +
+

Change your account settings

+
+
+
+ +
+
+ @Body +
+
+
diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/ManageNavMenu.razor b/Aquiis.Professional/Shared/Components/Account/Shared/ManageNavMenu.razor new file mode 100644 index 0000000..0ae7b78 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Shared/ManageNavMenu.razor @@ -0,0 +1,36 @@ +@using Microsoft.AspNetCore.Identity + +@inject SignInManager SignInManager + + + +@code { + private bool hasExternalLogins; + + protected override async Task OnInitializedAsync() + { + hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/RedirectToLogin.razor b/Aquiis.Professional/Shared/Components/Account/Shared/RedirectToLogin.razor new file mode 100644 index 0000000..1abdd8f --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Shared/RedirectToLogin.razor @@ -0,0 +1,8 @@ +@inject NavigationManager NavigationManager + +@code { + protected override void OnInitialized() + { + NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); + } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/ShowRecoveryCodes.razor b/Aquiis.Professional/Shared/Components/Account/Shared/ShowRecoveryCodes.razor new file mode 100644 index 0000000..1ce4b67 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Shared/ShowRecoveryCodes.razor @@ -0,0 +1,28 @@ + +

Recovery codes

+ +
+
+ @foreach (var recoveryCode in RecoveryCodes) + { +
+ @recoveryCode +
+ } +
+
+ +@code { + [Parameter] + public string[] RecoveryCodes { get; set; } = []; + + [Parameter] + public string? StatusMessage { get; set; } +} diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/StatusMessage.razor b/Aquiis.Professional/Shared/Components/Account/Shared/StatusMessage.razor new file mode 100644 index 0000000..805f391 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Account/Shared/StatusMessage.razor @@ -0,0 +1,29 @@ +@if (!string.IsNullOrEmpty(DisplayMessage)) +{ + var statusMessageClass = DisplayMessage.StartsWith("Error") ? "danger" : "success"; + +} + +@code { + private string? messageFromCookie; + + [Parameter] + public string? Message { get; set; } + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + private string? DisplayMessage => Message ?? messageFromCookie; + + protected override void OnInitialized() + { + messageFromCookie = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName]; + + if (messageFromCookie is not null) + { + HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName); + } + } +} diff --git a/Aquiis.Professional/Shared/Components/LeaseRenewalWidget.razor b/Aquiis.Professional/Shared/Components/LeaseRenewalWidget.razor new file mode 100644 index 0000000..1f13f05 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/LeaseRenewalWidget.razor @@ -0,0 +1,179 @@ +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Core.Entities +@inject LeaseService LeaseService +@rendermode InteractiveServer + +
+
+
+ Lease Renewals +
+ + View All + +
+
+ @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (expiringLeases == null || !expiringLeases.Any()) + { +

No leases expiring in the next 90 days.

+ } + else + { +
+
+ + + + + + + + +
+
+ +
+ @foreach (var lease in GetFilteredLeases()) + { + var daysRemaining = (lease.EndDate - DateTime.Today).Days; + var urgencyClass = daysRemaining <= 30 ? "danger" : daysRemaining <= 60 ? "warning" : "info"; + var urgencyIcon = daysRemaining <= 30 ? "exclamation-triangle-fill" : daysRemaining <= 60 ? "exclamation-circle-fill" : "info-circle-fill"; + +
+
+
+
+ + @lease.Property?.Address +
+

+ Tenant: @lease.Tenant?.FullName
+ End Date: @lease.EndDate.ToString("MMM dd, yyyy")
+ Current Rent: @lease.MonthlyRent.ToString("C") + @if (lease.ProposedRenewalRent.HasValue) + { + → @lease.ProposedRenewalRent.Value.ToString("C") + } +

+ @if (!string.IsNullOrEmpty(lease.RenewalStatus)) + { + + @lease.RenewalStatus + + } +
+
+ + @daysRemaining days + +
+
+
+ } +
+ + + } +
+
+ +@code { + private List expiringLeases = new(); + private List leases30Days = new(); + private List leases60Days = new(); + private List leases90Days = new(); + private bool isLoading = true; + private int selectedFilter = 30; + + protected override async Task OnInitializedAsync() + { + await LoadExpiringLeases(); + } + + private async Task LoadExpiringLeases() + { + try + { + isLoading = true; + var allLeases = await LeaseService.GetAllAsync(); + var today = DateTime.Today; + + expiringLeases = allLeases + .Where(l => l.Status == "Active" && + l.EndDate >= today && + l.EndDate <= today.AddDays(90)) + .OrderBy(l => l.EndDate) + .ToList(); + + leases30Days = expiringLeases + .Where(l => l.EndDate <= today.AddDays(30)) + .ToList(); + + leases60Days = expiringLeases + .Where(l => l.EndDate <= today.AddDays(60)) + .ToList(); + + leases90Days = expiringLeases; + } + catch (Exception ex) + { + // Log error + Console.WriteLine($"Error loading expiring leases: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private void FilterLeases(int days) + { + selectedFilter = days; + } + + private List GetFilteredLeases() + { + return selectedFilter switch + { + 30 => leases30Days, + 60 => leases60Days, + 90 => leases90Days, + _ => expiringLeases + }; + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + "Pending" => "secondary", + "Offered" => "info", + "Accepted" => "success", + "Declined" => "danger", + "Expired" => "dark", + _ => "secondary" + }; + } +} diff --git a/Aquiis.Professional/Shared/Components/NotesTimeline.razor b/Aquiis.Professional/Shared/Components/NotesTimeline.razor new file mode 100644 index 0000000..8c5c9f8 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/NotesTimeline.razor @@ -0,0 +1,246 @@ +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Shared.Components.Account +@using Microsoft.JSInterop +@inject NoteService NoteService +@inject UserContextService UserContext +@inject ToastService ToastService +@inject IJSRuntime JSRuntime + +@rendermode InteractiveServer + +
+
+ + +
+ @newNoteContent.Length / 5000 characters + +
+
+ + @if (isLoading) + { +
+
+ Loading notes... +
+
+ } + else if (notes.Any()) + { +
+
Timeline (@notes.Count)
+ @foreach (var note in notes) + { +
+
+
+
+
+ + + @GetUserDisplayName(note) + +
+ + @FormatTimestamp(note.CreatedOn) + +
+ @if (CanDelete && note.CreatedBy == currentUserId) + { + + } +
+

@note.Content

+
+
+
+ } +
+ } + else + { +
+ No notes yet. Add the first note above. +
+ } +
+ + + +@code { + [Parameter, EditorRequired] + public string EntityType { get; set; } = string.Empty; + + [Parameter, EditorRequired] + public Guid EntityId { get; set; } + + [Parameter] + public bool CanDelete { get; set; } = true; + + [Parameter] + public EventCallback OnNoteAdded { get; set; } + + private List notes = new(); + private string newNoteContent = string.Empty; + private bool isLoading = true; + private bool isSaving = false; + private string currentUserId = string.Empty; + + protected override async Task OnInitializedAsync() + { + currentUserId = (await UserContext.GetUserIdAsync()) ?? string.Empty; + await LoadNotes(); + } + + protected override async Task OnParametersSetAsync() + { + // Reload notes when EntityId changes + if (EntityId != Guid.Empty) + { + await LoadNotes(); + } + } + + private async Task LoadNotes() + { + isLoading = true; + try + { + if (EntityId != Guid.Empty) + { + notes = await NoteService.GetNotesAsync(EntityType, EntityId); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading notes: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task AddNote() + { + if (string.IsNullOrWhiteSpace(newNoteContent)) + return; + + isSaving = true; + try + { + var note = await NoteService.AddNoteAsync(EntityType, EntityId, newNoteContent); + + // Add to the beginning of the list + notes.Insert(0, note); + + newNoteContent = string.Empty; + ToastService.ShowSuccess("Note added successfully"); + + if (OnNoteAdded.HasDelegate) + { + await OnNoteAdded.InvokeAsync(); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error adding note: {ex.Message}"); + } + finally + { + isSaving = false; + } + } + + private async Task DeleteNote(Guid noteId) + { + if (!await JSRuntime.InvokeAsync("confirm", "Are you sure you want to delete this note?")) + return; + + try + { + var success = await NoteService.DeleteNoteAsync(noteId); + if (success) + { + notes.RemoveAll(n => n.Id == noteId); + ToastService.ShowSuccess("Note deleted successfully"); + } + else + { + ToastService.ShowError("Note not found or already deleted"); + } + } + catch (Exception ex) + { + ToastService.ShowError($"Error deleting note: {ex.Message}"); + } + } + + private string GetUserDisplayName(Note note) + { + if (!string.IsNullOrEmpty(note.UserFullName)) + return note.UserFullName; + + return "Unknown User"; + } + + private string FormatTimestamp(DateTime timestamp) + { + var now = DateTime.UtcNow; + var diff = now - timestamp; + + if (diff.TotalMinutes < 1) + return "Just now"; + if (diff.TotalMinutes < 60) + return $"{(int)diff.TotalMinutes} minute{((int)diff.TotalMinutes != 1 ? "s" : "")} ago"; + if (diff.TotalHours < 24) + return $"{(int)diff.TotalHours} hour{((int)diff.TotalHours != 1 ? "s" : "")} ago"; + if (diff.TotalDays < 7) + return $"{(int)diff.TotalDays} day{((int)diff.TotalDays != 1 ? "s" : "")} ago"; + + return timestamp.ToString("MMM dd, yyyy 'at' h:mm tt"); + } +} diff --git a/Aquiis.Professional/Shared/Components/NotificationBell.razor b/Aquiis.Professional/Shared/Components/NotificationBell.razor new file mode 100644 index 0000000..a79f909 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/NotificationBell.razor @@ -0,0 +1,296 @@ +@using Aquiis.Professional.Infrastructure.Services +@inject NotificationService NotificationService +@inject NavigationManager NavigationManager +@rendermode InteractiveServer +@namespace Aquiis.Professional.Shared.Components + +Notification Bell + +@if (isLoading) +{ +
+ +
+} +else if (notifications.Count > 0) +{ + +} else { +
+ +
+} + + +@if (showNotificationModal && selectedNotification != null) +{ + +} + +@code { + private Notification? selectedNotification; + + private bool showNotificationModal = false; + + private bool isLoading = true; + private bool isDropdownOpen = false; + private int notificationCount = 0; + private List notifications = new List(); + + + protected override async Task OnInitializedAsync() + { + await LoadNotificationsAsync(); + } + + private async Task LoadNotificationsAsync() + { + isLoading = true; + notifications = await NotificationService.GetUnreadNotificationsAsync(); + notifications = notifications.OrderByDescending(n => n.CreatedOn).Take(5).ToList(); + notificationCount = notifications.Count; + + notifications = new List{ + new Notification { Id= Guid.NewGuid(), Title = "New message from John", Category = "Messages", Message = "Hey, can we meet tomorrow?", CreatedOn = DateTime.Now, IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "Your report is ready", Category = "Reports", Message = "Your monthly report is now available.", CreatedOn = DateTime.Now.AddDays(-1), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "System maintenance scheduled", Category = "System", Message = "System maintenance is scheduled for tonight at 11 PM.", CreatedOn = DateTime.Now.AddDays(-5), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "New comment on your post", Category = "Comments", Message = "Alice commented on your post.", CreatedOn = DateTime.Now.AddDays(-2), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "Password will expire soon", Category = "Security", Message = "Your password will expire in 3 days.", CreatedOn = DateTime.Now.AddDays(-3), IsRead = false } + }; + notificationCount = notifications.Count(n => !n.IsRead); + isLoading = false; + } + + private async Task ShowNotification(Notification notification) + { + selectedNotification = notification; + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + await NotificationService.MarkAsReadAsync(notification.Id); + notificationCount = notifications.Count(n => !n.IsRead); + showNotificationModal = true; + } + + private void CloseModal() + { + showNotificationModal = false; + selectedNotification = null; + } + + private void ViewRelatedEntity() + { + if (selectedNotification?.RelatedEntityId.HasValue == true) + { + var route = EntityRouteHelper.GetEntityRoute( + selectedNotification.RelatedEntityType, + selectedNotification.RelatedEntityId.Value); + NavigationManager.NavigateTo(route); + CloseModal(); + } + } + + private void ToggleDropdown() + { + isDropdownOpen = !isDropdownOpen; + } + + private async Task MarkAllAsRead() + { + foreach (var notification in notifications) + { + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + } + notificationCount = 0; + ToggleDropdown(); + StateHasChanged(); + } + + private void GoToNotificationCenter() + { + ToggleDropdown(); + NavigationManager.NavigateTo("/notifications"); + } + + private string GetCategoryBadgeColor(string category) => category switch + { + "Lease" => "primary", + "Payment" => "success", + "Maintenance" => "warning", + "Application" => "info", + "Security" => "danger", + _ => "secondary" + }; + + private string GetTypeBadgeColor(string type) => type switch + { + "Info" => "info", + "Warning" => "warning", + "Error" => "danger", + "Success" => "success", + _ => "secondary" + }; +} + + \ No newline at end of file diff --git a/Aquiis.Professional/Shared/Components/OrganizationSwitcher.razor b/Aquiis.Professional/Shared/Components/OrganizationSwitcher.razor new file mode 100644 index 0000000..90e5c23 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/OrganizationSwitcher.razor @@ -0,0 +1,190 @@ +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Core.Constants +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@implements IDisposable +@rendermode InteractiveServer + +@if (isLoading) +{ +
+ +
+} +else if (accessibleOrganizations.Count > 0) +{ + +} + +@code { + private List accessibleOrganizations = new(); + private Organization? currentOrg; + private string? currentRole; + private bool isAccountOwner; + private bool isLoading = true; + private bool isDropdownOpen = false; + + protected override async Task OnInitializedAsync() + { + // Subscribe to location changes + Navigation.LocationChanged += OnLocationChanged; + await LoadOrganizationContextAsync(); + } + + private async void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e) + { + // Refresh the user context cache first to get the latest organization + await UserContext.RefreshAsync(); + + // Then refresh the organization context when navigation occurs + await LoadOrganizationContextAsync(); + await InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + Navigation.LocationChanged -= OnLocationChanged; + } + + private void ToggleDropdown() + { + isDropdownOpen = !isDropdownOpen; + } + + private async Task LoadOrganizationContextAsync() + { + try + { + isLoading = true; + + var userId = await UserContext.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + { + throw new InvalidOperationException("Cannot load organizations: User ID is not available in context."); + } + + // Get all organizations user has access to + accessibleOrganizations = await OrganizationService.GetActiveUserAssignmentsAsync(); + + // Only try to get active organization if user has access to organizations + if (accessibleOrganizations.Any()) + { + // Get current active organization + try + { + currentOrg = await UserContext.GetActiveOrganizationAsync(); + + // Get current role in active organization + currentRole = await UserContext.GetCurrentOrganizationRoleAsync(); + } + catch (InvalidOperationException) + { + // User doesn't have an active organization yet (e.g., just registered) + // This is OK - the switcher will just show no organization + currentOrg = null; + currentRole = null; + } + } + + // Check if user is account owner + isAccountOwner = await UserContext.IsAccountOwnerAsync(); + } + finally + { + isLoading = false; + } + } + + private async Task SwitchOrganizationAsync(Guid organizationId) + { + isDropdownOpen = false; // Close dropdown + + try + { + // Don't switch if already on this organization + if (currentOrg?.Id == organizationId) + { + return; + } + + var success = await UserContext.SwitchOrganizationAsync(organizationId); + + if (success) + { + // Reload the page to refresh all data with new organization context + Navigation.NavigateTo(Navigation.Uri, forceLoad: true); + } + } + catch (Exception) + { + // Error handling - could show toast notification here + // For now, silently fail and stay on current org + } + } + + private string GetRoleBadgeClass(string role) + { + return role switch + { + ApplicationConstants.OrganizationRoles.Owner => "bg-primary", + ApplicationConstants.OrganizationRoles.Administrator => "bg-info", + ApplicationConstants.OrganizationRoles.PropertyManager => "bg-success", + ApplicationConstants.OrganizationRoles.User => "bg-secondary", + _ => "bg-secondary" + }; + } +} diff --git a/Aquiis.Professional/Shared/Components/Pages/About.razor b/Aquiis.Professional/Shared/Components/Pages/About.razor new file mode 100644 index 0000000..7632fc0 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Pages/About.razor @@ -0,0 +1,21 @@ +@page "/about" + +
+
+

Property Management System

+

Manage your rental properties, tenants, leases, and payments with ease.

+
+ + +

Welcome back! Access your dashboard to manage your properties.

+
+ +

Sign in to access your dashboard and manage your properties.

+ +
+
+
+
\ No newline at end of file diff --git a/Aquiis.Professional/Shared/Components/Pages/Error.razor b/Aquiis.Professional/Shared/Components/Pages/Error.razor new file mode 100644 index 0000000..7a84043 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/Aquiis.Professional/Shared/Components/Pages/Home.razor b/Aquiis.Professional/Shared/Components/Pages/Home.razor new file mode 100644 index 0000000..9f0c5d8 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Pages/Home.razor @@ -0,0 +1,478 @@ +@page "/" +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.EntityFrameworkCore +@using Aquiis.Professional.Infrastructure.Data +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.Professional.Shared.Components + +@inject NavigationManager NavigationManager +@inject PropertyService PropertyService +@inject TenantService TenantService +@inject LeaseService LeaseService +@inject MaintenanceService MaintenanceService +@inject InvoiceService InvoiceService +@inject UserContextService UserContextService +@inject ApplicationDbContext DbContext + +@rendermode InteractiveServer + +Dashboard - Property Management + + + + + + +
+
+

Property Management Dashboard

+
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else + { +
+
+
+
+
+
+

@totalProperties

+

Total Properties

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@availableProperties

+

Available Properties

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@totalTenants

+

Total Tenants

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@activeLeases

+

Active Leases

+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
Available Properties
+
+
+ @if (availablePropertiesList.Any()) + { +
+ @foreach (var property in availablePropertiesList) + { +
+
+
+ + @property.Address + +
+ @property.City, @property.State - @property.PropertyType +
+
+ @FormatPropertyStatus(property.Status) +
+ @property.MonthlyRent.ToString("C") +
+
+ } +
+ } + else + { +

No available properties found.

+ } +
+
+
+
+
+
+
Pending Leases
+
+
+ @if (pendingLeases.Any()) + { +
+ @foreach (var lease in pendingLeases) + { +
+
+
+ + @lease.Property.Address + +
+ @lease.CreatedOn.ToString("MMM dd, yyyy") +
+

@(lease.Tenant?.FullName ?? "Pending")

+
+ Start: @lease.StartDate.ToString("MMM dd, yyyy") + Pending +
+
+ } +
+ } + else + { +

No pending leases found.

+ } +
+
+
+
+ +
+
+ +
+
+
+
+
Open Maintenance Requests
+ View All +
+
+ @if (openMaintenanceRequests.Any()) + { +
+ @foreach (var request in openMaintenanceRequests) + { +
+
+
+
+ + @request.Title + +
+ + @request.Property?.Address - @request.RequestType + +
+
+ @request.Priority + @if (request.IsOverdue) + { +
+ Overdue + } +
+
+
+ @request.RequestedOn.ToString("MMM dd, yyyy") + @request.Status +
+
+ } +
+ } + else + { +

No open maintenance requests.

+ } +
+
+
+
+
+
+
Recent Invoices
+ View All +
+
+ @if (recentInvoices.Any()) + { +
+ @foreach (var invoice in recentInvoices) + { +
+
+
+ + @invoice.InvoiceNumber + +
+ @invoice.InvoicedOn.ToString("MMM dd, yyyy") +
+

@invoice.Lease?.Tenant?.FullName

+
+
+ Due: @invoice.DueOn.ToString("MMM dd, yyyy") + @if (invoice.IsOverdue) + { +
+ @invoice.DaysOverdue days overdue + } +
+
+ @invoice.Status +
+ @invoice.Amount.ToString("C") +
+
+
+ } +
+ } + else + { +

No recent invoices found.

+ } +
+
+
+
+ } +
+
+ +
+
+

Property Management System

+

Manage your rental properties, tenants, leases, and payments with ease.

+
+

Sign in to access your dashboard and manage your properties.

+ +
+
+ +
+
+
+
+
+ +
Property Management
+

Track and manage all your rental properties in one place.

+
+
+
+
+
+
+ +
Tenant Management
+

Manage tenant information, leases, and communications.

+
+
+
+
+
+
+ +
Payment Tracking
+

Track rent payments, invoices, and financial records.

+
+
+
+
+
+
+
+ +@code { + private bool isLoading = true; + private int totalProperties = 0; + private int availableProperties = 0; + private int totalTenants = 0; + private int activeLeases = 0; + + private List availablePropertiesList = new(); + private List pendingLeases = new List(); + private List openMaintenanceRequests = new List(); + private List recentInvoices = new List(); + + private List properties = new List(); + private List leases = new List(); + private List tenants = new List(); + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateTask; + if (authState.User.Identity?.IsAuthenticated == true) + { + await LoadDashboardData(); + } + isLoading = false; + } + + private async Task LoadDashboardData() + { + try + { + // Check authentication first + if (!await UserContextService.IsAuthenticatedAsync()) + return; + + var userId = await UserContextService.GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + return; + + var organizationId = await UserContextService.GetActiveOrganizationIdAsync(); + if (!organizationId.HasValue) + return; + + // Load summary counts + var allProperties = await PropertyService.GetAllAsync(); + properties = allProperties.Where(p => !p.IsDeleted).ToList(); + totalProperties = properties.Count; + availableProperties = properties.Count(p => p.IsAvailable); + + var allTenants = await TenantService.GetAllAsync(); + tenants = allTenants.Where(t => !t.IsDeleted).ToList(); + totalTenants = tenants.Count; + + var allLeases = await LeaseService.GetAllAsync(); + leases = allLeases.Where(l => !l.IsDeleted).ToList(); + activeLeases = leases.Count(l => l.Status == "Active"); + + // Load available properties and pending leases + availablePropertiesList = properties + .Where(p => p.OrganizationId == organizationId && p.IsAvailable) + .OrderByDescending(p => p.CreatedOn) + .Take(5) + .ToList(); + + pendingLeases = leases + .Where(l => l.OrganizationId == organizationId && l.Status == "Pending") + .OrderByDescending(l => l.CreatedOn) + .Take(5) + .ToList(); + + // Load open maintenance requests + var allMaintenanceRequests = await MaintenanceService.GetAllAsync(); + openMaintenanceRequests = allMaintenanceRequests + .Where(m => m.OrganizationId == organizationId && m.Status != "Completed" && m.Status != "Cancelled") + .OrderByDescending(m => m.Priority == "Urgent" ? 1 : m.Priority == "High" ? 2 : 3) + .ThenByDescending(m => m.RequestedOn) + .Take(5) + .ToList(); + + // Load recent invoices + var allInvoices = await InvoiceService.GetAllAsync(); + recentInvoices = allInvoices + .Where(i => i.Status != "Paid" && i.Status != "Cancelled") + .OrderByDescending(i => i.InvoicedOn) + .Take(5) + .ToList(); + } + catch (InvalidOperationException) + { + // UserContext not yet initialized - silent return + return; + } + } + + private string GetInvoiceStatusBadgeClass(string status) + { + return status switch + { + "Paid" => "bg-success", + "Pending" => "bg-warning", + "Overdue" => "bg-danger", + "Cancelled" => "bg-secondary", + _ => "bg-info" + }; + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + "Available" => "bg-success", + "ApplicationPending" => "bg-info", + "LeasePending" => "bg-warning", + "Occupied" => "bg-danger", + "UnderRenovation" => "bg-secondary", + "OffMarket" => "bg-dark", + _ => "bg-secondary" + }; + } + + private string FormatPropertyStatus(string status) + { + return status switch + { + "ApplicationPending" => "Application Pending", + "LeasePending" => "Lease Pending", + "UnderRenovation" => "Under Renovation", + "OffMarket" => "Off Market", + _ => status + }; + } + + private void NavigateToCalendar() + { + NavigationManager.NavigateTo("/propertymanagement/calendar"); + } +} diff --git a/Aquiis.Professional/Shared/Components/SchemaValidationWarning.razor b/Aquiis.Professional/Shared/Components/SchemaValidationWarning.razor new file mode 100644 index 0000000..f05c374 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/SchemaValidationWarning.razor @@ -0,0 +1,66 @@ +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@inject SchemaValidationService SchemaService +@inject NavigationManager Navigation +@rendermode InteractiveServer + +@if (showWarning && !isValid) +{ + +} + +@code { + [Parameter] + public string ExpectedVersion { get; set; } = "1.0.0"; + + private bool isValid = true; + private bool showWarning = true; + private string validationMessage = string.Empty; + private string? databaseVersion; + private string expectedVersion = "1.0.0"; + + protected override async Task OnInitializedAsync() + { + await ValidateSchema(); + } + + private async Task ValidateSchema() + { + try + { + var (valid, message, dbVersion) = await SchemaService.ValidateSchemaVersionAsync(); + isValid = valid; + validationMessage = message; + databaseVersion = dbVersion; + expectedVersion = ExpectedVersion; + } + catch (Exception ex) + { + isValid = false; + validationMessage = $"Error validating schema: {ex.Message}"; + } + } +} diff --git a/Aquiis.Professional/Shared/Components/SessionTimeoutModal.razor b/Aquiis.Professional/Shared/Components/SessionTimeoutModal.razor new file mode 100644 index 0000000..911d655 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/SessionTimeoutModal.razor @@ -0,0 +1,139 @@ +@inject SessionTimeoutService SessionTimeoutService +@inject NavigationManager NavigationManager +@inject IJSRuntime JSRuntime +@implements IDisposable + +@if (ShowWarning) +{ + +} + +@code { + private bool ShowWarning { get; set; } + private int SecondsRemaining { get; set; } + private DotNetObjectReference? _dotNetRef; + + protected override void OnInitialized() + { + SessionTimeoutService.OnWarningTriggered += HandleWarningTriggered; + SessionTimeoutService.OnWarningCountdown += HandleCountdown; + SessionTimeoutService.OnTimeout += HandleTimeout; + + // Start the service when modal is initialized + SessionTimeoutService.Start(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _dotNetRef = DotNetObjectReference.Create(this); + await JSRuntime.InvokeVoidAsync("sessionTimeoutManager.initialize", _dotNetRef); + } + } + + [JSInvokable] + public void RecordActivity() + { + SessionTimeoutService.RecordActivity(); + } + + private void HandleWarningTriggered() + { + InvokeAsync(() => + { + ShowWarning = true; + SecondsRemaining = SessionTimeoutService.WarningSecondsRemaining; + StateHasChanged(); + }); + } + + private void HandleCountdown(int secondsRemaining) + { + InvokeAsync(() => + { + SecondsRemaining = secondsRemaining; + StateHasChanged(); + }); + } + + private void HandleTimeout() + { + InvokeAsync(async () => + { + ShowWarning = false; + SessionTimeoutService.Stop(); + + // Find and submit the logout form that exists in NavMenu + await JSRuntime.InvokeVoidAsync("eval", "document.querySelector('form[action=\"Account/Logout\"]')?.submit()"); + }); + } + + private async Task ExtendSession() + { + ShowWarning = false; + SessionTimeoutService.ExtendSession(); + + // Refresh the session cookie on server + try + { + await JSRuntime.InvokeVoidAsync("fetch", "/api/session/refresh", new { method = "POST" }); + } + catch + { + // If refresh fails, just continue - the activity recording should be enough + } + + StateHasChanged(); + } + + private async Task Logout() + { + ShowWarning = false; + SessionTimeoutService.Stop(); + + // Find and submit the logout form that exists in NavMenu + await JSRuntime.InvokeVoidAsync("eval", "document.querySelector('form[action=\"Account/Logout\"]')?.submit()"); + } + + public void Dispose() + { + SessionTimeoutService.OnWarningTriggered -= HandleWarningTriggered; + SessionTimeoutService.OnWarningCountdown -= HandleCountdown; + SessionTimeoutService.OnTimeout -= HandleTimeout; + SessionTimeoutService.Stop(); + + if (_dotNetRef != null) + { + JSRuntime.InvokeVoidAsync("sessionTimeoutManager.dispose"); + _dotNetRef.Dispose(); + } + } +} diff --git a/Aquiis.Professional/Shared/Components/Shared/OrganizationAuthorizeView.razor b/Aquiis.Professional/Shared/Components/Shared/OrganizationAuthorizeView.razor new file mode 100644 index 0000000..59890ac --- /dev/null +++ b/Aquiis.Professional/Shared/Components/Shared/OrganizationAuthorizeView.razor @@ -0,0 +1,62 @@ +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Core.Constants + +@inject UserContextService UserContextService + +@if (_isAuthorized) +{ + @ChildContent +} +else if (NotAuthorized != null) +{ + @NotAuthorized +} + +@code { + [Parameter] + public string Roles { get; set; } = string.Empty; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public RenderFragment? NotAuthorized { get; set; } + + private bool _isAuthorized = false; + + protected override async Task OnInitializedAsync() + { + await CheckAuthorizationAsync(); + } + + private async Task CheckAuthorizationAsync() + { + if (string.IsNullOrWhiteSpace(Roles)) + { + _isAuthorized = false; + return; + } + + try + { + var userRole = await UserContextService.GetCurrentOrganizationRoleAsync(); + + if (string.IsNullOrEmpty(userRole)) + { + _isAuthorized = false; + return; + } + + var allowedRoles = Roles.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(r => r.Trim()) + .ToArray(); + + _isAuthorized = allowedRoles.Contains(userRole); + } + catch (InvalidOperationException) + { + // User doesn't have an active organization + _isAuthorized = false; + } + } +} diff --git a/Aquiis.Professional/Shared/Components/ToastContainer.razor b/Aquiis.Professional/Shared/Components/ToastContainer.razor new file mode 100644 index 0000000..04a6707 --- /dev/null +++ b/Aquiis.Professional/Shared/Components/ToastContainer.razor @@ -0,0 +1,164 @@ +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@implements IDisposable +@inject ToastService ToastService +@rendermode InteractiveServer + + + +
+ @foreach (var toast in _toasts) + { + + } +
+ +@code { + private List _toasts = new(); + private Dictionary _timers = new(); + private HashSet _removingToasts = new(); + + protected override void OnInitialized() + { + ToastService.OnShow += ShowToast; + } + + private void ShowToast(ToastMessage toast) + { + InvokeAsync(() => + { + _toasts.Add(toast); + StateHasChanged(); + + // Auto-remove after duration + var timer = new System.Threading.Timer(_ => + { + RemoveToast(toast.Id); + }, null, toast.Duration, System.Threading.Timeout.Infinite); + + _timers[toast.Id] = timer; + }); + } + + private void RemoveToast(string toastId) + { + InvokeAsync(async () => + { + var toast = _toasts.FirstOrDefault(t => t.Id == toastId); + if (toast != null && !_removingToasts.Contains(toastId)) + { + _removingToasts.Add(toastId); + StateHasChanged(); + + // Wait for slide-out animation to complete + await Task.Delay(300); + + _toasts.Remove(toast); + _removingToasts.Remove(toastId); + + if (_timers.ContainsKey(toastId)) + { + _timers[toastId].Dispose(); + _timers.Remove(toastId); + } + + StateHasChanged(); + } + }); + } + + private string GetAnimationClass(string toastId) + { + return _removingToasts.Contains(toastId) ? "toast-slide-out" : "toast-slide-in"; + } + + private string GetToastClass(ToastType type) + { + return type switch + { + ToastType.Success => "bg-success text-white", + ToastType.Error => "bg-danger text-white", + ToastType.Warning => "bg-warning text-dark", + ToastType.Info => "bg-info text-white", + _ => "bg-secondary text-white" + }; + } + + private string GetIconClass(ToastType type) + { + return type switch + { + ToastType.Success => "bi-check-circle-fill text-white", + ToastType.Error => "bi-exclamation-circle-fill text-white", + ToastType.Warning => "bi-exclamation-triangle-fill text-dark", + ToastType.Info => "bi-info-circle-fill text-white", + _ => "bi-bell-fill text-white" + }; + } + + private string GetTimeAgo(DateTime timestamp) + { + var timeSpan = DateTime.Now - timestamp; + + if (timeSpan.TotalSeconds < 60) + return "just now"; + if (timeSpan.TotalMinutes < 60) + return $"{(int)timeSpan.TotalMinutes}m ago"; + if (timeSpan.TotalHours < 24) + return $"{(int)timeSpan.TotalHours}h ago"; + + return timestamp.ToString("MMM d"); + } + + public void Dispose() + { + ToastService.OnShow -= ShowToast; + + foreach (var timer in _timers.Values) + { + timer.Dispose(); + } + _timers.Clear(); + } +} diff --git a/Aquiis.Professional/Shared/Layout/MainLayout.razor b/Aquiis.Professional/Shared/Layout/MainLayout.razor new file mode 100644 index 0000000..85f142e --- /dev/null +++ b/Aquiis.Professional/Shared/Layout/MainLayout.razor @@ -0,0 +1,55 @@ +@inherits LayoutComponentBase +@using Aquiis.Professional.Shared.Components +@inject ThemeService ThemeService +@implements IDisposable + +
+ + +
+
+ + + About + + +
+ + +
+
+
+
+ +
+ @Body +
+
+
+ + + + + + + + +
+ An unhandled error has occurred. + Reload + 🗙 +
+ +@code { + protected override void OnInitialized() + { + ThemeService.OnThemeChanged += StateHasChanged; + } + + public void Dispose() + { + ThemeService.OnThemeChanged -= StateHasChanged; + } +} diff --git a/Aquiis.Professional/Shared/Layout/MainLayout.razor.css b/Aquiis.Professional/Shared/Layout/MainLayout.razor.css new file mode 100644 index 0000000..393ac82 --- /dev/null +++ b/Aquiis.Professional/Shared/Layout/MainLayout.razor.css @@ -0,0 +1,103 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: var(--bs-body-bg) !important; + border-bottom: 1px solid var(--bs-border-color) !important; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + +.top-row ::deep a, +.top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + color: var(--bs-body-color) !important; +} + +.top-row ::deep a:hover, +.top-row ::deep .btn-link:hover { + text-decoration: underline; +} + +.top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, + .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, + article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} diff --git a/Aquiis.Professional/Shared/Layout/NavMenu.razor b/Aquiis.Professional/Shared/Layout/NavMenu.razor new file mode 100644 index 0000000..844ca00 --- /dev/null +++ b/Aquiis.Professional/Shared/Layout/NavMenu.razor @@ -0,0 +1,269 @@ +@implements IDisposable + +@inject NavigationManager NavigationManager +@inject ThemeService ThemeService +@inject IJSRuntime JSRuntime + + + + + + + + +@code { + private string? currentUrl; + private string currentTheme = "light"; + + protected override void OnInitialized() + { + currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); + NavigationManager.LocationChanged += OnLocationChanged; + ThemeService.OnThemeChanged += OnThemeChanged; + currentTheme = ThemeService.CurrentTheme; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + // Load saved theme from localStorage and sync with service + var savedTheme = await JSRuntime.InvokeAsync("eval", "localStorage.getItem('theme') || 'light'"); + + // Only update if different from current + if (ThemeService.CurrentTheme != savedTheme) + { + ThemeService.SetTheme(savedTheme); + } + + // Always ensure the DOM has the correct theme attribute + await JSRuntime.InvokeVoidAsync("themeManager.setTheme", savedTheme); + StateHasChanged(); + } + catch + { + // Fallback if localStorage not available + ThemeService.SetTheme("light"); + await JSRuntime.InvokeVoidAsync("themeManager.setTheme", "light"); + } + } + } + + private async Task ToggleTheme() + { + try + { + ThemeService.ToggleTheme(); + currentTheme = ThemeService.CurrentTheme; + // Save to localStorage and update DOM immediately + await JSRuntime.InvokeVoidAsync("themeManager.setTheme", ThemeService.CurrentTheme); + StateHasChanged(); + Console.WriteLine($"Theme toggled to: {ThemeService.CurrentTheme}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error toggling theme: {ex.Message}"); + } + } + + private string GetThemeIcon() + { + return currentTheme == "light" ? "bi-moon-fill" : "bi-sun-fill"; + } + + private void OnThemeChanged() + { + InvokeAsync(async () => + { + try + { + currentTheme = ThemeService.CurrentTheme; + // Re-apply theme when service notifies of change + await JSRuntime.InvokeVoidAsync("themeManager.setTheme", ThemeService.CurrentTheme); + } + catch { } + StateHasChanged(); + }); + } + + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + currentUrl = NavigationManager.ToBaseRelativePath(e.Location); + + // Re-apply current theme after navigation + try + { + await JSRuntime.InvokeVoidAsync("themeManager.setTheme", ThemeService.CurrentTheme); + } + catch { } + + StateHasChanged(); + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + ThemeService.OnThemeChanged -= OnThemeChanged; + } +} + diff --git a/Aquiis.Professional/Shared/Layout/NavMenu.razor.css b/Aquiis.Professional/Shared/Layout/NavMenu.razor.css new file mode 100644 index 0000000..01486ae --- /dev/null +++ b/Aquiis.Professional/Shared/Layout/NavMenu.razor.css @@ -0,0 +1,139 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 3.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") + no-repeat center/1.75rem rgba(255, 255, 255, 0.1); + z-index: 100; +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + min-height: 3.5rem; + background-color: rgba(0, 0, 0, 0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.bi-lock-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E"); +} + +.bi-person-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E"); +} + +.bi-person-badge-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E"); +} + +.bi-person-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E"); +} + +.bi-arrow-bar-left-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + +.nav-item:first-of-type { + padding-top: 1rem; +} + +.nav-item:last-of-type { + padding-bottom: 1rem; +} + +.nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; +} + +.nav-item ::deep a.active { + background-color: rgba(255, 255, 255, 0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); + color: white; +} + +.nav-separator { + border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.2); + margin: 0.5rem 1rem; +} + +@media (max-width: 640px) { + .top-row { + justify-content: space-between; + } + + .nav-scrollable { + display: none; + } + + .navbar-toggler:checked ~ .nav-scrollable { + display: block; + } +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/Aquiis.Professional/Shared/Routes.razor b/Aquiis.Professional/Shared/Routes.razor new file mode 100644 index 0000000..9207141 --- /dev/null +++ b/Aquiis.Professional/Shared/Routes.razor @@ -0,0 +1,11 @@ +@using Aquiis.Professional.Shared.Components.Account.Shared + + + + + + + + + + diff --git a/Aquiis.Professional/Shared/Services/DatabaseBackupService.cs b/Aquiis.Professional/Shared/Services/DatabaseBackupService.cs new file mode 100644 index 0000000..39faac1 --- /dev/null +++ b/Aquiis.Professional/Shared/Services/DatabaseBackupService.cs @@ -0,0 +1,414 @@ +using Aquiis.Professional.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using ElectronNET.API; + +namespace Aquiis.Professional.Shared.Services +{ + /// + /// Service for managing database backups and recovery operations. + /// Provides automatic backups before migrations, manual backup capability, + /// and recovery from corrupted databases. + /// + public class DatabaseBackupService + { + private readonly ILogger _logger; + private readonly ApplicationDbContext _dbContext; + private readonly IConfiguration _configuration; + private readonly ElectronPathService _electronPathService; + + public DatabaseBackupService( + ILogger logger, + ApplicationDbContext dbContext, + IConfiguration configuration, + ElectronPathService electronPathService) + { + _logger = logger; + _dbContext = dbContext; + _configuration = configuration; + _electronPathService = electronPathService; + } + + /// + /// Creates a backup of the SQLite database file + /// + /// Reason for backup (e.g., "Manual", "Pre-Migration", "Scheduled") + /// Path to the backup file, or null if backup failed + public async Task CreateBackupAsync(string backupReason = "Manual") + { + try + { + var dbPath = await GetDatabasePathAsync(); + _logger.LogInformation("Attempting to create backup of database at: {DbPath}", dbPath); + + if (!File.Exists(dbPath)) + { + _logger.LogWarning("Database file not found at {DbPath}, skipping backup", dbPath); + return null; + } + + var backupDir = Path.Combine(Path.GetDirectoryName(dbPath)!, "Backups"); + _logger.LogInformation("Creating backup directory: {BackupDir}", backupDir); + Directory.CreateDirectory(backupDir); + + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + var backupFileName = $"Aquiis_Backup_{backupReason}_{timestamp}.db"; + var backupPath = Path.Combine(backupDir, backupFileName); + + _logger.LogInformation("Backup will be created at: {BackupPath}", backupPath); + + // Force WAL checkpoint to flush all data from WAL file into main database file + try + { + var connection = _dbContext.Database.GetDbConnection(); + await connection.OpenAsync(); + using (var command = connection.CreateCommand()) + { + command.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + await command.ExecuteNonQueryAsync(); + _logger.LogInformation("WAL checkpoint completed - all data flushed to main database file"); + } + await connection.CloseAsync(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to checkpoint WAL before backup"); + } + + // Try to close any open connections before backup + try + { + await _dbContext.Database.CloseConnectionAsync(); + _logger.LogInformation("Database connection closed successfully"); + } + catch (Exception closeEx) + { + _logger.LogWarning(closeEx, "Error closing database connection, continuing anyway"); + } + + // Small delay to ensure file handles are released + await Task.Delay(100); + + // Copy the database file with retry logic + int retries = 3; + bool copied = false; + Exception? lastException = null; + + for (int i = 0; i < retries && !copied; i++) + { + try + { + File.Copy(dbPath, backupPath, overwrite: false); + copied = true; + _logger.LogInformation("Database file copied successfully on attempt {Attempt}", i + 1); + } + catch (IOException ioEx) when (i < retries - 1) + { + lastException = ioEx; + _logger.LogWarning("File copy attempt {Attempt} failed, retrying after delay: {Error}", + i + 1, ioEx.Message); + await Task.Delay(500); // Wait before retry + } + } + + if (!copied) + { + throw new IOException($"Failed to copy database file after {retries} attempts", lastException); + } + + _logger.LogInformation("Database backup created successfully: {BackupPath} (Reason: {Reason})", + backupPath, backupReason); + + // Clean up old backups (keep last 10) + await CleanupOldBackupsAsync(backupDir, keepCount: 10); + + return backupPath; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create database backup. Error: {ErrorMessage}", ex.Message); + return null; + } + } + + /// + /// Validates database integrity by attempting to open a connection and run a simple query + /// + /// True if database is healthy, false if corrupted + public async Task<(bool IsHealthy, string Message)> ValidateDatabaseHealthAsync() + { + try + { + // Try to open connection + await _dbContext.Database.OpenConnectionAsync(); + + // Try a simple query + var canQuery = await _dbContext.Database.CanConnectAsync(); + if (!canQuery) + { + return (false, "Cannot connect to database"); + } + + // SQLite-specific integrity check + var connection = _dbContext.Database.GetDbConnection(); + using var command = connection.CreateCommand(); + command.CommandText = "PRAGMA integrity_check;"; + + var result = await command.ExecuteScalarAsync(); + var integrityResult = result?.ToString() ?? "unknown"; + + if (integrityResult.Equals("ok", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Database integrity check passed"); + return (true, "Database is healthy"); + } + else + { + _logger.LogWarning("Database integrity check failed: {Result}", integrityResult); + return (false, $"Integrity check failed: {integrityResult}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Database health check failed"); + return (false, $"Health check error: {ex.Message}"); + } + finally + { + await _dbContext.Database.CloseConnectionAsync(); + } + } + + /// + /// Restores database from a backup file + /// + /// Path to the backup file to restore + /// True if restore was successful + public async Task RestoreFromBackupAsync(string backupPath) + { + try + { + if (!File.Exists(backupPath)) + { + _logger.LogError("Backup file not found: {BackupPath}", backupPath); + return false; + } + + var dbPath = await GetDatabasePathAsync(); + + // Close all connections and clear connection pool + await _dbContext.Database.CloseConnectionAsync(); + _dbContext.Dispose(); + + // Clear SQLite connection pool to release file locks + Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); + + // Give the system a moment to release file locks + await Task.Delay(100); + + // Create a backup of current database before restoring (with unique filename) + // Use milliseconds and a counter to ensure uniqueness + var timestamp = DateTime.Now.ToString("yyyyMMddHHmmssfff"); + var corruptedBackupPath = $"{dbPath}.corrupted.{timestamp}"; + + // If file still exists (very rare), add a counter + int counter = 1; + while (File.Exists(corruptedBackupPath)) + { + corruptedBackupPath = $"{dbPath}.corrupted.{timestamp}.{counter}"; + counter++; + } + + if (File.Exists(dbPath)) + { + // Move the current database to the corrupted backup path + File.Move(dbPath, corruptedBackupPath); + _logger.LogInformation("Current database moved to: {CorruptedPath}", corruptedBackupPath); + } + + // Restore from backup (now the original path is free) + File.Copy(backupPath, dbPath, overwrite: true); + + _logger.LogInformation("Database restored from backup: {BackupPath}", backupPath); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to restore database from backup"); + return false; + } + } + + /// + /// Lists all available backup files + /// + public async Task> GetAvailableBackupsAsync() + { + try + { + var dbPath = await GetDatabasePathAsync(); + var backupDir = Path.Combine(Path.GetDirectoryName(dbPath)!, "Backups"); + + if (!Directory.Exists(backupDir)) + { + return new List(); + } + + var backupFiles = Directory.GetFiles(backupDir, "*.db") + .OrderByDescending(f => File.GetCreationTime(f)) + .Select(f => new BackupInfo + { + FilePath = f, + FileName = Path.GetFileName(f), + CreatedDate = File.GetCreationTime(f), + SizeBytes = new FileInfo(f).Length, + SizeFormatted = FormatFileSize(new FileInfo(f).Length) + }) + .ToList(); + + return backupFiles; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to list backup files"); + return new List(); + } + } + + /// + /// Attempts to recover from a corrupted database by finding the most recent valid backup + /// + public async Task<(bool Success, string Message)> AutoRecoverFromCorruptionAsync() + { + try + { + _logger.LogWarning("Attempting automatic recovery from database corruption"); + + var backups = await GetAvailableBackupsAsync(); + if (!backups.Any()) + { + return (false, "No backup files available for recovery"); + } + + // Try each backup starting with the most recent + foreach (var backup in backups) + { + _logger.LogInformation("Attempting to restore from backup: {FileName}", backup.FileName); + + var restored = await RestoreFromBackupAsync(backup.FilePath); + if (restored) + { + return (true, $"Successfully recovered from backup: {backup.FileName}"); + } + } + + return (false, "All backup restoration attempts failed"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Auto-recovery failed"); + return (false, $"Auto-recovery error: {ex.Message}"); + } + } + + /// + /// Creates a backup before applying migrations (called from Program.cs) + /// + public async Task CreatePreMigrationBackupAsync() + { + var pendingMigrations = await _dbContext.Database.GetPendingMigrationsAsync(); + if (!pendingMigrations.Any()) + { + _logger.LogInformation("No pending migrations, skipping backup"); + return null; + } + + var migrationsCount = pendingMigrations.Count(); + var backupReason = $"PreMigration_{migrationsCount}Pending"; + + return await CreateBackupAsync(backupReason); + } + + /// + /// Gets the database file path for both Electron and web modes + /// + public async Task GetDatabasePathAsync() + { + if (HybridSupport.IsElectronActive) + { + return await _electronPathService.GetDatabasePathAsync(); + } + else + { + var connectionString = _configuration.GetConnectionString("DefaultConnection"); + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException("Connection string 'DefaultConnection' not found"); + } + + // Parse SQLite connection string - supports both "Data Source=" and "DataSource=" + var dbPath = connectionString + .Replace("Data Source=", "") + .Replace("DataSource=", "") + .Split(';')[0] + .Trim(); + + // Make absolute path if relative + if (!Path.IsPathRooted(dbPath)) + { + dbPath = Path.Combine(Directory.GetCurrentDirectory(), dbPath); + } + + _logger.LogInformation("Database path resolved to: {DbPath}", dbPath); + return dbPath; + } + } + + private async Task CleanupOldBackupsAsync(string backupDir, int keepCount) + { + await Task.Run(() => + { + try + { + var backupFiles = Directory.GetFiles(backupDir, "*.db") + .Select(f => new FileInfo(f)) + .OrderByDescending(f => f.CreationTime) + .Skip(keepCount) + .ToList(); + + foreach (var file in backupFiles) + { + file.Delete(); + _logger.LogInformation("Deleted old backup: {FileName}", file.Name); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup old backups"); + } + }); + } + + private string FormatFileSize(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB" }; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + } + + public class BackupInfo + { + public string FilePath { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public DateTime CreatedDate { get; set; } + public long SizeBytes { get; set; } + public string SizeFormatted { get; set; } = string.Empty; + } +} diff --git a/Aquiis.Professional/Shared/Services/ElectronPathService.cs b/Aquiis.Professional/Shared/Services/ElectronPathService.cs new file mode 100644 index 0000000..0fd5654 --- /dev/null +++ b/Aquiis.Professional/Shared/Services/ElectronPathService.cs @@ -0,0 +1,80 @@ +using ElectronNET.API; +using ElectronNET.API.Entities; +using Microsoft.Extensions.Configuration; + +namespace Aquiis.Professional.Shared.Services; + +public class ElectronPathService +{ + private readonly IConfiguration _configuration; + + public ElectronPathService(IConfiguration configuration) + { + _configuration = configuration; + } + + /// + /// Gets the database file path. Uses Electron's user data directory when running as desktop app, + /// otherwise uses the local Data folder for web mode. + /// + public async Task GetDatabasePathAsync() + { + var dbFileName = _configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; + + if (HybridSupport.IsElectronActive) + { + var userDataPath = await Electron.App.GetPathAsync(PathName.UserData); + var dbPath = Path.Combine(userDataPath, dbFileName); + + // Ensure the directory exists + var directory = Path.GetDirectoryName(dbPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + return dbPath; + } + else + { + // Web mode - use path from connection string or construct from settings + var connectionString = _configuration.GetConnectionString("DefaultConnection"); + if (!string.IsNullOrEmpty(connectionString)) + { + // Extract path from connection string + var dataSourcePrefix = connectionString.IndexOf("DataSource=", StringComparison.OrdinalIgnoreCase); + if (dataSourcePrefix >= 0) + { + var start = dataSourcePrefix + "DataSource=".Length; + var semicolonIndex = connectionString.IndexOf(';', start); + var path = semicolonIndex > 0 + ? connectionString.Substring(start, semicolonIndex - start) + : connectionString.Substring(start); + return path.Trim(); + } + } + + // Fallback to Infrastructure/Data directory + return Path.Combine("Infrastructure", "Data", dbFileName); + } + } + + /// + /// Gets the connection string for the database. + /// + public async Task GetConnectionStringAsync() + { + var dbPath = await GetDatabasePathAsync(); + return $"DataSource={dbPath};Cache=Shared"; + } + + /// + /// Static helper for early startup before DI is available. + /// Reads configuration directly from appsettings.json. + /// + public static async Task GetConnectionStringAsync(IConfiguration configuration) + { + var service = new ElectronPathService(configuration); + return await service.GetConnectionStringAsync(); + } +} diff --git a/Aquiis.Professional/Shared/Services/SessionTimeoutService.cs b/Aquiis.Professional/Shared/Services/SessionTimeoutService.cs new file mode 100644 index 0000000..391074d --- /dev/null +++ b/Aquiis.Professional/Shared/Services/SessionTimeoutService.cs @@ -0,0 +1,149 @@ +namespace Aquiis.Professional.Shared.Services; + +public class SessionTimeoutService +{ + private Timer? _warningTimer; + private Timer? _logoutTimer; + private DateTime _lastActivity; + private readonly object _lock = new(); + + public event Action? OnWarningTriggered; + public event Action? OnWarningCountdown; // Remaining seconds + public event Action? OnTimeout; + + public TimeSpan InactivityTimeout { get; set; } = TimeSpan.FromMinutes(10); + public TimeSpan WarningDuration { get; set; } = TimeSpan.FromMinutes(2); + public bool IsEnabled { get; set; } = true; + public bool IsWarningActive { get; private set; } + public int WarningSecondsRemaining { get; private set; } + + public SessionTimeoutService() + { + _lastActivity = DateTime.UtcNow; + } + + public void Start() + { + if (!IsEnabled) return; + + lock (_lock) + { + ResetActivity(); + StartMonitoring(); + } + } + + public void Stop() + { + lock (_lock) + { + _warningTimer?.Dispose(); + _logoutTimer?.Dispose(); + _warningTimer = null; + _logoutTimer = null; + IsWarningActive = false; + } + } + + public void RecordActivity() + { + if (!IsEnabled) return; + + lock (_lock) + { + _lastActivity = DateTime.UtcNow; + + // If warning is active, cancel it and restart monitoring + if (IsWarningActive) + { + CancelWarning(); + StartMonitoring(); + } + } + } + + public void ExtendSession() + { + RecordActivity(); + } + + private void StartMonitoring() + { + _warningTimer?.Dispose(); + _logoutTimer?.Dispose(); + + var warningTime = InactivityTimeout - WarningDuration; + + _warningTimer = new Timer( + _ => TriggerWarning(), + null, + warningTime, + Timeout.InfiniteTimeSpan + ); + } + + private void TriggerWarning() + { + lock (_lock) + { + if (!IsEnabled) return; + + IsWarningActive = true; + WarningSecondsRemaining = (int)WarningDuration.TotalSeconds; + + OnWarningTriggered?.Invoke(); + + // Start countdown timer + _logoutTimer = new Timer( + _ => CountdownTick(), + null, + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(1) + ); + } + } + + private void CountdownTick() + { + lock (_lock) + { + WarningSecondsRemaining--; + OnWarningCountdown?.Invoke(WarningSecondsRemaining); + + if (WarningSecondsRemaining <= 0) + { + TriggerTimeout(); + } + } + } + + private void TriggerTimeout() + { + lock (_lock) + { + IsWarningActive = false; + Stop(); + OnTimeout?.Invoke(); + } + } + + private void CancelWarning() + { + IsWarningActive = false; + _warningTimer?.Dispose(); + _logoutTimer?.Dispose(); + _warningTimer = null; + _logoutTimer = null; + } + + private void ResetActivity() + { + _lastActivity = DateTime.UtcNow; + IsWarningActive = false; + } + + public void Dispose() + { + Stop(); + } +} diff --git a/Aquiis.Professional/Shared/Services/ThemeService.cs b/Aquiis.Professional/Shared/Services/ThemeService.cs new file mode 100644 index 0000000..ecfc37f --- /dev/null +++ b/Aquiis.Professional/Shared/Services/ThemeService.cs @@ -0,0 +1,31 @@ +namespace Aquiis.Professional.Shared.Services; + +public class ThemeService +{ + private string _currentTheme = "light"; + + public event Action? OnThemeChanged; + + public string CurrentTheme => _currentTheme; + + public void SetTheme(string theme) + { + if (theme != "light" && theme != "dark") + { + throw new ArgumentException("Theme must be 'light' or 'dark'", nameof(theme)); + } + + _currentTheme = theme; + OnThemeChanged?.Invoke(); + } + + public void ToggleTheme() + { + SetTheme(_currentTheme == "light" ? "dark" : "light"); + } + + public string GetNextTheme() + { + return _currentTheme == "light" ? "dark" : "light"; + } +} diff --git a/Aquiis.Professional/Shared/Services/ToastService.cs b/Aquiis.Professional/Shared/Services/ToastService.cs new file mode 100644 index 0000000..0a39fc7 --- /dev/null +++ b/Aquiis.Professional/Shared/Services/ToastService.cs @@ -0,0 +1,76 @@ +using System; + +namespace Aquiis.Professional.Shared.Services +{ + public class ToastService + { + public event Action? OnShow; + + public void ShowSuccess(string message, string? title = null) + { + ShowToast(new ToastMessage + { + Type = ToastType.Success, + Title = title ?? "Success", + Message = message, + Duration = 15000 + }); + } + + public void ShowError(string message, string? title = null) + { + ShowToast(new ToastMessage + { + Type = ToastType.Error, + Title = title ?? "Error", + Message = message, + Duration = 17000 + }); + } + + public void ShowWarning(string message, string? title = null) + { + ShowToast(new ToastMessage + { + Type = ToastType.Warning, + Title = title ?? "Warning", + Message = message, + Duration = 16000 + }); + } + + public void ShowInfo(string message, string? title = null) + { + ShowToast(new ToastMessage + { + Type = ToastType.Info, + Title = title ?? "Info", + Message = message, + Duration = 15000 + }); + } + + private void ShowToast(ToastMessage message) + { + OnShow?.Invoke(message); + } + } + + public class ToastMessage + { + public string Id { get; set; } = Guid.NewGuid().ToString(); + public ToastType Type { get; set; } + public string Title { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public int Duration { get; set; } = 5000; + public DateTime Timestamp { get; set; } = DateTime.Now; + } + + public enum ToastType + { + Success, + Error, + Warning, + Info + } +} diff --git a/Aquiis.Professional/Shared/Services/UserContextService.cs b/Aquiis.Professional/Shared/Services/UserContextService.cs new file mode 100644 index 0000000..64d4c5a --- /dev/null +++ b/Aquiis.Professional/Shared/Services/UserContextService.cs @@ -0,0 +1,300 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Aquiis.Professional.Shared.Components.Account; +using Aquiis.Professional.Core.Entities; +using Aquiis.Professional.Core.Constants; +using System.Security.Claims; + +namespace Aquiis.Professional.Shared.Services +{ + + /// + /// Provides cached access to the current user's context information including OrganizationId. + /// This service is scoped per Blazor circuit, so the data is cached for the user's session. + /// + public class UserContextService + { + private readonly AuthenticationStateProvider _authenticationStateProvider; + private readonly UserManager _userManager; + private readonly Func> _organizationServiceFactory; + + // Cached values + private string? _userId; + private Guid? _organizationId; + private Guid? _activeOrganizationId; + private ApplicationUser? _currentUser; + private bool _isInitialized = false; + + public UserContextService( + AuthenticationStateProvider authenticationStateProvider, + UserManager userManager, + IServiceProvider serviceProvider) + { + _authenticationStateProvider = authenticationStateProvider; + _userManager = userManager; + // Use factory pattern to avoid circular dependency + _organizationServiceFactory = async () => + { + await Task.CompletedTask; + return serviceProvider.GetRequiredService(); + }; + } + + /// + /// Gets the current user's ID. Cached after first access. + /// + public async Task GetUserIdAsync() + { + await EnsureInitializedAsync(); + return _userId; + } + + /// + /// Gets the current user's OrganizationId. Cached after first access. + /// DEPRECATED: Use GetActiveOrganizationIdAsync() for multi-org support + /// + public async Task GetOrganizationIdAsync() + { + await EnsureInitializedAsync(); + return _organizationId; + } + + /// + /// Gets the current user's active organization ID (new multi-org support). + /// Throws InvalidOperationException if user has no active organization. + /// + public async Task GetActiveOrganizationIdAsync() + { + await EnsureInitializedAsync(); + + if (!_activeOrganizationId.HasValue || _activeOrganizationId == Guid.Empty) + { + throw new InvalidOperationException("User does not have an active organization. This is a critical security issue."); + } + + return _activeOrganizationId; + } + + /// + /// Gets the current ApplicationUser object. Cached after first access. + /// + public async Task GetCurrentUserAsync() + { + await EnsureInitializedAsync(); + return _currentUser; + } + + /// + /// Checks if a user is authenticated. + /// + public async Task IsAuthenticatedAsync() + { + var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); + return authState.User.Identity?.IsAuthenticated ?? false; + } + + /// + /// Gets the current user's email. + /// + public async Task GetUserEmailAsync() + { + await EnsureInitializedAsync(); + return _currentUser?.Email; + } + + /// + /// Gets the current user's full name. + /// + public async Task GetUserNameAsync() + { + await EnsureInitializedAsync(); + if (_currentUser != null) + { + return $"{_currentUser.FirstName} {_currentUser.LastName}".Trim(); + } + return null; + } + + /// + /// Checks if the current user is in the specified role. + /// + public async Task IsInRoleAsync(string role) + { + var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); + return authState.User.IsInRole(role); + } + + #region Multi-Organization Support + + /// + /// Get all organizations the current user has access to + /// + public async Task> GetAccessibleOrganizationsAsync() + { + var userId = await GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + return new List(); + + var organizationService = await _organizationServiceFactory(); + return await organizationService.GetUserOrganizationsAsync(userId); + } + + /// + /// Get the current user's role in the active organization + /// + public async Task GetCurrentOrganizationRoleAsync() + { + var userId = await GetUserIdAsync(); + var activeOrganizationId = await GetActiveOrganizationIdAsync(); + + if (string.IsNullOrEmpty(userId) || !activeOrganizationId.HasValue || activeOrganizationId == Guid.Empty) + return null; + + var organizationService = await _organizationServiceFactory(); + return await organizationService.GetUserRoleForOrganizationAsync(userId, activeOrganizationId.Value); + } + + /// + /// Get the active organization entity + /// + public async Task GetActiveOrganizationAsync() + { + var activeOrganizationId = await GetActiveOrganizationIdAsync(); + if (!activeOrganizationId.HasValue || activeOrganizationId == Guid.Empty) + return null; + + var organizationService = await _organizationServiceFactory(); + return await organizationService.GetOrganizationByIdAsync(activeOrganizationId.Value); + } + + /// + /// Get the organization entity by ID + /// + public async Task GetOrganizationByIdAsync(Guid organizationId) + { + if (organizationId == Guid.Empty) + return null; + + var organizationService = await _organizationServiceFactory(); + return await organizationService.GetOrganizationByIdAsync(organizationId); + } + + /// + /// Switch the user's active organization + /// + public async Task SwitchOrganizationAsync(Guid organizationId) + { + var userId = await GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + return false; + + // Verify user has access to this organization + var organizationService = await _organizationServiceFactory(); + if (!await organizationService.CanAccessOrganizationAsync(userId, organizationId)) + return false; + + // Update user's active organization + var user = await GetCurrentUserAsync(); + if (user == null) + return false; + + user.ActiveOrganizationId = organizationId; + var result = await _userManager.UpdateAsync(user); + + if (result.Succeeded) + { + // Refresh cache + await RefreshAsync(); + return true; + } + + return false; + } + + /// + /// Check if the current user has a specific permission in their active organization + /// + public async Task HasPermissionAsync(string permission) + { + var role = await GetCurrentOrganizationRoleAsync(); + if (string.IsNullOrEmpty(role)) + return false; + + // Permission checks based on role + return permission.ToLower() switch + { + "organizations.create" => role == ApplicationConstants.OrganizationRoles.Owner, + "organizations.delete" => role == ApplicationConstants.OrganizationRoles.Owner, + "organizations.backup" => role == ApplicationConstants.OrganizationRoles.Owner, + "organizations.deletedata" => role == ApplicationConstants.OrganizationRoles.Owner, + "settings.edit" => ApplicationConstants.OrganizationRoles.CanEditSettings(role), + "settings.retention" => role == ApplicationConstants.OrganizationRoles.Owner || role == ApplicationConstants.OrganizationRoles.Administrator, + "users.manage" => ApplicationConstants.OrganizationRoles.CanManageUsers(role), + "properties.manage" => role != ApplicationConstants.OrganizationRoles.User, + _ => false + }; + } + + /// + /// Check if the current user is an account owner (owns at least one organization) + /// + public async Task IsAccountOwnerAsync() + { + var userId = await GetUserIdAsync(); + if (string.IsNullOrEmpty(userId)) + return false; + + var organizationService = await _organizationServiceFactory(); + var ownedOrgs = await organizationService.GetOwnedOrganizationsAsync(userId); + return ownedOrgs.Any(); + } + + #endregion + + /// + /// Forces a refresh of the cached user data. + /// Call this if user data has been updated and you need to reload it. + /// + public async Task RefreshAsync() + { + _isInitialized = false; + _userId = null; + _organizationId = null; + _activeOrganizationId = null; + _currentUser = null; + await EnsureInitializedAsync(); + } + + /// + /// Initializes the user context by loading user data from the database. + /// This is called automatically on first access and cached for subsequent calls. + /// + private async Task EnsureInitializedAsync() + { + if (_isInitialized) + return; + + var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + + if (user.Identity?.IsAuthenticated == true) + { + var claimsUserId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + if (!string.IsNullOrEmpty(claimsUserId)) + { + _userId = claimsUserId; + } + { + _currentUser = await _userManager.FindByIdAsync(_userId!); + if (_currentUser != null) + { + _activeOrganizationId = _currentUser.ActiveOrganizationId; // New multi-org + } + } + } + + _isInitialized = true; + } + } +} diff --git a/Aquiis.Professional/Shared/_Imports.razor b/Aquiis.Professional/Shared/_Imports.razor new file mode 100644 index 0000000..a88b088 --- /dev/null +++ b/Aquiis.Professional/Shared/_Imports.razor @@ -0,0 +1,19 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Aquiis.Professional +@using Aquiis.Professional.Application.Services +@using Aquiis.Professional.Application.Services.PdfGenerators +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Shared.Layout +@using Aquiis.Professional.Shared.Components +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.Professional.Shared.Components.Shared +@using Aquiis.Professional.Core.Entities +@using Aquiis.Professional.Core.Constants diff --git a/Aquiis.Professional/Utilities/CalendarEventRouter.cs b/Aquiis.Professional/Utilities/CalendarEventRouter.cs new file mode 100644 index 0000000..0b92640 --- /dev/null +++ b/Aquiis.Professional/Utilities/CalendarEventRouter.cs @@ -0,0 +1,59 @@ +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Utilities +{ + /// + /// Helper class for routing calendar event clicks to appropriate detail pages + /// + public static class CalendarEventRouter + { + /// + /// Get the route URL for a calendar event based on its source entity type + /// + /// The calendar event + /// The route URL or null if it's a custom event or routing not available + public static string? GetRouteForEvent(CalendarEvent evt) + { + if (!evt.SourceEntityId.HasValue || string.IsNullOrEmpty(evt.SourceEntityType)) + return null; + + return evt.SourceEntityType switch + { + nameof(Tour) => $"/PropertyManagement/Tours/Details/{evt.SourceEntityId}", + nameof(Inspection) => $"/PropertyManagement/Inspections/View/{evt.SourceEntityId}", + nameof(MaintenanceRequest) => $"/PropertyManagement/Maintenance/View/{evt.SourceEntityId}", + // Add new schedulable entity routes here as they are created + _ => null + }; + } + + /// + /// Check if an event is routable (has a valid source entity and route) + /// + /// The calendar event + /// True if the event can be routed to a detail page + public static bool IsRoutable(CalendarEvent evt) + { + return !string.IsNullOrEmpty(GetRouteForEvent(evt)); + } + + /// + /// Get a display label for the event type + /// + /// The calendar event + /// User-friendly label for the event source + public static string GetSourceLabel(CalendarEvent evt) + { + if (evt.IsCustomEvent) + return "Custom Event"; + + return evt.SourceEntityType switch + { + nameof(Tour) => "Property Tour", + nameof(Inspection) => "Property Inspection", + nameof(MaintenanceRequest) => "Maintenance Request", + _ => evt.EventType + }; + } + } +} diff --git a/Aquiis.Professional/Utilities/SchedulableEntityRegistry.cs b/Aquiis.Professional/Utilities/SchedulableEntityRegistry.cs new file mode 100644 index 0000000..766fb8f --- /dev/null +++ b/Aquiis.Professional/Utilities/SchedulableEntityRegistry.cs @@ -0,0 +1,87 @@ +using System.Reflection; +using Aquiis.Professional.Core.Entities; + +namespace Aquiis.Professional.Utilities; + +public static class SchedulableEntityRegistry +{ + private static List? _entityTypes; + private static Dictionary? _entityTypeMap; + + public static List GetSchedulableEntityTypes() + { + if (_entityTypes == null) + { + _entityTypes = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => typeof(ISchedulableEntity).IsAssignableFrom(t) + && t.IsClass && !t.IsAbstract) + .ToList(); + } + return _entityTypes; + } + + public static List GetEntityTypeNames() + { + var types = GetSchedulableEntityTypes(); + var names = new List(); + + foreach (var type in types) + { + try + { + // Create a temporary instance to get the event type name + var instance = Activator.CreateInstance(type) as ISchedulableEntity; + if (instance != null) + { + var eventType = instance.GetEventType(); + if (!string.IsNullOrEmpty(eventType) && !names.Contains(eventType)) + { + names.Add(eventType); + } + } + } + catch + { + // If instantiation fails, use the class name as fallback + if (!names.Contains(type.Name)) + { + names.Add(type.Name); + } + } + } + + return names; + } + + public static Dictionary GetEntityTypeMap() + { + if (_entityTypeMap == null) + { + _entityTypeMap = new Dictionary(); + var types = GetSchedulableEntityTypes(); + + foreach (var type in types) + { + try + { + var instance = Activator.CreateInstance(type) as ISchedulableEntity; + if (instance != null) + { + var eventType = instance.GetEventType(); + if (!string.IsNullOrEmpty(eventType) && !_entityTypeMap.ContainsKey(eventType)) + { + _entityTypeMap[eventType] = type; + } + } + } + catch + { + // Skip types that can't be instantiated + } + } + } + + return _entityTypeMap; + } +} diff --git a/Aquiis.Professional/appsettings.Development.json b/Aquiis.Professional/appsettings.Development.json new file mode 100644 index 0000000..3cacbdc --- /dev/null +++ b/Aquiis.Professional/appsettings.Development.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "SessionTimeout": { + "InactivityTimeoutMinutes": 18, + "WarningDurationMinutes": 1, + "Enabled": true + } +} diff --git a/Aquiis.Professional/appsettings.json b/Aquiis.Professional/appsettings.json new file mode 100644 index 0000000..5654d9a --- /dev/null +++ b/Aquiis.Professional/appsettings.json @@ -0,0 +1,49 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "DataSource=Infrastructure/Data/app_v0.3.0.db;Cache=Shared" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + }, + "AllowedHosts": "*", + "ApplicationSettings": { + "AppName": "Aquiis", + "Version": "0.3.0", + "Author": "CIS Guru", + "Email": "cisguru@outlook.com", + "Repository": "https://github.com/xnodeoncode/Aquiis", + "SoftDeleteEnabled": true, + "DatabaseFileName": "app_v0.3.0.db", + "PreviousDatabaseFileName": "app_v0.0.0.db", + "SchemaVersion": "0.3.0" + }, + "SessionTimeout": { + "InactivityTimeoutMinutes": 18, + "WarningDurationMinutes": 3, + "Enabled": true + }, + "DataProtection": { + "ApplicationName": "Aquiis" + }, + "Notifications": { + "EnableInApp": true, + "EnableEmail": true, + "EnableSMS": true, + "GracefulDegradation": true + }, + "SendGrid": { + "ApiKey": "{{SENDGRID_API_KEY}}", + "FromEmail": "noreply@aquiis.com", + "FromName": "Aquiis Property Management" + }, + "Twilio": { + "AccountSid": "{{TWILIO_ACCOUNT_SID}}", + "AuthToken": "{{TWILIO_AUTH_TOKEN}}", + "PhoneNumber": "{{TWILIO_PHONE_NUMBER}}" + } +} diff --git a/Aquiis.Professional/electron.manifest.json b/Aquiis.Professional/electron.manifest.json new file mode 100644 index 0000000..e48efaf --- /dev/null +++ b/Aquiis.Professional/electron.manifest.json @@ -0,0 +1,50 @@ +{ + "executable": "Aquiis.Professional", + "splashscreen": { + "imageFile": "wwwroot/assets/splash.png" + }, + "name": "aquiis-property-management", + "author": "Aquiis", + "singleInstance": false, + "environment": "Production", + "aspCoreBackendPort": 8888, + "build": { + "appId": "com.aquiis.propertymanagement", + "productName": "AquiisPropertyManagement", + "copyright": "Copyright © 2025", + "buildVersion": "1.0.0", + "compression": "maximum", + "directories": { + "output": "../../../bin/Desktop" + }, + "extraResources": [ + { + "from": "./bin", + "to": "bin", + "filter": ["**/*"] + } + ], + "files": [ + { + "from": "./ElectronHostHook/node_modules", + "to": "ElectronHostHook/node_modules", + "filter": ["**/*"] + }, + "**/*" + ], + "mac": { + "target": "dmg", + "icon": "Assets/icon.icns", + "category": "public.app-category.business" + }, + "win": { + "target": "nsis", + "icon": "Assets/icon.ico" + }, + "linux": { + "target": "AppImage", + "icon": "Assets/icon.png", + "category": "Office" + } + } +} diff --git a/Aquiis.Professional/libman.json b/Aquiis.Professional/libman.json new file mode 100644 index 0000000..3f9a280 --- /dev/null +++ b/Aquiis.Professional/libman.json @@ -0,0 +1,10 @@ +{ + "version": "3.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "bootstrap@5.3.8", + "destination": "wwwroot/lib/bootstrap" + } + ] +} \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/app.css b/Aquiis.Professional/wwwroot/app.css new file mode 100644 index 0000000..1127dd4 --- /dev/null +++ b/Aquiis.Professional/wwwroot/app.css @@ -0,0 +1,112 @@ +html, +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + background-color: var(--bs-body-bg) !important; + color: var(--bs-body-color) !important; + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* Ensure color-scheme is set for proper dark mode rendering */ +html[data-bs-theme="dark"] { + color-scheme: dark; +} + +html[data-bs-theme="light"] { + color-scheme: light; +} + +main { + background-color: var(--bs-body-bg) !important; + color: var(--bs-body-color) !important; +} + +.content { + background-color: var(--bs-body-bg) !important; + padding-top: 1.1rem; +} + +article { + background-color: var(--bs-body-bg) !important; + color: var(--bs-body-color) !important; +} + +a, +.btn-link { + color: #006bb7; +} + +[data-bs-theme="dark"] a, +[data-bs-theme="dark"] .btn-link { + color: #6ea8fe; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Checklist template card hover effect */ +.hover-shadow { + transition: all 0.3s ease; +} + +.hover-shadow:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; + transform: translateY(-2px); +} + +.btn:focus, +.btn:active:focus, +.btn-link.nav-link:focus, +.form-control:focus, +.form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type="checkbox"]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) + no-repeat 1rem/1.8rem, + #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + +.blazor-error-boundary::after { + content: "An error has occurred."; +} + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} + +.form-floating > .form-control-plaintext::placeholder, +.form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, +.form-floating > .form-control:focus::placeholder { + text-align: start; +} diff --git a/Aquiis.Professional/wwwroot/assets/database-fill-gear.svg b/Aquiis.Professional/wwwroot/assets/database-fill-gear.svg new file mode 100644 index 0000000..94fc2e6 --- /dev/null +++ b/Aquiis.Professional/wwwroot/assets/database-fill-gear.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/assets/database.svg b/Aquiis.Professional/wwwroot/assets/database.svg new file mode 100644 index 0000000..231c50c --- /dev/null +++ b/Aquiis.Professional/wwwroot/assets/database.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/assets/splash.png b/Aquiis.Professional/wwwroot/assets/splash.png new file mode 100644 index 0000000..2b6ea98 Binary files /dev/null and b/Aquiis.Professional/wwwroot/assets/splash.png differ diff --git a/Aquiis.Professional/wwwroot/assets/splash.svg b/Aquiis.Professional/wwwroot/assets/splash.svg new file mode 100644 index 0000000..d8d33cf --- /dev/null +++ b/Aquiis.Professional/wwwroot/assets/splash.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + Aquiis + + + + + Property Management + + + + + Loading application... + + + + + + + + + + + + + diff --git a/Aquiis.Professional/wwwroot/css/organization-switcher.css b/Aquiis.Professional/wwwroot/css/organization-switcher.css new file mode 100644 index 0000000..9fecf17 --- /dev/null +++ b/Aquiis.Professional/wwwroot/css/organization-switcher.css @@ -0,0 +1,47 @@ +/* Organization Switcher Component Styles */ +.org-switcher { + margin-left: auto; +} + +.org-switcher .btn { + min-width: 200px; + text-align: left; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.org-switcher .org-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.org-switcher .dropdown-menu { + min-width: 300px; + max-height: 400px; + overflow-y: auto; +} + +.org-switcher .dropdown-item { + padding: 0.75rem 1rem; +} + +.org-switcher .dropdown-item.active { + background-color: var(--bs-primary-bg-subtle); + color: var(--bs-emphasis-color); +} + +.org-switcher .dropdown-item:hover { + background-color: var(--bs-secondary-bg); +} + +.org-switcher-skeleton { + margin-left: auto; + padding: 0.5rem 1rem; +} + +.org-switcher .fw-semibold { + font-weight: 600; +} diff --git a/Aquiis.Professional/wwwroot/favicon.png b/Aquiis.Professional/wwwroot/favicon.png new file mode 100644 index 0000000..8422b59 Binary files /dev/null and b/Aquiis.Professional/wwwroot/favicon.png differ diff --git a/Aquiis.Professional/wwwroot/js/fileDownload.js b/Aquiis.Professional/wwwroot/js/fileDownload.js new file mode 100644 index 0000000..f4be8a0 --- /dev/null +++ b/Aquiis.Professional/wwwroot/js/fileDownload.js @@ -0,0 +1,74 @@ +// File download and view functionality for Blazor applications + +/** + * Downloads a file from base64 data + * @param {string} filename - The name of the file to download + * @param {string} base64Data - Base64 encoded file data + * @param {string} mimeType - MIME type of the file (e.g., 'application/pdf') + */ +window.downloadFile = function (filename, base64Data, mimeType) { + try { + // Convert base64 to binary + const byteCharacters = atob(base64Data); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + + // Create blob and download + const blob = new Blob([byteArray], { type: mimeType }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up the URL object + window.URL.revokeObjectURL(url); + } catch (error) { + console.error("Error downloading file:", error); + alert("Error downloading file. Please try again."); + } +}; + +/** + * Opens a file in a new browser tab for viewing + * @param {string} base64Data - Base64 encoded file data + * @param {string} mimeType - MIME type of the file (e.g., 'application/pdf') + */ +window.viewFile = function (base64Data, mimeType) { + try { + // Convert base64 to binary + const byteCharacters = atob(base64Data); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + + // Create blob and open in new tab + const blob = new Blob([byteArray], { type: mimeType }); + const url = window.URL.createObjectURL(blob); + + // Open in new window/tab + const newWindow = window.open(url, "_blank"); + + // If popup was blocked, provide alternative + if ( + !newWindow || + newWindow.closed || + typeof newWindow.closed === "undefined" + ) { + alert("Please allow popups for this site to view files."); + } + + // Note: We don't revoke the URL immediately as the new window needs it + // The browser will clean it up when the window is closed + } catch (error) { + console.error("Error viewing file:", error); + alert("Error viewing file. Please try again."); + } +}; diff --git a/Aquiis.Professional/wwwroot/js/sessionTimeout.js b/Aquiis.Professional/wwwroot/js/sessionTimeout.js new file mode 100644 index 0000000..f726b95 --- /dev/null +++ b/Aquiis.Professional/wwwroot/js/sessionTimeout.js @@ -0,0 +1,57 @@ +window.sessionTimeoutManager = { + dotNetRef: null, + activityEvents: ["mousedown", "mousemove", "keydown", "scroll", "touchstart"], + isTracking: false, + activityHandler: null, + + initialize: function (dotNetReference) { + this.dotNetRef = dotNetReference; + + // Create the activity handler once and store it + this.activityHandler = () => { + this.recordActivity(); + }; + + this.startTracking(); + console.log("Session timeout tracking initialized"); + }, + + startTracking: function () { + if (this.isTracking || !this.activityHandler) return; + + this.activityEvents.forEach((event) => { + document.addEventListener(event, this.activityHandler, { passive: true }); + }); + + this.isTracking = true; + console.log("Activity tracking started"); + }, + + stopTracking: function () { + if (!this.isTracking || !this.activityHandler) return; + + this.activityEvents.forEach((event) => { + document.removeEventListener(event, this.activityHandler); + }); + + this.isTracking = false; + console.log("Activity tracking stopped"); + }, + + recordActivity: function () { + if (this.dotNetRef) { + try { + this.dotNetRef.invokeMethodAsync("RecordActivity"); + } catch (error) { + console.error("Error recording activity:", error); + } + } + }, + + dispose: function () { + this.stopTracking(); + this.dotNetRef = null; + this.activityHandler = null; + console.log("Session timeout manager disposed"); + }, +}; diff --git a/Aquiis.Professional/wwwroot/js/theme.js b/Aquiis.Professional/wwwroot/js/theme.js new file mode 100644 index 0000000..168c1bf --- /dev/null +++ b/Aquiis.Professional/wwwroot/js/theme.js @@ -0,0 +1,58 @@ +window.themeManager = { + setTheme: function (theme) { + //console.log("Setting theme to:", theme); + document.documentElement.setAttribute("data-bs-theme", theme); + localStorage.setItem("theme", theme); + + // Force browser to recalculate CSS custom properties + // by triggering a reflow on the root element + document.documentElement.style.display = "none"; + void document.documentElement.offsetHeight; // Trigger reflow + document.documentElement.style.display = ""; + + // console.log( + // "Theme set. Current attribute:", + // document.documentElement.getAttribute("data-bs-theme") + // ); + }, + + getTheme: function () { + const theme = localStorage.getItem("theme") || "light"; + //console.log("Getting theme from localStorage:", theme); + return theme; + }, + + initTheme: function () { + const savedTheme = this.getTheme(); + this.setTheme(savedTheme); + return savedTheme; + }, +}; + +// Initialize theme IMMEDIATELY (before DOMContentLoaded) to prevent flash +if (typeof localStorage !== "undefined") { + const savedTheme = localStorage.getItem("theme") || "light"; + console.log("Initial theme load:", savedTheme); + document.documentElement.setAttribute("data-bs-theme", savedTheme); + + // Watch for Blazor navigation and re-apply theme + // This handles Interactive Server mode where components persist + const observer = new MutationObserver(function (mutations) { + const currentTheme = document.documentElement.getAttribute("data-bs-theme"); + if (currentTheme) { + // Re-trigger reflow to ensure CSS variables are applied + document.documentElement.style.display = "none"; + void document.documentElement.offsetHeight; + document.documentElement.style.display = ""; + //console.log("Theme re-applied after DOM mutation:", currentTheme); + } + }); + + // Start observing after a short delay to let Blazor initialize + setTimeout(() => { + observer.observe(document.body, { + childList: true, + subtree: true, + }); + }, 1000); +} diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css new file mode 100644 index 0000000..448e442 --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css @@ -0,0 +1,4085 @@ +/*! + * Bootstrap Grid v5.3.8 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +.container, +.container-fluid, +.container-xxl, +.container-xl, +.container-lg, +.container-md, +.container-sm { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + width: 100%; + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container-sm, .container { + max-width: 540px; + } +} +@media (min-width: 768px) { + .container-md, .container-sm, .container { + max-width: 720px; + } +} +@media (min-width: 992px) { + .container-lg, .container-md, .container-sm, .container { + max-width: 960px; + } +} +@media (min-width: 1200px) { + .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1140px; + } +} +@media (min-width: 1400px) { + .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1320px; + } +} +:root { + --bs-breakpoint-xs: 0; + --bs-breakpoint-sm: 576px; + --bs-breakpoint-md: 768px; + --bs-breakpoint-lg: 992px; + --bs-breakpoint-xl: 1200px; + --bs-breakpoint-xxl: 1400px; +} + +.row { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + display: flex; + flex-wrap: wrap; + margin-top: calc(-1 * var(--bs-gutter-y)); + margin-right: calc(-0.5 * var(--bs-gutter-x)); + margin-left: calc(-0.5 * var(--bs-gutter-x)); +} +.row > * { + box-sizing: border-box; + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); + margin-top: var(--bs-gutter-y); +} + +.col { + flex: 1 0 0; +} + +.row-cols-auto > * { + flex: 0 0 auto; + width: auto; +} + +.row-cols-1 > * { + flex: 0 0 auto; + width: 100%; +} + +.row-cols-2 > * { + flex: 0 0 auto; + width: 50%; +} + +.row-cols-3 > * { + flex: 0 0 auto; + width: 33.33333333%; +} + +.row-cols-4 > * { + flex: 0 0 auto; + width: 25%; +} + +.row-cols-5 > * { + flex: 0 0 auto; + width: 20%; +} + +.row-cols-6 > * { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; +} + +.col-1 { + flex: 0 0 auto; + width: 8.33333333%; +} + +.col-2 { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-3 { + flex: 0 0 auto; + width: 25%; +} + +.col-4 { + flex: 0 0 auto; + width: 33.33333333%; +} + +.col-5 { + flex: 0 0 auto; + width: 41.66666667%; +} + +.col-6 { + flex: 0 0 auto; + width: 50%; +} + +.col-7 { + flex: 0 0 auto; + width: 58.33333333%; +} + +.col-8 { + flex: 0 0 auto; + width: 66.66666667%; +} + +.col-9 { + flex: 0 0 auto; + width: 75%; +} + +.col-10 { + flex: 0 0 auto; + width: 83.33333333%; +} + +.col-11 { + flex: 0 0 auto; + width: 91.66666667%; +} + +.col-12 { + flex: 0 0 auto; + width: 100%; +} + +.offset-1 { + margin-left: 8.33333333%; +} + +.offset-2 { + margin-left: 16.66666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.33333333%; +} + +.offset-5 { + margin-left: 41.66666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.33333333%; +} + +.offset-8 { + margin-left: 66.66666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.33333333%; +} + +.offset-11 { + margin-left: 91.66666667%; +} + +.g-0, +.gx-0 { + --bs-gutter-x: 0; +} + +.g-0, +.gy-0 { + --bs-gutter-y: 0; +} + +.g-1, +.gx-1 { + --bs-gutter-x: 0.25rem; +} + +.g-1, +.gy-1 { + --bs-gutter-y: 0.25rem; +} + +.g-2, +.gx-2 { + --bs-gutter-x: 0.5rem; +} + +.g-2, +.gy-2 { + --bs-gutter-y: 0.5rem; +} + +.g-3, +.gx-3 { + --bs-gutter-x: 1rem; +} + +.g-3, +.gy-3 { + --bs-gutter-y: 1rem; +} + +.g-4, +.gx-4 { + --bs-gutter-x: 1.5rem; +} + +.g-4, +.gy-4 { + --bs-gutter-y: 1.5rem; +} + +.g-5, +.gx-5 { + --bs-gutter-x: 3rem; +} + +.g-5, +.gy-5 { + --bs-gutter-y: 3rem; +} + +@media (min-width: 576px) { + .col-sm { + flex: 1 0 0; + } + .row-cols-sm-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-sm-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-sm-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-sm-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-sm-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-sm-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-sm-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-sm-auto { + flex: 0 0 auto; + width: auto; + } + .col-sm-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-sm-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-sm-3 { + flex: 0 0 auto; + width: 25%; + } + .col-sm-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-sm-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-sm-6 { + flex: 0 0 auto; + width: 50%; + } + .col-sm-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-sm-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-sm-9 { + flex: 0 0 auto; + width: 75%; + } + .col-sm-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-sm-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-sm-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.33333333%; + } + .offset-sm-2 { + margin-left: 16.66666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.33333333%; + } + .offset-sm-5 { + margin-left: 41.66666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.33333333%; + } + .offset-sm-8 { + margin-left: 66.66666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.33333333%; + } + .offset-sm-11 { + margin-left: 91.66666667%; + } + .g-sm-0, + .gx-sm-0 { + --bs-gutter-x: 0; + } + .g-sm-0, + .gy-sm-0 { + --bs-gutter-y: 0; + } + .g-sm-1, + .gx-sm-1 { + --bs-gutter-x: 0.25rem; + } + .g-sm-1, + .gy-sm-1 { + --bs-gutter-y: 0.25rem; + } + .g-sm-2, + .gx-sm-2 { + --bs-gutter-x: 0.5rem; + } + .g-sm-2, + .gy-sm-2 { + --bs-gutter-y: 0.5rem; + } + .g-sm-3, + .gx-sm-3 { + --bs-gutter-x: 1rem; + } + .g-sm-3, + .gy-sm-3 { + --bs-gutter-y: 1rem; + } + .g-sm-4, + .gx-sm-4 { + --bs-gutter-x: 1.5rem; + } + .g-sm-4, + .gy-sm-4 { + --bs-gutter-y: 1.5rem; + } + .g-sm-5, + .gx-sm-5 { + --bs-gutter-x: 3rem; + } + .g-sm-5, + .gy-sm-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 768px) { + .col-md { + flex: 1 0 0; + } + .row-cols-md-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-md-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-md-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-md-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-md-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-md-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-md-auto { + flex: 0 0 auto; + width: auto; + } + .col-md-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-md-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-md-3 { + flex: 0 0 auto; + width: 25%; + } + .col-md-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-md-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-md-6 { + flex: 0 0 auto; + width: 50%; + } + .col-md-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-md-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-md-9 { + flex: 0 0 auto; + width: 75%; + } + .col-md-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-md-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-md-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.33333333%; + } + .offset-md-2 { + margin-left: 16.66666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.33333333%; + } + .offset-md-5 { + margin-left: 41.66666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.33333333%; + } + .offset-md-8 { + margin-left: 66.66666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.33333333%; + } + .offset-md-11 { + margin-left: 91.66666667%; + } + .g-md-0, + .gx-md-0 { + --bs-gutter-x: 0; + } + .g-md-0, + .gy-md-0 { + --bs-gutter-y: 0; + } + .g-md-1, + .gx-md-1 { + --bs-gutter-x: 0.25rem; + } + .g-md-1, + .gy-md-1 { + --bs-gutter-y: 0.25rem; + } + .g-md-2, + .gx-md-2 { + --bs-gutter-x: 0.5rem; + } + .g-md-2, + .gy-md-2 { + --bs-gutter-y: 0.5rem; + } + .g-md-3, + .gx-md-3 { + --bs-gutter-x: 1rem; + } + .g-md-3, + .gy-md-3 { + --bs-gutter-y: 1rem; + } + .g-md-4, + .gx-md-4 { + --bs-gutter-x: 1.5rem; + } + .g-md-4, + .gy-md-4 { + --bs-gutter-y: 1.5rem; + } + .g-md-5, + .gx-md-5 { + --bs-gutter-x: 3rem; + } + .g-md-5, + .gy-md-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 992px) { + .col-lg { + flex: 1 0 0; + } + .row-cols-lg-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-lg-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-lg-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-lg-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-lg-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-lg-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-lg-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-lg-auto { + flex: 0 0 auto; + width: auto; + } + .col-lg-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-lg-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-lg-3 { + flex: 0 0 auto; + width: 25%; + } + .col-lg-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-lg-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-lg-6 { + flex: 0 0 auto; + width: 50%; + } + .col-lg-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-lg-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-lg-9 { + flex: 0 0 auto; + width: 75%; + } + .col-lg-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-lg-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-lg-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.33333333%; + } + .offset-lg-2 { + margin-left: 16.66666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.33333333%; + } + .offset-lg-5 { + margin-left: 41.66666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.33333333%; + } + .offset-lg-8 { + margin-left: 66.66666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.33333333%; + } + .offset-lg-11 { + margin-left: 91.66666667%; + } + .g-lg-0, + .gx-lg-0 { + --bs-gutter-x: 0; + } + .g-lg-0, + .gy-lg-0 { + --bs-gutter-y: 0; + } + .g-lg-1, + .gx-lg-1 { + --bs-gutter-x: 0.25rem; + } + .g-lg-1, + .gy-lg-1 { + --bs-gutter-y: 0.25rem; + } + .g-lg-2, + .gx-lg-2 { + --bs-gutter-x: 0.5rem; + } + .g-lg-2, + .gy-lg-2 { + --bs-gutter-y: 0.5rem; + } + .g-lg-3, + .gx-lg-3 { + --bs-gutter-x: 1rem; + } + .g-lg-3, + .gy-lg-3 { + --bs-gutter-y: 1rem; + } + .g-lg-4, + .gx-lg-4 { + --bs-gutter-x: 1.5rem; + } + .g-lg-4, + .gy-lg-4 { + --bs-gutter-y: 1.5rem; + } + .g-lg-5, + .gx-lg-5 { + --bs-gutter-x: 3rem; + } + .g-lg-5, + .gy-lg-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1200px) { + .col-xl { + flex: 1 0 0; + } + .row-cols-xl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xl-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-xl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xl-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.33333333%; + } + .offset-xl-2 { + margin-left: 16.66666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.33333333%; + } + .offset-xl-5 { + margin-left: 41.66666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.33333333%; + } + .offset-xl-8 { + margin-left: 66.66666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.33333333%; + } + .offset-xl-11 { + margin-left: 91.66666667%; + } + .g-xl-0, + .gx-xl-0 { + --bs-gutter-x: 0; + } + .g-xl-0, + .gy-xl-0 { + --bs-gutter-y: 0; + } + .g-xl-1, + .gx-xl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xl-1, + .gy-xl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xl-2, + .gx-xl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xl-2, + .gy-xl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xl-3, + .gx-xl-3 { + --bs-gutter-x: 1rem; + } + .g-xl-3, + .gy-xl-3 { + --bs-gutter-y: 1rem; + } + .g-xl-4, + .gx-xl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xl-4, + .gy-xl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xl-5, + .gx-xl-5 { + --bs-gutter-x: 3rem; + } + .g-xl-5, + .gy-xl-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1400px) { + .col-xxl { + flex: 1 0 0; + } + .row-cols-xxl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xxl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xxl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xxl-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-xxl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xxl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xxl-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xxl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xxl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xxl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xxl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xxl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xxl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xxl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xxl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xxl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xxl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xxl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xxl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xxl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xxl-0 { + margin-left: 0; + } + .offset-xxl-1 { + margin-left: 8.33333333%; + } + .offset-xxl-2 { + margin-left: 16.66666667%; + } + .offset-xxl-3 { + margin-left: 25%; + } + .offset-xxl-4 { + margin-left: 33.33333333%; + } + .offset-xxl-5 { + margin-left: 41.66666667%; + } + .offset-xxl-6 { + margin-left: 50%; + } + .offset-xxl-7 { + margin-left: 58.33333333%; + } + .offset-xxl-8 { + margin-left: 66.66666667%; + } + .offset-xxl-9 { + margin-left: 75%; + } + .offset-xxl-10 { + margin-left: 83.33333333%; + } + .offset-xxl-11 { + margin-left: 91.66666667%; + } + .g-xxl-0, + .gx-xxl-0 { + --bs-gutter-x: 0; + } + .g-xxl-0, + .gy-xxl-0 { + --bs-gutter-y: 0; + } + .g-xxl-1, + .gx-xxl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xxl-1, + .gy-xxl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xxl-2, + .gx-xxl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xxl-2, + .gy-xxl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xxl-3, + .gx-xxl-3 { + --bs-gutter-x: 1rem; + } + .g-xxl-3, + .gy-xxl-3 { + --bs-gutter-y: 1rem; + } + .g-xxl-4, + .gx-xxl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xxl-4, + .gy-xxl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xxl-5, + .gx-xxl-5 { + --bs-gutter-x: 3rem; + } + .g-xxl-5, + .gy-xxl-5 { + --bs-gutter-y: 3rem; + } +} +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-grid { + display: grid !important; +} + +.d-inline-grid { + display: inline-grid !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +.d-none { + display: none !important; +} + +.flex-fill { + flex: 1 1 auto !important; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-grow-0 { + flex-grow: 0 !important; +} + +.flex-grow-1 { + flex-grow: 1 !important; +} + +.flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + flex-shrink: 1 !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.justify-content-evenly { + justify-content: space-evenly !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +.order-first { + order: -1 !important; +} + +.order-0 { + order: 0 !important; +} + +.order-1 { + order: 1 !important; +} + +.order-2 { + order: 2 !important; +} + +.order-3 { + order: 3 !important; +} + +.order-4 { + order: 4 !important; +} + +.order-5 { + order: 5 !important; +} + +.order-last { + order: 6 !important; +} + +.m-0 { + margin: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; +} + +.mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; +} + +.mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; +} + +.mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; +} + +.mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} + +.mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; +} + +.mx-auto { + margin-right: auto !important; + margin-left: auto !important; +} + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mt-2 { + margin-top: 0.5rem !important; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.mt-4 { + margin-top: 1.5rem !important; +} + +.mt-5 { + margin-top: 3rem !important; +} + +.mt-auto { + margin-top: auto !important; +} + +.me-0 { + margin-right: 0 !important; +} + +.me-1 { + margin-right: 0.25rem !important; +} + +.me-2 { + margin-right: 0.5rem !important; +} + +.me-3 { + margin-right: 1rem !important; +} + +.me-4 { + margin-right: 1.5rem !important; +} + +.me-5 { + margin-right: 3rem !important; +} + +.me-auto { + margin-right: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 3rem !important; +} + +.mb-auto { + margin-bottom: auto !important; +} + +.ms-0 { + margin-left: 0 !important; +} + +.ms-1 { + margin-left: 0.25rem !important; +} + +.ms-2 { + margin-left: 0.5rem !important; +} + +.ms-3 { + margin-left: 1rem !important; +} + +.ms-4 { + margin-left: 1.5rem !important; +} + +.ms-5 { + margin-left: 3rem !important; +} + +.ms-auto { + margin-left: auto !important; +} + +.p-0 { + padding: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.px-0 { + padding-right: 0 !important; + padding-left: 0 !important; +} + +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; +} + +.px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; +} + +.px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; +} + +.px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; +} + +.px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; +} + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pt-0 { + padding-top: 0 !important; +} + +.pt-1 { + padding-top: 0.25rem !important; +} + +.pt-2 { + padding-top: 0.5rem !important; +} + +.pt-3 { + padding-top: 1rem !important; +} + +.pt-4 { + padding-top: 1.5rem !important; +} + +.pt-5 { + padding-top: 3rem !important; +} + +.pe-0 { + padding-right: 0 !important; +} + +.pe-1 { + padding-right: 0.25rem !important; +} + +.pe-2 { + padding-right: 0.5rem !important; +} + +.pe-3 { + padding-right: 1rem !important; +} + +.pe-4 { + padding-right: 1.5rem !important; +} + +.pe-5 { + padding-right: 3rem !important; +} + +.pb-0 { + padding-bottom: 0 !important; +} + +.pb-1 { + padding-bottom: 0.25rem !important; +} + +.pb-2 { + padding-bottom: 0.5rem !important; +} + +.pb-3 { + padding-bottom: 1rem !important; +} + +.pb-4 { + padding-bottom: 1.5rem !important; +} + +.pb-5 { + padding-bottom: 3rem !important; +} + +.ps-0 { + padding-left: 0 !important; +} + +.ps-1 { + padding-left: 0.25rem !important; +} + +.ps-2 { + padding-left: 0.5rem !important; +} + +.ps-3 { + padding-left: 1rem !important; +} + +.ps-4 { + padding-left: 1.5rem !important; +} + +.ps-5 { + padding-left: 3rem !important; +} + +@media (min-width: 576px) { + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-grid { + display: grid !important; + } + .d-sm-inline-grid { + display: inline-grid !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: flex !important; + } + .d-sm-inline-flex { + display: inline-flex !important; + } + .d-sm-none { + display: none !important; + } + .flex-sm-fill { + flex: 1 1 auto !important; + } + .flex-sm-row { + flex-direction: row !important; + } + .flex-sm-column { + flex-direction: column !important; + } + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + .flex-sm-grow-0 { + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + flex-shrink: 1 !important; + } + .flex-sm-wrap { + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-sm-start { + justify-content: flex-start !important; + } + .justify-content-sm-end { + justify-content: flex-end !important; + } + .justify-content-sm-center { + justify-content: center !important; + } + .justify-content-sm-between { + justify-content: space-between !important; + } + .justify-content-sm-around { + justify-content: space-around !important; + } + .justify-content-sm-evenly { + justify-content: space-evenly !important; + } + .align-items-sm-start { + align-items: flex-start !important; + } + .align-items-sm-end { + align-items: flex-end !important; + } + .align-items-sm-center { + align-items: center !important; + } + .align-items-sm-baseline { + align-items: baseline !important; + } + .align-items-sm-stretch { + align-items: stretch !important; + } + .align-content-sm-start { + align-content: flex-start !important; + } + .align-content-sm-end { + align-content: flex-end !important; + } + .align-content-sm-center { + align-content: center !important; + } + .align-content-sm-between { + align-content: space-between !important; + } + .align-content-sm-around { + align-content: space-around !important; + } + .align-content-sm-stretch { + align-content: stretch !important; + } + .align-self-sm-auto { + align-self: auto !important; + } + .align-self-sm-start { + align-self: flex-start !important; + } + .align-self-sm-end { + align-self: flex-end !important; + } + .align-self-sm-center { + align-self: center !important; + } + .align-self-sm-baseline { + align-self: baseline !important; + } + .align-self-sm-stretch { + align-self: stretch !important; + } + .order-sm-first { + order: -1 !important; + } + .order-sm-0 { + order: 0 !important; + } + .order-sm-1 { + order: 1 !important; + } + .order-sm-2 { + order: 2 !important; + } + .order-sm-3 { + order: 3 !important; + } + .order-sm-4 { + order: 4 !important; + } + .order-sm-5 { + order: 5 !important; + } + .order-sm-last { + order: 6 !important; + } + .m-sm-0 { + margin: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mx-sm-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-sm-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-sm-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-sm-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-sm-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-sm-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-sm-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-sm-0 { + margin-top: 0 !important; + } + .mt-sm-1 { + margin-top: 0.25rem !important; + } + .mt-sm-2 { + margin-top: 0.5rem !important; + } + .mt-sm-3 { + margin-top: 1rem !important; + } + .mt-sm-4 { + margin-top: 1.5rem !important; + } + .mt-sm-5 { + margin-top: 3rem !important; + } + .mt-sm-auto { + margin-top: auto !important; + } + .me-sm-0 { + margin-right: 0 !important; + } + .me-sm-1 { + margin-right: 0.25rem !important; + } + .me-sm-2 { + margin-right: 0.5rem !important; + } + .me-sm-3 { + margin-right: 1rem !important; + } + .me-sm-4 { + margin-right: 1.5rem !important; + } + .me-sm-5 { + margin-right: 3rem !important; + } + .me-sm-auto { + margin-right: auto !important; + } + .mb-sm-0 { + margin-bottom: 0 !important; + } + .mb-sm-1 { + margin-bottom: 0.25rem !important; + } + .mb-sm-2 { + margin-bottom: 0.5rem !important; + } + .mb-sm-3 { + margin-bottom: 1rem !important; + } + .mb-sm-4 { + margin-bottom: 1.5rem !important; + } + .mb-sm-5 { + margin-bottom: 3rem !important; + } + .mb-sm-auto { + margin-bottom: auto !important; + } + .ms-sm-0 { + margin-left: 0 !important; + } + .ms-sm-1 { + margin-left: 0.25rem !important; + } + .ms-sm-2 { + margin-left: 0.5rem !important; + } + .ms-sm-3 { + margin-left: 1rem !important; + } + .ms-sm-4 { + margin-left: 1.5rem !important; + } + .ms-sm-5 { + margin-left: 3rem !important; + } + .ms-sm-auto { + margin-left: auto !important; + } + .p-sm-0 { + padding: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .px-sm-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-sm-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-sm-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-sm-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-sm-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-sm-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-sm-0 { + padding-top: 0 !important; + } + .pt-sm-1 { + padding-top: 0.25rem !important; + } + .pt-sm-2 { + padding-top: 0.5rem !important; + } + .pt-sm-3 { + padding-top: 1rem !important; + } + .pt-sm-4 { + padding-top: 1.5rem !important; + } + .pt-sm-5 { + padding-top: 3rem !important; + } + .pe-sm-0 { + padding-right: 0 !important; + } + .pe-sm-1 { + padding-right: 0.25rem !important; + } + .pe-sm-2 { + padding-right: 0.5rem !important; + } + .pe-sm-3 { + padding-right: 1rem !important; + } + .pe-sm-4 { + padding-right: 1.5rem !important; + } + .pe-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-0 { + padding-bottom: 0 !important; + } + .pb-sm-1 { + padding-bottom: 0.25rem !important; + } + .pb-sm-2 { + padding-bottom: 0.5rem !important; + } + .pb-sm-3 { + padding-bottom: 1rem !important; + } + .pb-sm-4 { + padding-bottom: 1.5rem !important; + } + .pb-sm-5 { + padding-bottom: 3rem !important; + } + .ps-sm-0 { + padding-left: 0 !important; + } + .ps-sm-1 { + padding-left: 0.25rem !important; + } + .ps-sm-2 { + padding-left: 0.5rem !important; + } + .ps-sm-3 { + padding-left: 1rem !important; + } + .ps-sm-4 { + padding-left: 1.5rem !important; + } + .ps-sm-5 { + padding-left: 3rem !important; + } +} +@media (min-width: 768px) { + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-grid { + display: grid !important; + } + .d-md-inline-grid { + display: inline-grid !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: flex !important; + } + .d-md-inline-flex { + display: inline-flex !important; + } + .d-md-none { + display: none !important; + } + .flex-md-fill { + flex: 1 1 auto !important; + } + .flex-md-row { + flex-direction: row !important; + } + .flex-md-column { + flex-direction: column !important; + } + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + .flex-md-grow-0 { + flex-grow: 0 !important; + } + .flex-md-grow-1 { + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + flex-shrink: 1 !important; + } + .flex-md-wrap { + flex-wrap: wrap !important; + } + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-md-start { + justify-content: flex-start !important; + } + .justify-content-md-end { + justify-content: flex-end !important; + } + .justify-content-md-center { + justify-content: center !important; + } + .justify-content-md-between { + justify-content: space-between !important; + } + .justify-content-md-around { + justify-content: space-around !important; + } + .justify-content-md-evenly { + justify-content: space-evenly !important; + } + .align-items-md-start { + align-items: flex-start !important; + } + .align-items-md-end { + align-items: flex-end !important; + } + .align-items-md-center { + align-items: center !important; + } + .align-items-md-baseline { + align-items: baseline !important; + } + .align-items-md-stretch { + align-items: stretch !important; + } + .align-content-md-start { + align-content: flex-start !important; + } + .align-content-md-end { + align-content: flex-end !important; + } + .align-content-md-center { + align-content: center !important; + } + .align-content-md-between { + align-content: space-between !important; + } + .align-content-md-around { + align-content: space-around !important; + } + .align-content-md-stretch { + align-content: stretch !important; + } + .align-self-md-auto { + align-self: auto !important; + } + .align-self-md-start { + align-self: flex-start !important; + } + .align-self-md-end { + align-self: flex-end !important; + } + .align-self-md-center { + align-self: center !important; + } + .align-self-md-baseline { + align-self: baseline !important; + } + .align-self-md-stretch { + align-self: stretch !important; + } + .order-md-first { + order: -1 !important; + } + .order-md-0 { + order: 0 !important; + } + .order-md-1 { + order: 1 !important; + } + .order-md-2 { + order: 2 !important; + } + .order-md-3 { + order: 3 !important; + } + .order-md-4 { + order: 4 !important; + } + .order-md-5 { + order: 5 !important; + } + .order-md-last { + order: 6 !important; + } + .m-md-0 { + margin: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mx-md-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-md-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-md-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-md-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-md-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-md-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-md-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-md-0 { + margin-top: 0 !important; + } + .mt-md-1 { + margin-top: 0.25rem !important; + } + .mt-md-2 { + margin-top: 0.5rem !important; + } + .mt-md-3 { + margin-top: 1rem !important; + } + .mt-md-4 { + margin-top: 1.5rem !important; + } + .mt-md-5 { + margin-top: 3rem !important; + } + .mt-md-auto { + margin-top: auto !important; + } + .me-md-0 { + margin-right: 0 !important; + } + .me-md-1 { + margin-right: 0.25rem !important; + } + .me-md-2 { + margin-right: 0.5rem !important; + } + .me-md-3 { + margin-right: 1rem !important; + } + .me-md-4 { + margin-right: 1.5rem !important; + } + .me-md-5 { + margin-right: 3rem !important; + } + .me-md-auto { + margin-right: auto !important; + } + .mb-md-0 { + margin-bottom: 0 !important; + } + .mb-md-1 { + margin-bottom: 0.25rem !important; + } + .mb-md-2 { + margin-bottom: 0.5rem !important; + } + .mb-md-3 { + margin-bottom: 1rem !important; + } + .mb-md-4 { + margin-bottom: 1.5rem !important; + } + .mb-md-5 { + margin-bottom: 3rem !important; + } + .mb-md-auto { + margin-bottom: auto !important; + } + .ms-md-0 { + margin-left: 0 !important; + } + .ms-md-1 { + margin-left: 0.25rem !important; + } + .ms-md-2 { + margin-left: 0.5rem !important; + } + .ms-md-3 { + margin-left: 1rem !important; + } + .ms-md-4 { + margin-left: 1.5rem !important; + } + .ms-md-5 { + margin-left: 3rem !important; + } + .ms-md-auto { + margin-left: auto !important; + } + .p-md-0 { + padding: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .px-md-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-md-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-md-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-md-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-md-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-md-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-md-0 { + padding-top: 0 !important; + } + .pt-md-1 { + padding-top: 0.25rem !important; + } + .pt-md-2 { + padding-top: 0.5rem !important; + } + .pt-md-3 { + padding-top: 1rem !important; + } + .pt-md-4 { + padding-top: 1.5rem !important; + } + .pt-md-5 { + padding-top: 3rem !important; + } + .pe-md-0 { + padding-right: 0 !important; + } + .pe-md-1 { + padding-right: 0.25rem !important; + } + .pe-md-2 { + padding-right: 0.5rem !important; + } + .pe-md-3 { + padding-right: 1rem !important; + } + .pe-md-4 { + padding-right: 1.5rem !important; + } + .pe-md-5 { + padding-right: 3rem !important; + } + .pb-md-0 { + padding-bottom: 0 !important; + } + .pb-md-1 { + padding-bottom: 0.25rem !important; + } + .pb-md-2 { + padding-bottom: 0.5rem !important; + } + .pb-md-3 { + padding-bottom: 1rem !important; + } + .pb-md-4 { + padding-bottom: 1.5rem !important; + } + .pb-md-5 { + padding-bottom: 3rem !important; + } + .ps-md-0 { + padding-left: 0 !important; + } + .ps-md-1 { + padding-left: 0.25rem !important; + } + .ps-md-2 { + padding-left: 0.5rem !important; + } + .ps-md-3 { + padding-left: 1rem !important; + } + .ps-md-4 { + padding-left: 1.5rem !important; + } + .ps-md-5 { + padding-left: 3rem !important; + } +} +@media (min-width: 992px) { + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-grid { + display: grid !important; + } + .d-lg-inline-grid { + display: inline-grid !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: flex !important; + } + .d-lg-inline-flex { + display: inline-flex !important; + } + .d-lg-none { + display: none !important; + } + .flex-lg-fill { + flex: 1 1 auto !important; + } + .flex-lg-row { + flex-direction: row !important; + } + .flex-lg-column { + flex-direction: column !important; + } + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + .flex-lg-grow-0 { + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + flex-shrink: 1 !important; + } + .flex-lg-wrap { + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-lg-start { + justify-content: flex-start !important; + } + .justify-content-lg-end { + justify-content: flex-end !important; + } + .justify-content-lg-center { + justify-content: center !important; + } + .justify-content-lg-between { + justify-content: space-between !important; + } + .justify-content-lg-around { + justify-content: space-around !important; + } + .justify-content-lg-evenly { + justify-content: space-evenly !important; + } + .align-items-lg-start { + align-items: flex-start !important; + } + .align-items-lg-end { + align-items: flex-end !important; + } + .align-items-lg-center { + align-items: center !important; + } + .align-items-lg-baseline { + align-items: baseline !important; + } + .align-items-lg-stretch { + align-items: stretch !important; + } + .align-content-lg-start { + align-content: flex-start !important; + } + .align-content-lg-end { + align-content: flex-end !important; + } + .align-content-lg-center { + align-content: center !important; + } + .align-content-lg-between { + align-content: space-between !important; + } + .align-content-lg-around { + align-content: space-around !important; + } + .align-content-lg-stretch { + align-content: stretch !important; + } + .align-self-lg-auto { + align-self: auto !important; + } + .align-self-lg-start { + align-self: flex-start !important; + } + .align-self-lg-end { + align-self: flex-end !important; + } + .align-self-lg-center { + align-self: center !important; + } + .align-self-lg-baseline { + align-self: baseline !important; + } + .align-self-lg-stretch { + align-self: stretch !important; + } + .order-lg-first { + order: -1 !important; + } + .order-lg-0 { + order: 0 !important; + } + .order-lg-1 { + order: 1 !important; + } + .order-lg-2 { + order: 2 !important; + } + .order-lg-3 { + order: 3 !important; + } + .order-lg-4 { + order: 4 !important; + } + .order-lg-5 { + order: 5 !important; + } + .order-lg-last { + order: 6 !important; + } + .m-lg-0 { + margin: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mx-lg-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-lg-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-lg-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-lg-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-lg-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-lg-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-lg-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-lg-0 { + margin-top: 0 !important; + } + .mt-lg-1 { + margin-top: 0.25rem !important; + } + .mt-lg-2 { + margin-top: 0.5rem !important; + } + .mt-lg-3 { + margin-top: 1rem !important; + } + .mt-lg-4 { + margin-top: 1.5rem !important; + } + .mt-lg-5 { + margin-top: 3rem !important; + } + .mt-lg-auto { + margin-top: auto !important; + } + .me-lg-0 { + margin-right: 0 !important; + } + .me-lg-1 { + margin-right: 0.25rem !important; + } + .me-lg-2 { + margin-right: 0.5rem !important; + } + .me-lg-3 { + margin-right: 1rem !important; + } + .me-lg-4 { + margin-right: 1.5rem !important; + } + .me-lg-5 { + margin-right: 3rem !important; + } + .me-lg-auto { + margin-right: auto !important; + } + .mb-lg-0 { + margin-bottom: 0 !important; + } + .mb-lg-1 { + margin-bottom: 0.25rem !important; + } + .mb-lg-2 { + margin-bottom: 0.5rem !important; + } + .mb-lg-3 { + margin-bottom: 1rem !important; + } + .mb-lg-4 { + margin-bottom: 1.5rem !important; + } + .mb-lg-5 { + margin-bottom: 3rem !important; + } + .mb-lg-auto { + margin-bottom: auto !important; + } + .ms-lg-0 { + margin-left: 0 !important; + } + .ms-lg-1 { + margin-left: 0.25rem !important; + } + .ms-lg-2 { + margin-left: 0.5rem !important; + } + .ms-lg-3 { + margin-left: 1rem !important; + } + .ms-lg-4 { + margin-left: 1.5rem !important; + } + .ms-lg-5 { + margin-left: 3rem !important; + } + .ms-lg-auto { + margin-left: auto !important; + } + .p-lg-0 { + padding: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .px-lg-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-lg-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-lg-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-lg-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-lg-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-lg-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-lg-0 { + padding-top: 0 !important; + } + .pt-lg-1 { + padding-top: 0.25rem !important; + } + .pt-lg-2 { + padding-top: 0.5rem !important; + } + .pt-lg-3 { + padding-top: 1rem !important; + } + .pt-lg-4 { + padding-top: 1.5rem !important; + } + .pt-lg-5 { + padding-top: 3rem !important; + } + .pe-lg-0 { + padding-right: 0 !important; + } + .pe-lg-1 { + padding-right: 0.25rem !important; + } + .pe-lg-2 { + padding-right: 0.5rem !important; + } + .pe-lg-3 { + padding-right: 1rem !important; + } + .pe-lg-4 { + padding-right: 1.5rem !important; + } + .pe-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-0 { + padding-bottom: 0 !important; + } + .pb-lg-1 { + padding-bottom: 0.25rem !important; + } + .pb-lg-2 { + padding-bottom: 0.5rem !important; + } + .pb-lg-3 { + padding-bottom: 1rem !important; + } + .pb-lg-4 { + padding-bottom: 1.5rem !important; + } + .pb-lg-5 { + padding-bottom: 3rem !important; + } + .ps-lg-0 { + padding-left: 0 !important; + } + .ps-lg-1 { + padding-left: 0.25rem !important; + } + .ps-lg-2 { + padding-left: 0.5rem !important; + } + .ps-lg-3 { + padding-left: 1rem !important; + } + .ps-lg-4 { + padding-left: 1.5rem !important; + } + .ps-lg-5 { + padding-left: 3rem !important; + } +} +@media (min-width: 1200px) { + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-grid { + display: grid !important; + } + .d-xl-inline-grid { + display: inline-grid !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: flex !important; + } + .d-xl-inline-flex { + display: inline-flex !important; + } + .d-xl-none { + display: none !important; + } + .flex-xl-fill { + flex: 1 1 auto !important; + } + .flex-xl-row { + flex-direction: row !important; + } + .flex-xl-column { + flex-direction: column !important; + } + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xl-grow-0 { + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xl-wrap { + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xl-start { + justify-content: flex-start !important; + } + .justify-content-xl-end { + justify-content: flex-end !important; + } + .justify-content-xl-center { + justify-content: center !important; + } + .justify-content-xl-between { + justify-content: space-between !important; + } + .justify-content-xl-around { + justify-content: space-around !important; + } + .justify-content-xl-evenly { + justify-content: space-evenly !important; + } + .align-items-xl-start { + align-items: flex-start !important; + } + .align-items-xl-end { + align-items: flex-end !important; + } + .align-items-xl-center { + align-items: center !important; + } + .align-items-xl-baseline { + align-items: baseline !important; + } + .align-items-xl-stretch { + align-items: stretch !important; + } + .align-content-xl-start { + align-content: flex-start !important; + } + .align-content-xl-end { + align-content: flex-end !important; + } + .align-content-xl-center { + align-content: center !important; + } + .align-content-xl-between { + align-content: space-between !important; + } + .align-content-xl-around { + align-content: space-around !important; + } + .align-content-xl-stretch { + align-content: stretch !important; + } + .align-self-xl-auto { + align-self: auto !important; + } + .align-self-xl-start { + align-self: flex-start !important; + } + .align-self-xl-end { + align-self: flex-end !important; + } + .align-self-xl-center { + align-self: center !important; + } + .align-self-xl-baseline { + align-self: baseline !important; + } + .align-self-xl-stretch { + align-self: stretch !important; + } + .order-xl-first { + order: -1 !important; + } + .order-xl-0 { + order: 0 !important; + } + .order-xl-1 { + order: 1 !important; + } + .order-xl-2 { + order: 2 !important; + } + .order-xl-3 { + order: 3 !important; + } + .order-xl-4 { + order: 4 !important; + } + .order-xl-5 { + order: 5 !important; + } + .order-xl-last { + order: 6 !important; + } + .m-xl-0 { + margin: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mx-xl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-xl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-xl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-xl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-xl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-xl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-xl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xl-0 { + margin-top: 0 !important; + } + .mt-xl-1 { + margin-top: 0.25rem !important; + } + .mt-xl-2 { + margin-top: 0.5rem !important; + } + .mt-xl-3 { + margin-top: 1rem !important; + } + .mt-xl-4 { + margin-top: 1.5rem !important; + } + .mt-xl-5 { + margin-top: 3rem !important; + } + .mt-xl-auto { + margin-top: auto !important; + } + .me-xl-0 { + margin-right: 0 !important; + } + .me-xl-1 { + margin-right: 0.25rem !important; + } + .me-xl-2 { + margin-right: 0.5rem !important; + } + .me-xl-3 { + margin-right: 1rem !important; + } + .me-xl-4 { + margin-right: 1.5rem !important; + } + .me-xl-5 { + margin-right: 3rem !important; + } + .me-xl-auto { + margin-right: auto !important; + } + .mb-xl-0 { + margin-bottom: 0 !important; + } + .mb-xl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xl-3 { + margin-bottom: 1rem !important; + } + .mb-xl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xl-5 { + margin-bottom: 3rem !important; + } + .mb-xl-auto { + margin-bottom: auto !important; + } + .ms-xl-0 { + margin-left: 0 !important; + } + .ms-xl-1 { + margin-left: 0.25rem !important; + } + .ms-xl-2 { + margin-left: 0.5rem !important; + } + .ms-xl-3 { + margin-left: 1rem !important; + } + .ms-xl-4 { + margin-left: 1.5rem !important; + } + .ms-xl-5 { + margin-left: 3rem !important; + } + .ms-xl-auto { + margin-left: auto !important; + } + .p-xl-0 { + padding: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .px-xl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-xl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-xl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-xl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-xl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-xl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xl-0 { + padding-top: 0 !important; + } + .pt-xl-1 { + padding-top: 0.25rem !important; + } + .pt-xl-2 { + padding-top: 0.5rem !important; + } + .pt-xl-3 { + padding-top: 1rem !important; + } + .pt-xl-4 { + padding-top: 1.5rem !important; + } + .pt-xl-5 { + padding-top: 3rem !important; + } + .pe-xl-0 { + padding-right: 0 !important; + } + .pe-xl-1 { + padding-right: 0.25rem !important; + } + .pe-xl-2 { + padding-right: 0.5rem !important; + } + .pe-xl-3 { + padding-right: 1rem !important; + } + .pe-xl-4 { + padding-right: 1.5rem !important; + } + .pe-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-0 { + padding-bottom: 0 !important; + } + .pb-xl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xl-3 { + padding-bottom: 1rem !important; + } + .pb-xl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xl-5 { + padding-bottom: 3rem !important; + } + .ps-xl-0 { + padding-left: 0 !important; + } + .ps-xl-1 { + padding-left: 0.25rem !important; + } + .ps-xl-2 { + padding-left: 0.5rem !important; + } + .ps-xl-3 { + padding-left: 1rem !important; + } + .ps-xl-4 { + padding-left: 1.5rem !important; + } + .ps-xl-5 { + padding-left: 3rem !important; + } +} +@media (min-width: 1400px) { + .d-xxl-inline { + display: inline !important; + } + .d-xxl-inline-block { + display: inline-block !important; + } + .d-xxl-block { + display: block !important; + } + .d-xxl-grid { + display: grid !important; + } + .d-xxl-inline-grid { + display: inline-grid !important; + } + .d-xxl-table { + display: table !important; + } + .d-xxl-table-row { + display: table-row !important; + } + .d-xxl-table-cell { + display: table-cell !important; + } + .d-xxl-flex { + display: flex !important; + } + .d-xxl-inline-flex { + display: inline-flex !important; + } + .d-xxl-none { + display: none !important; + } + .flex-xxl-fill { + flex: 1 1 auto !important; + } + .flex-xxl-row { + flex-direction: row !important; + } + .flex-xxl-column { + flex-direction: column !important; + } + .flex-xxl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xxl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xxl-grow-0 { + flex-grow: 0 !important; + } + .flex-xxl-grow-1 { + flex-grow: 1 !important; + } + .flex-xxl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xxl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xxl-wrap { + flex-wrap: wrap !important; + } + .flex-xxl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xxl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xxl-start { + justify-content: flex-start !important; + } + .justify-content-xxl-end { + justify-content: flex-end !important; + } + .justify-content-xxl-center { + justify-content: center !important; + } + .justify-content-xxl-between { + justify-content: space-between !important; + } + .justify-content-xxl-around { + justify-content: space-around !important; + } + .justify-content-xxl-evenly { + justify-content: space-evenly !important; + } + .align-items-xxl-start { + align-items: flex-start !important; + } + .align-items-xxl-end { + align-items: flex-end !important; + } + .align-items-xxl-center { + align-items: center !important; + } + .align-items-xxl-baseline { + align-items: baseline !important; + } + .align-items-xxl-stretch { + align-items: stretch !important; + } + .align-content-xxl-start { + align-content: flex-start !important; + } + .align-content-xxl-end { + align-content: flex-end !important; + } + .align-content-xxl-center { + align-content: center !important; + } + .align-content-xxl-between { + align-content: space-between !important; + } + .align-content-xxl-around { + align-content: space-around !important; + } + .align-content-xxl-stretch { + align-content: stretch !important; + } + .align-self-xxl-auto { + align-self: auto !important; + } + .align-self-xxl-start { + align-self: flex-start !important; + } + .align-self-xxl-end { + align-self: flex-end !important; + } + .align-self-xxl-center { + align-self: center !important; + } + .align-self-xxl-baseline { + align-self: baseline !important; + } + .align-self-xxl-stretch { + align-self: stretch !important; + } + .order-xxl-first { + order: -1 !important; + } + .order-xxl-0 { + order: 0 !important; + } + .order-xxl-1 { + order: 1 !important; + } + .order-xxl-2 { + order: 2 !important; + } + .order-xxl-3 { + order: 3 !important; + } + .order-xxl-4 { + order: 4 !important; + } + .order-xxl-5 { + order: 5 !important; + } + .order-xxl-last { + order: 6 !important; + } + .m-xxl-0 { + margin: 0 !important; + } + .m-xxl-1 { + margin: 0.25rem !important; + } + .m-xxl-2 { + margin: 0.5rem !important; + } + .m-xxl-3 { + margin: 1rem !important; + } + .m-xxl-4 { + margin: 1.5rem !important; + } + .m-xxl-5 { + margin: 3rem !important; + } + .m-xxl-auto { + margin: auto !important; + } + .mx-xxl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-xxl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-xxl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-xxl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-xxl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-xxl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-xxl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-xxl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xxl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xxl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xxl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xxl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xxl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xxl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xxl-0 { + margin-top: 0 !important; + } + .mt-xxl-1 { + margin-top: 0.25rem !important; + } + .mt-xxl-2 { + margin-top: 0.5rem !important; + } + .mt-xxl-3 { + margin-top: 1rem !important; + } + .mt-xxl-4 { + margin-top: 1.5rem !important; + } + .mt-xxl-5 { + margin-top: 3rem !important; + } + .mt-xxl-auto { + margin-top: auto !important; + } + .me-xxl-0 { + margin-right: 0 !important; + } + .me-xxl-1 { + margin-right: 0.25rem !important; + } + .me-xxl-2 { + margin-right: 0.5rem !important; + } + .me-xxl-3 { + margin-right: 1rem !important; + } + .me-xxl-4 { + margin-right: 1.5rem !important; + } + .me-xxl-5 { + margin-right: 3rem !important; + } + .me-xxl-auto { + margin-right: auto !important; + } + .mb-xxl-0 { + margin-bottom: 0 !important; + } + .mb-xxl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xxl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xxl-3 { + margin-bottom: 1rem !important; + } + .mb-xxl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xxl-5 { + margin-bottom: 3rem !important; + } + .mb-xxl-auto { + margin-bottom: auto !important; + } + .ms-xxl-0 { + margin-left: 0 !important; + } + .ms-xxl-1 { + margin-left: 0.25rem !important; + } + .ms-xxl-2 { + margin-left: 0.5rem !important; + } + .ms-xxl-3 { + margin-left: 1rem !important; + } + .ms-xxl-4 { + margin-left: 1.5rem !important; + } + .ms-xxl-5 { + margin-left: 3rem !important; + } + .ms-xxl-auto { + margin-left: auto !important; + } + .p-xxl-0 { + padding: 0 !important; + } + .p-xxl-1 { + padding: 0.25rem !important; + } + .p-xxl-2 { + padding: 0.5rem !important; + } + .p-xxl-3 { + padding: 1rem !important; + } + .p-xxl-4 { + padding: 1.5rem !important; + } + .p-xxl-5 { + padding: 3rem !important; + } + .px-xxl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-xxl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-xxl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-xxl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-xxl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-xxl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-xxl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xxl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xxl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xxl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xxl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xxl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xxl-0 { + padding-top: 0 !important; + } + .pt-xxl-1 { + padding-top: 0.25rem !important; + } + .pt-xxl-2 { + padding-top: 0.5rem !important; + } + .pt-xxl-3 { + padding-top: 1rem !important; + } + .pt-xxl-4 { + padding-top: 1.5rem !important; + } + .pt-xxl-5 { + padding-top: 3rem !important; + } + .pe-xxl-0 { + padding-right: 0 !important; + } + .pe-xxl-1 { + padding-right: 0.25rem !important; + } + .pe-xxl-2 { + padding-right: 0.5rem !important; + } + .pe-xxl-3 { + padding-right: 1rem !important; + } + .pe-xxl-4 { + padding-right: 1.5rem !important; + } + .pe-xxl-5 { + padding-right: 3rem !important; + } + .pb-xxl-0 { + padding-bottom: 0 !important; + } + .pb-xxl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xxl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xxl-3 { + padding-bottom: 1rem !important; + } + .pb-xxl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xxl-5 { + padding-bottom: 3rem !important; + } + .ps-xxl-0 { + padding-left: 0 !important; + } + .ps-xxl-1 { + padding-left: 0.25rem !important; + } + .ps-xxl-2 { + padding-left: 0.5rem !important; + } + .ps-xxl-3 { + padding-left: 1rem !important; + } + .ps-xxl-4 { + padding-left: 1.5rem !important; + } + .ps-xxl-5 { + padding-left: 3rem !important; + } +} +@media print { + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-grid { + display: grid !important; + } + .d-print-inline-grid { + display: inline-grid !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: flex !important; + } + .d-print-inline-flex { + display: inline-flex !important; + } + .d-print-none { + display: none !important; + } +} + +/*# sourceMappingURL=bootstrap-grid.css.map */ \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map new file mode 100644 index 0000000..492db77 --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","bootstrap-grid.css","../../scss/mixins/_breakpoints.scss","../../scss/_variables.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"AACE;;;;EAAA;ACKA;;;;;;;ECHA,qBAAA;EACA,gBAAA;EACA,WAAA;EACA,6CAAA;EACA,4CAAA;EACA,kBAAA;EACA,iBAAA;ACUF;;AC4CI;EH5CE;IACE,gBIkee;EF9drB;AACF;ACsCI;EH5CE;IACE,gBIkee;EFzdrB;AACF;ACiCI;EH5CE;IACE,gBIkee;EFpdrB;AACF;AC4BI;EH5CE;IACE,iBIkee;EF/crB;AACF;ACuBI;EH5CE;IACE,iBIkee;EF1crB;AACF;AGzCA;EAEI,qBAAA;EAAA,yBAAA;EAAA,yBAAA;EAAA,yBAAA;EAAA,0BAAA;EAAA,2BAAA;AH+CJ;;AG1CE;ECNA,qBAAA;EACA,gBAAA;EACA,aAAA;EACA,eAAA;EAEA,yCAAA;EACA,6CAAA;EACA,4CAAA;AJmDF;AGjDI;ECGF,sBAAA;EAIA,cAAA;EACA,WAAA;EACA,eAAA;EACA,6CAAA;EACA,4CAAA;EACA,8BAAA;AJ8CF;;AICM;EACE,WAAA;AJER;;AICM;EApCJ,cAAA;EACA,WAAA;AJuCF;;AIzBE;EACE,cAAA;EACA,WAAA;AJ4BJ;;AI9BE;EACE,cAAA;EACA,UAAA;AJiCJ;;AInCE;EACE,cAAA;EACA,mBAAA;AJsCJ;;AIxCE;EACE,cAAA;EACA,UAAA;AJ2CJ;;AI7CE;EACE,cAAA;EACA,UAAA;AJgDJ;;AIlDE;EACE,cAAA;EACA,mBAAA;AJqDJ;;AItBM;EAhDJ,cAAA;EACA,WAAA;AJ0EF;;AIrBU;EAhEN,cAAA;EACA,kBAAA;AJyFJ;;AI1BU;EAhEN,cAAA;EACA,mBAAA;AJ8FJ;;AI/BU;EAhEN,cAAA;EACA,UAAA;AJmGJ;;AIpCU;EAhEN,cAAA;EACA,mBAAA;AJwGJ;;AIzCU;EAhEN,cAAA;EACA,mBAAA;AJ6GJ;;AI9CU;EAhEN,cAAA;EACA,UAAA;AJkHJ;;AInDU;EAhEN,cAAA;EACA,mBAAA;AJuHJ;;AIxDU;EAhEN,cAAA;EACA,mBAAA;AJ4HJ;;AI7DU;EAhEN,cAAA;EACA,UAAA;AJiIJ;;AIlEU;EAhEN,cAAA;EACA,mBAAA;AJsIJ;;AIvEU;EAhEN,cAAA;EACA,mBAAA;AJ2IJ;;AI5EU;EAhEN,cAAA;EACA,WAAA;AJgJJ;;AIzEY;EAxDV,wBAAA;AJqIF;;AI7EY;EAxDV,yBAAA;AJyIF;;AIjFY;EAxDV,gBAAA;AJ6IF;;AIrFY;EAxDV,yBAAA;AJiJF;;AIzFY;EAxDV,yBAAA;AJqJF;;AI7FY;EAxDV,gBAAA;AJyJF;;AIjGY;EAxDV,yBAAA;AJ6JF;;AIrGY;EAxDV,yBAAA;AJiKF;;AIzGY;EAxDV,gBAAA;AJqKF;;AI7GY;EAxDV,yBAAA;AJyKF;;AIjHY;EAxDV,yBAAA;AJ6KF;;AI1GQ;;EAEE,gBAAA;AJ6GV;;AI1GQ;;EAEE,gBAAA;AJ6GV;;AIpHQ;;EAEE,sBAAA;AJuHV;;AIpHQ;;EAEE,sBAAA;AJuHV;;AI9HQ;;EAEE,qBAAA;AJiIV;;AI9HQ;;EAEE,qBAAA;AJiIV;;AIxIQ;;EAEE,mBAAA;AJ2IV;;AIxIQ;;EAEE,mBAAA;AJ2IV;;AIlJQ;;EAEE,qBAAA;AJqJV;;AIlJQ;;EAEE,qBAAA;AJqJV;;AI5JQ;;EAEE,mBAAA;AJ+JV;;AI5JQ;;EAEE,mBAAA;AJ+JV;;ACzNI;EGUE;IACE,WAAA;EJmNN;EIhNI;IApCJ,cAAA;IACA,WAAA;EJuPA;EIzOA;IACE,cAAA;IACA,WAAA;EJ2OF;EI7OA;IACE,cAAA;IACA,UAAA;EJ+OF;EIjPA;IACE,cAAA;IACA,mBAAA;EJmPF;EIrPA;IACE,cAAA;IACA,UAAA;EJuPF;EIzPA;IACE,cAAA;IACA,UAAA;EJ2PF;EI7PA;IACE,cAAA;IACA,mBAAA;EJ+PF;EIhOI;IAhDJ,cAAA;IACA,WAAA;EJmRA;EI9NQ;IAhEN,cAAA;IACA,kBAAA;EJiSF;EIlOQ;IAhEN,cAAA;IACA,mBAAA;EJqSF;EItOQ;IAhEN,cAAA;IACA,UAAA;EJySF;EI1OQ;IAhEN,cAAA;IACA,mBAAA;EJ6SF;EI9OQ;IAhEN,cAAA;IACA,mBAAA;EJiTF;EIlPQ;IAhEN,cAAA;IACA,UAAA;EJqTF;EItPQ;IAhEN,cAAA;IACA,mBAAA;EJyTF;EI1PQ;IAhEN,cAAA;IACA,mBAAA;EJ6TF;EI9PQ;IAhEN,cAAA;IACA,UAAA;EJiUF;EIlQQ;IAhEN,cAAA;IACA,mBAAA;EJqUF;EItQQ;IAhEN,cAAA;IACA,mBAAA;EJyUF;EI1QQ;IAhEN,cAAA;IACA,WAAA;EJ6UF;EItQU;IAxDV,cAAA;EJiUA;EIzQU;IAxDV,wBAAA;EJoUA;EI5QU;IAxDV,yBAAA;EJuUA;EI/QU;IAxDV,gBAAA;EJ0UA;EIlRU;IAxDV,yBAAA;EJ6UA;EIrRU;IAxDV,yBAAA;EJgVA;EIxRU;IAxDV,gBAAA;EJmVA;EI3RU;IAxDV,yBAAA;EJsVA;EI9RU;IAxDV,yBAAA;EJyVA;EIjSU;IAxDV,gBAAA;EJ4VA;EIpSU;IAxDV,yBAAA;EJ+VA;EIvSU;IAxDV,yBAAA;EJkWA;EI/RM;;IAEE,gBAAA;EJiSR;EI9RM;;IAEE,gBAAA;EJgSR;EIvSM;;IAEE,sBAAA;EJySR;EItSM;;IAEE,sBAAA;EJwSR;EI/SM;;IAEE,qBAAA;EJiTR;EI9SM;;IAEE,qBAAA;EJgTR;EIvTM;;IAEE,mBAAA;EJyTR;EItTM;;IAEE,mBAAA;EJwTR;EI/TM;;IAEE,qBAAA;EJiUR;EI9TM;;IAEE,qBAAA;EJgUR;EIvUM;;IAEE,mBAAA;EJyUR;EItUM;;IAEE,mBAAA;EJwUR;AACF;ACnYI;EGUE;IACE,WAAA;EJ4XN;EIzXI;IApCJ,cAAA;IACA,WAAA;EJgaA;EIlZA;IACE,cAAA;IACA,WAAA;EJoZF;EItZA;IACE,cAAA;IACA,UAAA;EJwZF;EI1ZA;IACE,cAAA;IACA,mBAAA;EJ4ZF;EI9ZA;IACE,cAAA;IACA,UAAA;EJgaF;EIlaA;IACE,cAAA;IACA,UAAA;EJoaF;EItaA;IACE,cAAA;IACA,mBAAA;EJwaF;EIzYI;IAhDJ,cAAA;IACA,WAAA;EJ4bA;EIvYQ;IAhEN,cAAA;IACA,kBAAA;EJ0cF;EI3YQ;IAhEN,cAAA;IACA,mBAAA;EJ8cF;EI/YQ;IAhEN,cAAA;IACA,UAAA;EJkdF;EInZQ;IAhEN,cAAA;IACA,mBAAA;EJsdF;EIvZQ;IAhEN,cAAA;IACA,mBAAA;EJ0dF;EI3ZQ;IAhEN,cAAA;IACA,UAAA;EJ8dF;EI/ZQ;IAhEN,cAAA;IACA,mBAAA;EJkeF;EInaQ;IAhEN,cAAA;IACA,mBAAA;EJseF;EIvaQ;IAhEN,cAAA;IACA,UAAA;EJ0eF;EI3aQ;IAhEN,cAAA;IACA,mBAAA;EJ8eF;EI/aQ;IAhEN,cAAA;IACA,mBAAA;EJkfF;EInbQ;IAhEN,cAAA;IACA,WAAA;EJsfF;EI/aU;IAxDV,cAAA;EJ0eA;EIlbU;IAxDV,wBAAA;EJ6eA;EIrbU;IAxDV,yBAAA;EJgfA;EIxbU;IAxDV,gBAAA;EJmfA;EI3bU;IAxDV,yBAAA;EJsfA;EI9bU;IAxDV,yBAAA;EJyfA;EIjcU;IAxDV,gBAAA;EJ4fA;EIpcU;IAxDV,yBAAA;EJ+fA;EIvcU;IAxDV,yBAAA;EJkgBA;EI1cU;IAxDV,gBAAA;EJqgBA;EI7cU;IAxDV,yBAAA;EJwgBA;EIhdU;IAxDV,yBAAA;EJ2gBA;EIxcM;;IAEE,gBAAA;EJ0cR;EIvcM;;IAEE,gBAAA;EJycR;EIhdM;;IAEE,sBAAA;EJkdR;EI/cM;;IAEE,sBAAA;EJidR;EIxdM;;IAEE,qBAAA;EJ0dR;EIvdM;;IAEE,qBAAA;EJydR;EIheM;;IAEE,mBAAA;EJkeR;EI/dM;;IAEE,mBAAA;EJieR;EIxeM;;IAEE,qBAAA;EJ0eR;EIveM;;IAEE,qBAAA;EJyeR;EIhfM;;IAEE,mBAAA;EJkfR;EI/eM;;IAEE,mBAAA;EJifR;AACF;AC5iBI;EGUE;IACE,WAAA;EJqiBN;EIliBI;IApCJ,cAAA;IACA,WAAA;EJykBA;EI3jBA;IACE,cAAA;IACA,WAAA;EJ6jBF;EI/jBA;IACE,cAAA;IACA,UAAA;EJikBF;EInkBA;IACE,cAAA;IACA,mBAAA;EJqkBF;EIvkBA;IACE,cAAA;IACA,UAAA;EJykBF;EI3kBA;IACE,cAAA;IACA,UAAA;EJ6kBF;EI/kBA;IACE,cAAA;IACA,mBAAA;EJilBF;EIljBI;IAhDJ,cAAA;IACA,WAAA;EJqmBA;EIhjBQ;IAhEN,cAAA;IACA,kBAAA;EJmnBF;EIpjBQ;IAhEN,cAAA;IACA,mBAAA;EJunBF;EIxjBQ;IAhEN,cAAA;IACA,UAAA;EJ2nBF;EI5jBQ;IAhEN,cAAA;IACA,mBAAA;EJ+nBF;EIhkBQ;IAhEN,cAAA;IACA,mBAAA;EJmoBF;EIpkBQ;IAhEN,cAAA;IACA,UAAA;EJuoBF;EIxkBQ;IAhEN,cAAA;IACA,mBAAA;EJ2oBF;EI5kBQ;IAhEN,cAAA;IACA,mBAAA;EJ+oBF;EIhlBQ;IAhEN,cAAA;IACA,UAAA;EJmpBF;EIplBQ;IAhEN,cAAA;IACA,mBAAA;EJupBF;EIxlBQ;IAhEN,cAAA;IACA,mBAAA;EJ2pBF;EI5lBQ;IAhEN,cAAA;IACA,WAAA;EJ+pBF;EIxlBU;IAxDV,cAAA;EJmpBA;EI3lBU;IAxDV,wBAAA;EJspBA;EI9lBU;IAxDV,yBAAA;EJypBA;EIjmBU;IAxDV,gBAAA;EJ4pBA;EIpmBU;IAxDV,yBAAA;EJ+pBA;EIvmBU;IAxDV,yBAAA;EJkqBA;EI1mBU;IAxDV,gBAAA;EJqqBA;EI7mBU;IAxDV,yBAAA;EJwqBA;EIhnBU;IAxDV,yBAAA;EJ2qBA;EInnBU;IAxDV,gBAAA;EJ8qBA;EItnBU;IAxDV,yBAAA;EJirBA;EIznBU;IAxDV,yBAAA;EJorBA;EIjnBM;;IAEE,gBAAA;EJmnBR;EIhnBM;;IAEE,gBAAA;EJknBR;EIznBM;;IAEE,sBAAA;EJ2nBR;EIxnBM;;IAEE,sBAAA;EJ0nBR;EIjoBM;;IAEE,qBAAA;EJmoBR;EIhoBM;;IAEE,qBAAA;EJkoBR;EIzoBM;;IAEE,mBAAA;EJ2oBR;EIxoBM;;IAEE,mBAAA;EJ0oBR;EIjpBM;;IAEE,qBAAA;EJmpBR;EIhpBM;;IAEE,qBAAA;EJkpBR;EIzpBM;;IAEE,mBAAA;EJ2pBR;EIxpBM;;IAEE,mBAAA;EJ0pBR;AACF;ACrtBI;EGUE;IACE,WAAA;EJ8sBN;EI3sBI;IApCJ,cAAA;IACA,WAAA;EJkvBA;EIpuBA;IACE,cAAA;IACA,WAAA;EJsuBF;EIxuBA;IACE,cAAA;IACA,UAAA;EJ0uBF;EI5uBA;IACE,cAAA;IACA,mBAAA;EJ8uBF;EIhvBA;IACE,cAAA;IACA,UAAA;EJkvBF;EIpvBA;IACE,cAAA;IACA,UAAA;EJsvBF;EIxvBA;IACE,cAAA;IACA,mBAAA;EJ0vBF;EI3tBI;IAhDJ,cAAA;IACA,WAAA;EJ8wBA;EIztBQ;IAhEN,cAAA;IACA,kBAAA;EJ4xBF;EI7tBQ;IAhEN,cAAA;IACA,mBAAA;EJgyBF;EIjuBQ;IAhEN,cAAA;IACA,UAAA;EJoyBF;EIruBQ;IAhEN,cAAA;IACA,mBAAA;EJwyBF;EIzuBQ;IAhEN,cAAA;IACA,mBAAA;EJ4yBF;EI7uBQ;IAhEN,cAAA;IACA,UAAA;EJgzBF;EIjvBQ;IAhEN,cAAA;IACA,mBAAA;EJozBF;EIrvBQ;IAhEN,cAAA;IACA,mBAAA;EJwzBF;EIzvBQ;IAhEN,cAAA;IACA,UAAA;EJ4zBF;EI7vBQ;IAhEN,cAAA;IACA,mBAAA;EJg0BF;EIjwBQ;IAhEN,cAAA;IACA,mBAAA;EJo0BF;EIrwBQ;IAhEN,cAAA;IACA,WAAA;EJw0BF;EIjwBU;IAxDV,cAAA;EJ4zBA;EIpwBU;IAxDV,wBAAA;EJ+zBA;EIvwBU;IAxDV,yBAAA;EJk0BA;EI1wBU;IAxDV,gBAAA;EJq0BA;EI7wBU;IAxDV,yBAAA;EJw0BA;EIhxBU;IAxDV,yBAAA;EJ20BA;EInxBU;IAxDV,gBAAA;EJ80BA;EItxBU;IAxDV,yBAAA;EJi1BA;EIzxBU;IAxDV,yBAAA;EJo1BA;EI5xBU;IAxDV,gBAAA;EJu1BA;EI/xBU;IAxDV,yBAAA;EJ01BA;EIlyBU;IAxDV,yBAAA;EJ61BA;EI1xBM;;IAEE,gBAAA;EJ4xBR;EIzxBM;;IAEE,gBAAA;EJ2xBR;EIlyBM;;IAEE,sBAAA;EJoyBR;EIjyBM;;IAEE,sBAAA;EJmyBR;EI1yBM;;IAEE,qBAAA;EJ4yBR;EIzyBM;;IAEE,qBAAA;EJ2yBR;EIlzBM;;IAEE,mBAAA;EJozBR;EIjzBM;;IAEE,mBAAA;EJmzBR;EI1zBM;;IAEE,qBAAA;EJ4zBR;EIzzBM;;IAEE,qBAAA;EJ2zBR;EIl0BM;;IAEE,mBAAA;EJo0BR;EIj0BM;;IAEE,mBAAA;EJm0BR;AACF;AC93BI;EGUE;IACE,WAAA;EJu3BN;EIp3BI;IApCJ,cAAA;IACA,WAAA;EJ25BA;EI74BA;IACE,cAAA;IACA,WAAA;EJ+4BF;EIj5BA;IACE,cAAA;IACA,UAAA;EJm5BF;EIr5BA;IACE,cAAA;IACA,mBAAA;EJu5BF;EIz5BA;IACE,cAAA;IACA,UAAA;EJ25BF;EI75BA;IACE,cAAA;IACA,UAAA;EJ+5BF;EIj6BA;IACE,cAAA;IACA,mBAAA;EJm6BF;EIp4BI;IAhDJ,cAAA;IACA,WAAA;EJu7BA;EIl4BQ;IAhEN,cAAA;IACA,kBAAA;EJq8BF;EIt4BQ;IAhEN,cAAA;IACA,mBAAA;EJy8BF;EI14BQ;IAhEN,cAAA;IACA,UAAA;EJ68BF;EI94BQ;IAhEN,cAAA;IACA,mBAAA;EJi9BF;EIl5BQ;IAhEN,cAAA;IACA,mBAAA;EJq9BF;EIt5BQ;IAhEN,cAAA;IACA,UAAA;EJy9BF;EI15BQ;IAhEN,cAAA;IACA,mBAAA;EJ69BF;EI95BQ;IAhEN,cAAA;IACA,mBAAA;EJi+BF;EIl6BQ;IAhEN,cAAA;IACA,UAAA;EJq+BF;EIt6BQ;IAhEN,cAAA;IACA,mBAAA;EJy+BF;EI16BQ;IAhEN,cAAA;IACA,mBAAA;EJ6+BF;EI96BQ;IAhEN,cAAA;IACA,WAAA;EJi/BF;EI16BU;IAxDV,cAAA;EJq+BA;EI76BU;IAxDV,wBAAA;EJw+BA;EIh7BU;IAxDV,yBAAA;EJ2+BA;EIn7BU;IAxDV,gBAAA;EJ8+BA;EIt7BU;IAxDV,yBAAA;EJi/BA;EIz7BU;IAxDV,yBAAA;EJo/BA;EI57BU;IAxDV,gBAAA;EJu/BA;EI/7BU;IAxDV,yBAAA;EJ0/BA;EIl8BU;IAxDV,yBAAA;EJ6/BA;EIr8BU;IAxDV,gBAAA;EJggCA;EIx8BU;IAxDV,yBAAA;EJmgCA;EI38BU;IAxDV,yBAAA;EJsgCA;EIn8BM;;IAEE,gBAAA;EJq8BR;EIl8BM;;IAEE,gBAAA;EJo8BR;EI38BM;;IAEE,sBAAA;EJ68BR;EI18BM;;IAEE,sBAAA;EJ48BR;EIn9BM;;IAEE,qBAAA;EJq9BR;EIl9BM;;IAEE,qBAAA;EJo9BR;EI39BM;;IAEE,mBAAA;EJ69BR;EI19BM;;IAEE,mBAAA;EJ49BR;EIn+BM;;IAEE,qBAAA;EJq+BR;EIl+BM;;IAEE,qBAAA;EJo+BR;EI3+BM;;IAEE,mBAAA;EJ6+BR;EI1+BM;;IAEE,mBAAA;EJ4+BR;AACF;AKpiCQ;EAOI,0BAAA;ALgiCZ;;AKviCQ;EAOI,gCAAA;ALoiCZ;;AK3iCQ;EAOI,yBAAA;ALwiCZ;;AK/iCQ;EAOI,wBAAA;AL4iCZ;;AKnjCQ;EAOI,+BAAA;ALgjCZ;;AKvjCQ;EAOI,yBAAA;ALojCZ;;AK3jCQ;EAOI,6BAAA;ALwjCZ;;AK/jCQ;EAOI,8BAAA;AL4jCZ;;AKnkCQ;EAOI,wBAAA;ALgkCZ;;AKvkCQ;EAOI,+BAAA;ALokCZ;;AK3kCQ;EAOI,wBAAA;ALwkCZ;;AK/kCQ;EAOI,yBAAA;AL4kCZ;;AKnlCQ;EAOI,8BAAA;ALglCZ;;AKvlCQ;EAOI,iCAAA;ALolCZ;;AK3lCQ;EAOI,sCAAA;ALwlCZ;;AK/lCQ;EAOI,yCAAA;AL4lCZ;;AKnmCQ;EAOI,uBAAA;ALgmCZ;;AKvmCQ;EAOI,uBAAA;ALomCZ;;AK3mCQ;EAOI,yBAAA;ALwmCZ;;AK/mCQ;EAOI,yBAAA;AL4mCZ;;AKnnCQ;EAOI,0BAAA;ALgnCZ;;AKvnCQ;EAOI,4BAAA;ALonCZ;;AK3nCQ;EAOI,kCAAA;ALwnCZ;;AK/nCQ;EAOI,sCAAA;AL4nCZ;;AKnoCQ;EAOI,oCAAA;ALgoCZ;;AKvoCQ;EAOI,kCAAA;ALooCZ;;AK3oCQ;EAOI,yCAAA;ALwoCZ;;AK/oCQ;EAOI,wCAAA;AL4oCZ;;AKnpCQ;EAOI,wCAAA;ALgpCZ;;AKvpCQ;EAOI,kCAAA;ALopCZ;;AK3pCQ;EAOI,gCAAA;ALwpCZ;;AK/pCQ;EAOI,8BAAA;AL4pCZ;;AKnqCQ;EAOI,gCAAA;ALgqCZ;;AKvqCQ;EAOI,+BAAA;ALoqCZ;;AK3qCQ;EAOI,oCAAA;ALwqCZ;;AK/qCQ;EAOI,kCAAA;AL4qCZ;;AKnrCQ;EAOI,gCAAA;ALgrCZ;;AKvrCQ;EAOI,uCAAA;ALorCZ;;AK3rCQ;EAOI,sCAAA;ALwrCZ;;AK/rCQ;EAOI,iCAAA;AL4rCZ;;AKnsCQ;EAOI,2BAAA;ALgsCZ;;AKvsCQ;EAOI,iCAAA;ALosCZ;;AK3sCQ;EAOI,+BAAA;ALwsCZ;;AK/sCQ;EAOI,6BAAA;AL4sCZ;;AKntCQ;EAOI,+BAAA;ALgtCZ;;AKvtCQ;EAOI,8BAAA;ALotCZ;;AK3tCQ;EAOI,oBAAA;ALwtCZ;;AK/tCQ;EAOI,mBAAA;AL4tCZ;;AKnuCQ;EAOI,mBAAA;ALguCZ;;AKvuCQ;EAOI,mBAAA;ALouCZ;;AK3uCQ;EAOI,mBAAA;ALwuCZ;;AK/uCQ;EAOI,mBAAA;AL4uCZ;;AKnvCQ;EAOI,mBAAA;ALgvCZ;;AKvvCQ;EAOI,mBAAA;ALovCZ;;AK3vCQ;EAOI,oBAAA;ALwvCZ;;AK/vCQ;EAOI,0BAAA;AL4vCZ;;AKnwCQ;EAOI,yBAAA;ALgwCZ;;AKvwCQ;EAOI,uBAAA;ALowCZ;;AK3wCQ;EAOI,yBAAA;ALwwCZ;;AK/wCQ;EAOI,uBAAA;AL4wCZ;;AKnxCQ;EAOI,uBAAA;ALgxCZ;;AKvxCQ;EAOI,0BAAA;EAAA,yBAAA;ALqxCZ;;AK5xCQ;EAOI,gCAAA;EAAA,+BAAA;AL0xCZ;;AKjyCQ;EAOI,+BAAA;EAAA,8BAAA;AL+xCZ;;AKtyCQ;EAOI,6BAAA;EAAA,4BAAA;ALoyCZ;;AK3yCQ;EAOI,+BAAA;EAAA,8BAAA;ALyyCZ;;AKhzCQ;EAOI,6BAAA;EAAA,4BAAA;AL8yCZ;;AKrzCQ;EAOI,6BAAA;EAAA,4BAAA;ALmzCZ;;AK1zCQ;EAOI,wBAAA;EAAA,2BAAA;ALwzCZ;;AK/zCQ;EAOI,8BAAA;EAAA,iCAAA;AL6zCZ;;AKp0CQ;EAOI,6BAAA;EAAA,gCAAA;ALk0CZ;;AKz0CQ;EAOI,2BAAA;EAAA,8BAAA;ALu0CZ;;AK90CQ;EAOI,6BAAA;EAAA,gCAAA;AL40CZ;;AKn1CQ;EAOI,2BAAA;EAAA,8BAAA;ALi1CZ;;AKx1CQ;EAOI,2BAAA;EAAA,8BAAA;ALs1CZ;;AK71CQ;EAOI,wBAAA;AL01CZ;;AKj2CQ;EAOI,8BAAA;AL81CZ;;AKr2CQ;EAOI,6BAAA;ALk2CZ;;AKz2CQ;EAOI,2BAAA;ALs2CZ;;AK72CQ;EAOI,6BAAA;AL02CZ;;AKj3CQ;EAOI,2BAAA;AL82CZ;;AKr3CQ;EAOI,2BAAA;ALk3CZ;;AKz3CQ;EAOI,0BAAA;ALs3CZ;;AK73CQ;EAOI,gCAAA;AL03CZ;;AKj4CQ;EAOI,+BAAA;AL83CZ;;AKr4CQ;EAOI,6BAAA;ALk4CZ;;AKz4CQ;EAOI,+BAAA;ALs4CZ;;AK74CQ;EAOI,6BAAA;AL04CZ;;AKj5CQ;EAOI,6BAAA;AL84CZ;;AKr5CQ;EAOI,2BAAA;ALk5CZ;;AKz5CQ;EAOI,iCAAA;ALs5CZ;;AK75CQ;EAOI,gCAAA;AL05CZ;;AKj6CQ;EAOI,8BAAA;AL85CZ;;AKr6CQ;EAOI,gCAAA;ALk6CZ;;AKz6CQ;EAOI,8BAAA;ALs6CZ;;AK76CQ;EAOI,8BAAA;AL06CZ;;AKj7CQ;EAOI,yBAAA;AL86CZ;;AKr7CQ;EAOI,+BAAA;ALk7CZ;;AKz7CQ;EAOI,8BAAA;ALs7CZ;;AK77CQ;EAOI,4BAAA;AL07CZ;;AKj8CQ;EAOI,8BAAA;AL87CZ;;AKr8CQ;EAOI,4BAAA;ALk8CZ;;AKz8CQ;EAOI,4BAAA;ALs8CZ;;AK78CQ;EAOI,qBAAA;AL08CZ;;AKj9CQ;EAOI,2BAAA;AL88CZ;;AKr9CQ;EAOI,0BAAA;ALk9CZ;;AKz9CQ;EAOI,wBAAA;ALs9CZ;;AK79CQ;EAOI,0BAAA;AL09CZ;;AKj+CQ;EAOI,wBAAA;AL89CZ;;AKr+CQ;EAOI,2BAAA;EAAA,0BAAA;ALm+CZ;;AK1+CQ;EAOI,iCAAA;EAAA,gCAAA;ALw+CZ;;AK/+CQ;EAOI,gCAAA;EAAA,+BAAA;AL6+CZ;;AKp/CQ;EAOI,8BAAA;EAAA,6BAAA;ALk/CZ;;AKz/CQ;EAOI,gCAAA;EAAA,+BAAA;ALu/CZ;;AK9/CQ;EAOI,8BAAA;EAAA,6BAAA;AL4/CZ;;AKngDQ;EAOI,yBAAA;EAAA,4BAAA;ALigDZ;;AKxgDQ;EAOI,+BAAA;EAAA,kCAAA;ALsgDZ;;AK7gDQ;EAOI,8BAAA;EAAA,iCAAA;AL2gDZ;;AKlhDQ;EAOI,4BAAA;EAAA,+BAAA;ALghDZ;;AKvhDQ;EAOI,8BAAA;EAAA,iCAAA;ALqhDZ;;AK5hDQ;EAOI,4BAAA;EAAA,+BAAA;AL0hDZ;;AKjiDQ;EAOI,yBAAA;AL8hDZ;;AKriDQ;EAOI,+BAAA;ALkiDZ;;AKziDQ;EAOI,8BAAA;ALsiDZ;;AK7iDQ;EAOI,4BAAA;AL0iDZ;;AKjjDQ;EAOI,8BAAA;AL8iDZ;;AKrjDQ;EAOI,4BAAA;ALkjDZ;;AKzjDQ;EAOI,2BAAA;ALsjDZ;;AK7jDQ;EAOI,iCAAA;AL0jDZ;;AKjkDQ;EAOI,gCAAA;AL8jDZ;;AKrkDQ;EAOI,8BAAA;ALkkDZ;;AKzkDQ;EAOI,gCAAA;ALskDZ;;AK7kDQ;EAOI,8BAAA;AL0kDZ;;AKjlDQ;EAOI,4BAAA;AL8kDZ;;AKrlDQ;EAOI,kCAAA;ALklDZ;;AKzlDQ;EAOI,iCAAA;ALslDZ;;AK7lDQ;EAOI,+BAAA;AL0lDZ;;AKjmDQ;EAOI,iCAAA;AL8lDZ;;AKrmDQ;EAOI,+BAAA;ALkmDZ;;AKzmDQ;EAOI,0BAAA;ALsmDZ;;AK7mDQ;EAOI,gCAAA;AL0mDZ;;AKjnDQ;EAOI,+BAAA;AL8mDZ;;AKrnDQ;EAOI,6BAAA;ALknDZ;;AKznDQ;EAOI,+BAAA;ALsnDZ;;AK7nDQ;EAOI,6BAAA;AL0nDZ;;ACpoDI;EIGI;IAOI,0BAAA;EL+nDV;EKtoDM;IAOI,gCAAA;ELkoDV;EKzoDM;IAOI,yBAAA;ELqoDV;EK5oDM;IAOI,wBAAA;ELwoDV;EK/oDM;IAOI,+BAAA;EL2oDV;EKlpDM;IAOI,yBAAA;EL8oDV;EKrpDM;IAOI,6BAAA;ELipDV;EKxpDM;IAOI,8BAAA;ELopDV;EK3pDM;IAOI,wBAAA;ELupDV;EK9pDM;IAOI,+BAAA;EL0pDV;EKjqDM;IAOI,wBAAA;EL6pDV;EKpqDM;IAOI,yBAAA;ELgqDV;EKvqDM;IAOI,8BAAA;ELmqDV;EK1qDM;IAOI,iCAAA;ELsqDV;EK7qDM;IAOI,sCAAA;ELyqDV;EKhrDM;IAOI,yCAAA;EL4qDV;EKnrDM;IAOI,uBAAA;EL+qDV;EKtrDM;IAOI,uBAAA;ELkrDV;EKzrDM;IAOI,yBAAA;ELqrDV;EK5rDM;IAOI,yBAAA;ELwrDV;EK/rDM;IAOI,0BAAA;EL2rDV;EKlsDM;IAOI,4BAAA;EL8rDV;EKrsDM;IAOI,kCAAA;ELisDV;EKxsDM;IAOI,sCAAA;ELosDV;EK3sDM;IAOI,oCAAA;ELusDV;EK9sDM;IAOI,kCAAA;EL0sDV;EKjtDM;IAOI,yCAAA;EL6sDV;EKptDM;IAOI,wCAAA;ELgtDV;EKvtDM;IAOI,wCAAA;ELmtDV;EK1tDM;IAOI,kCAAA;ELstDV;EK7tDM;IAOI,gCAAA;ELytDV;EKhuDM;IAOI,8BAAA;EL4tDV;EKnuDM;IAOI,gCAAA;EL+tDV;EKtuDM;IAOI,+BAAA;ELkuDV;EKzuDM;IAOI,oCAAA;ELquDV;EK5uDM;IAOI,kCAAA;ELwuDV;EK/uDM;IAOI,gCAAA;EL2uDV;EKlvDM;IAOI,uCAAA;EL8uDV;EKrvDM;IAOI,sCAAA;ELivDV;EKxvDM;IAOI,iCAAA;ELovDV;EK3vDM;IAOI,2BAAA;ELuvDV;EK9vDM;IAOI,iCAAA;EL0vDV;EKjwDM;IAOI,+BAAA;EL6vDV;EKpwDM;IAOI,6BAAA;ELgwDV;EKvwDM;IAOI,+BAAA;ELmwDV;EK1wDM;IAOI,8BAAA;ELswDV;EK7wDM;IAOI,oBAAA;ELywDV;EKhxDM;IAOI,mBAAA;EL4wDV;EKnxDM;IAOI,mBAAA;EL+wDV;EKtxDM;IAOI,mBAAA;ELkxDV;EKzxDM;IAOI,mBAAA;ELqxDV;EK5xDM;IAOI,mBAAA;ELwxDV;EK/xDM;IAOI,mBAAA;EL2xDV;EKlyDM;IAOI,mBAAA;EL8xDV;EKryDM;IAOI,oBAAA;ELiyDV;EKxyDM;IAOI,0BAAA;ELoyDV;EK3yDM;IAOI,yBAAA;ELuyDV;EK9yDM;IAOI,uBAAA;EL0yDV;EKjzDM;IAOI,yBAAA;EL6yDV;EKpzDM;IAOI,uBAAA;ELgzDV;EKvzDM;IAOI,uBAAA;ELmzDV;EK1zDM;IAOI,0BAAA;IAAA,yBAAA;ELuzDV;EK9zDM;IAOI,gCAAA;IAAA,+BAAA;EL2zDV;EKl0DM;IAOI,+BAAA;IAAA,8BAAA;EL+zDV;EKt0DM;IAOI,6BAAA;IAAA,4BAAA;ELm0DV;EK10DM;IAOI,+BAAA;IAAA,8BAAA;ELu0DV;EK90DM;IAOI,6BAAA;IAAA,4BAAA;EL20DV;EKl1DM;IAOI,6BAAA;IAAA,4BAAA;EL+0DV;EKt1DM;IAOI,wBAAA;IAAA,2BAAA;ELm1DV;EK11DM;IAOI,8BAAA;IAAA,iCAAA;ELu1DV;EK91DM;IAOI,6BAAA;IAAA,gCAAA;EL21DV;EKl2DM;IAOI,2BAAA;IAAA,8BAAA;EL+1DV;EKt2DM;IAOI,6BAAA;IAAA,gCAAA;ELm2DV;EK12DM;IAOI,2BAAA;IAAA,8BAAA;ELu2DV;EK92DM;IAOI,2BAAA;IAAA,8BAAA;EL22DV;EKl3DM;IAOI,wBAAA;EL82DV;EKr3DM;IAOI,8BAAA;ELi3DV;EKx3DM;IAOI,6BAAA;ELo3DV;EK33DM;IAOI,2BAAA;ELu3DV;EK93DM;IAOI,6BAAA;EL03DV;EKj4DM;IAOI,2BAAA;EL63DV;EKp4DM;IAOI,2BAAA;ELg4DV;EKv4DM;IAOI,0BAAA;ELm4DV;EK14DM;IAOI,gCAAA;ELs4DV;EK74DM;IAOI,+BAAA;ELy4DV;EKh5DM;IAOI,6BAAA;EL44DV;EKn5DM;IAOI,+BAAA;EL+4DV;EKt5DM;IAOI,6BAAA;ELk5DV;EKz5DM;IAOI,6BAAA;ELq5DV;EK55DM;IAOI,2BAAA;ELw5DV;EK/5DM;IAOI,iCAAA;EL25DV;EKl6DM;IAOI,gCAAA;EL85DV;EKr6DM;IAOI,8BAAA;ELi6DV;EKx6DM;IAOI,gCAAA;ELo6DV;EK36DM;IAOI,8BAAA;ELu6DV;EK96DM;IAOI,8BAAA;EL06DV;EKj7DM;IAOI,yBAAA;EL66DV;EKp7DM;IAOI,+BAAA;ELg7DV;EKv7DM;IAOI,8BAAA;ELm7DV;EK17DM;IAOI,4BAAA;ELs7DV;EK77DM;IAOI,8BAAA;ELy7DV;EKh8DM;IAOI,4BAAA;EL47DV;EKn8DM;IAOI,4BAAA;EL+7DV;EKt8DM;IAOI,qBAAA;ELk8DV;EKz8DM;IAOI,2BAAA;ELq8DV;EK58DM;IAOI,0BAAA;ELw8DV;EK/8DM;IAOI,wBAAA;EL28DV;EKl9DM;IAOI,0BAAA;EL88DV;EKr9DM;IAOI,wBAAA;ELi9DV;EKx9DM;IAOI,2BAAA;IAAA,0BAAA;ELq9DV;EK59DM;IAOI,iCAAA;IAAA,gCAAA;ELy9DV;EKh+DM;IAOI,gCAAA;IAAA,+BAAA;EL69DV;EKp+DM;IAOI,8BAAA;IAAA,6BAAA;ELi+DV;EKx+DM;IAOI,gCAAA;IAAA,+BAAA;ELq+DV;EK5+DM;IAOI,8BAAA;IAAA,6BAAA;ELy+DV;EKh/DM;IAOI,yBAAA;IAAA,4BAAA;EL6+DV;EKp/DM;IAOI,+BAAA;IAAA,kCAAA;ELi/DV;EKx/DM;IAOI,8BAAA;IAAA,iCAAA;ELq/DV;EK5/DM;IAOI,4BAAA;IAAA,+BAAA;ELy/DV;EKhgEM;IAOI,8BAAA;IAAA,iCAAA;EL6/DV;EKpgEM;IAOI,4BAAA;IAAA,+BAAA;ELigEV;EKxgEM;IAOI,yBAAA;ELogEV;EK3gEM;IAOI,+BAAA;ELugEV;EK9gEM;IAOI,8BAAA;EL0gEV;EKjhEM;IAOI,4BAAA;EL6gEV;EKphEM;IAOI,8BAAA;ELghEV;EKvhEM;IAOI,4BAAA;ELmhEV;EK1hEM;IAOI,2BAAA;ELshEV;EK7hEM;IAOI,iCAAA;ELyhEV;EKhiEM;IAOI,gCAAA;EL4hEV;EKniEM;IAOI,8BAAA;EL+hEV;EKtiEM;IAOI,gCAAA;ELkiEV;EKziEM;IAOI,8BAAA;ELqiEV;EK5iEM;IAOI,4BAAA;ELwiEV;EK/iEM;IAOI,kCAAA;EL2iEV;EKljEM;IAOI,iCAAA;EL8iEV;EKrjEM;IAOI,+BAAA;ELijEV;EKxjEM;IAOI,iCAAA;ELojEV;EK3jEM;IAOI,+BAAA;ELujEV;EK9jEM;IAOI,0BAAA;EL0jEV;EKjkEM;IAOI,gCAAA;EL6jEV;EKpkEM;IAOI,+BAAA;ELgkEV;EKvkEM;IAOI,6BAAA;ELmkEV;EK1kEM;IAOI,+BAAA;ELskEV;EK7kEM;IAOI,6BAAA;ELykEV;AACF;ACplEI;EIGI;IAOI,0BAAA;EL8kEV;EKrlEM;IAOI,gCAAA;ELilEV;EKxlEM;IAOI,yBAAA;ELolEV;EK3lEM;IAOI,wBAAA;ELulEV;EK9lEM;IAOI,+BAAA;EL0lEV;EKjmEM;IAOI,yBAAA;EL6lEV;EKpmEM;IAOI,6BAAA;ELgmEV;EKvmEM;IAOI,8BAAA;ELmmEV;EK1mEM;IAOI,wBAAA;ELsmEV;EK7mEM;IAOI,+BAAA;ELymEV;EKhnEM;IAOI,wBAAA;EL4mEV;EKnnEM;IAOI,yBAAA;EL+mEV;EKtnEM;IAOI,8BAAA;ELknEV;EKznEM;IAOI,iCAAA;ELqnEV;EK5nEM;IAOI,sCAAA;ELwnEV;EK/nEM;IAOI,yCAAA;EL2nEV;EKloEM;IAOI,uBAAA;EL8nEV;EKroEM;IAOI,uBAAA;ELioEV;EKxoEM;IAOI,yBAAA;ELooEV;EK3oEM;IAOI,yBAAA;ELuoEV;EK9oEM;IAOI,0BAAA;EL0oEV;EKjpEM;IAOI,4BAAA;EL6oEV;EKppEM;IAOI,kCAAA;ELgpEV;EKvpEM;IAOI,sCAAA;ELmpEV;EK1pEM;IAOI,oCAAA;ELspEV;EK7pEM;IAOI,kCAAA;ELypEV;EKhqEM;IAOI,yCAAA;EL4pEV;EKnqEM;IAOI,wCAAA;EL+pEV;EKtqEM;IAOI,wCAAA;ELkqEV;EKzqEM;IAOI,kCAAA;ELqqEV;EK5qEM;IAOI,gCAAA;ELwqEV;EK/qEM;IAOI,8BAAA;EL2qEV;EKlrEM;IAOI,gCAAA;EL8qEV;EKrrEM;IAOI,+BAAA;ELirEV;EKxrEM;IAOI,oCAAA;ELorEV;EK3rEM;IAOI,kCAAA;ELurEV;EK9rEM;IAOI,gCAAA;EL0rEV;EKjsEM;IAOI,uCAAA;EL6rEV;EKpsEM;IAOI,sCAAA;ELgsEV;EKvsEM;IAOI,iCAAA;ELmsEV;EK1sEM;IAOI,2BAAA;ELssEV;EK7sEM;IAOI,iCAAA;ELysEV;EKhtEM;IAOI,+BAAA;EL4sEV;EKntEM;IAOI,6BAAA;EL+sEV;EKttEM;IAOI,+BAAA;ELktEV;EKztEM;IAOI,8BAAA;ELqtEV;EK5tEM;IAOI,oBAAA;ELwtEV;EK/tEM;IAOI,mBAAA;EL2tEV;EKluEM;IAOI,mBAAA;EL8tEV;EKruEM;IAOI,mBAAA;ELiuEV;EKxuEM;IAOI,mBAAA;ELouEV;EK3uEM;IAOI,mBAAA;ELuuEV;EK9uEM;IAOI,mBAAA;EL0uEV;EKjvEM;IAOI,mBAAA;EL6uEV;EKpvEM;IAOI,oBAAA;ELgvEV;EKvvEM;IAOI,0BAAA;ELmvEV;EK1vEM;IAOI,yBAAA;ELsvEV;EK7vEM;IAOI,uBAAA;ELyvEV;EKhwEM;IAOI,yBAAA;EL4vEV;EKnwEM;IAOI,uBAAA;EL+vEV;EKtwEM;IAOI,uBAAA;ELkwEV;EKzwEM;IAOI,0BAAA;IAAA,yBAAA;ELswEV;EK7wEM;IAOI,gCAAA;IAAA,+BAAA;EL0wEV;EKjxEM;IAOI,+BAAA;IAAA,8BAAA;EL8wEV;EKrxEM;IAOI,6BAAA;IAAA,4BAAA;ELkxEV;EKzxEM;IAOI,+BAAA;IAAA,8BAAA;ELsxEV;EK7xEM;IAOI,6BAAA;IAAA,4BAAA;EL0xEV;EKjyEM;IAOI,6BAAA;IAAA,4BAAA;EL8xEV;EKryEM;IAOI,wBAAA;IAAA,2BAAA;ELkyEV;EKzyEM;IAOI,8BAAA;IAAA,iCAAA;ELsyEV;EK7yEM;IAOI,6BAAA;IAAA,gCAAA;EL0yEV;EKjzEM;IAOI,2BAAA;IAAA,8BAAA;EL8yEV;EKrzEM;IAOI,6BAAA;IAAA,gCAAA;ELkzEV;EKzzEM;IAOI,2BAAA;IAAA,8BAAA;ELszEV;EK7zEM;IAOI,2BAAA;IAAA,8BAAA;EL0zEV;EKj0EM;IAOI,wBAAA;EL6zEV;EKp0EM;IAOI,8BAAA;ELg0EV;EKv0EM;IAOI,6BAAA;ELm0EV;EK10EM;IAOI,2BAAA;ELs0EV;EK70EM;IAOI,6BAAA;ELy0EV;EKh1EM;IAOI,2BAAA;EL40EV;EKn1EM;IAOI,2BAAA;EL+0EV;EKt1EM;IAOI,0BAAA;ELk1EV;EKz1EM;IAOI,gCAAA;ELq1EV;EK51EM;IAOI,+BAAA;ELw1EV;EK/1EM;IAOI,6BAAA;EL21EV;EKl2EM;IAOI,+BAAA;EL81EV;EKr2EM;IAOI,6BAAA;ELi2EV;EKx2EM;IAOI,6BAAA;ELo2EV;EK32EM;IAOI,2BAAA;ELu2EV;EK92EM;IAOI,iCAAA;EL02EV;EKj3EM;IAOI,gCAAA;EL62EV;EKp3EM;IAOI,8BAAA;ELg3EV;EKv3EM;IAOI,gCAAA;ELm3EV;EK13EM;IAOI,8BAAA;ELs3EV;EK73EM;IAOI,8BAAA;ELy3EV;EKh4EM;IAOI,yBAAA;EL43EV;EKn4EM;IAOI,+BAAA;EL+3EV;EKt4EM;IAOI,8BAAA;ELk4EV;EKz4EM;IAOI,4BAAA;ELq4EV;EK54EM;IAOI,8BAAA;ELw4EV;EK/4EM;IAOI,4BAAA;EL24EV;EKl5EM;IAOI,4BAAA;EL84EV;EKr5EM;IAOI,qBAAA;ELi5EV;EKx5EM;IAOI,2BAAA;ELo5EV;EK35EM;IAOI,0BAAA;ELu5EV;EK95EM;IAOI,wBAAA;EL05EV;EKj6EM;IAOI,0BAAA;EL65EV;EKp6EM;IAOI,wBAAA;ELg6EV;EKv6EM;IAOI,2BAAA;IAAA,0BAAA;ELo6EV;EK36EM;IAOI,iCAAA;IAAA,gCAAA;ELw6EV;EK/6EM;IAOI,gCAAA;IAAA,+BAAA;EL46EV;EKn7EM;IAOI,8BAAA;IAAA,6BAAA;ELg7EV;EKv7EM;IAOI,gCAAA;IAAA,+BAAA;ELo7EV;EK37EM;IAOI,8BAAA;IAAA,6BAAA;ELw7EV;EK/7EM;IAOI,yBAAA;IAAA,4BAAA;EL47EV;EKn8EM;IAOI,+BAAA;IAAA,kCAAA;ELg8EV;EKv8EM;IAOI,8BAAA;IAAA,iCAAA;ELo8EV;EK38EM;IAOI,4BAAA;IAAA,+BAAA;ELw8EV;EK/8EM;IAOI,8BAAA;IAAA,iCAAA;EL48EV;EKn9EM;IAOI,4BAAA;IAAA,+BAAA;ELg9EV;EKv9EM;IAOI,yBAAA;ELm9EV;EK19EM;IAOI,+BAAA;ELs9EV;EK79EM;IAOI,8BAAA;ELy9EV;EKh+EM;IAOI,4BAAA;EL49EV;EKn+EM;IAOI,8BAAA;EL+9EV;EKt+EM;IAOI,4BAAA;ELk+EV;EKz+EM;IAOI,2BAAA;ELq+EV;EK5+EM;IAOI,iCAAA;ELw+EV;EK/+EM;IAOI,gCAAA;EL2+EV;EKl/EM;IAOI,8BAAA;EL8+EV;EKr/EM;IAOI,gCAAA;ELi/EV;EKx/EM;IAOI,8BAAA;ELo/EV;EK3/EM;IAOI,4BAAA;ELu/EV;EK9/EM;IAOI,kCAAA;EL0/EV;EKjgFM;IAOI,iCAAA;EL6/EV;EKpgFM;IAOI,+BAAA;ELggFV;EKvgFM;IAOI,iCAAA;ELmgFV;EK1gFM;IAOI,+BAAA;ELsgFV;EK7gFM;IAOI,0BAAA;ELygFV;EKhhFM;IAOI,gCAAA;EL4gFV;EKnhFM;IAOI,+BAAA;EL+gFV;EKthFM;IAOI,6BAAA;ELkhFV;EKzhFM;IAOI,+BAAA;ELqhFV;EK5hFM;IAOI,6BAAA;ELwhFV;AACF;ACniFI;EIGI;IAOI,0BAAA;EL6hFV;EKpiFM;IAOI,gCAAA;ELgiFV;EKviFM;IAOI,yBAAA;ELmiFV;EK1iFM;IAOI,wBAAA;ELsiFV;EK7iFM;IAOI,+BAAA;ELyiFV;EKhjFM;IAOI,yBAAA;EL4iFV;EKnjFM;IAOI,6BAAA;EL+iFV;EKtjFM;IAOI,8BAAA;ELkjFV;EKzjFM;IAOI,wBAAA;ELqjFV;EK5jFM;IAOI,+BAAA;ELwjFV;EK/jFM;IAOI,wBAAA;EL2jFV;EKlkFM;IAOI,yBAAA;EL8jFV;EKrkFM;IAOI,8BAAA;ELikFV;EKxkFM;IAOI,iCAAA;ELokFV;EK3kFM;IAOI,sCAAA;ELukFV;EK9kFM;IAOI,yCAAA;EL0kFV;EKjlFM;IAOI,uBAAA;EL6kFV;EKplFM;IAOI,uBAAA;ELglFV;EKvlFM;IAOI,yBAAA;ELmlFV;EK1lFM;IAOI,yBAAA;ELslFV;EK7lFM;IAOI,0BAAA;ELylFV;EKhmFM;IAOI,4BAAA;EL4lFV;EKnmFM;IAOI,kCAAA;EL+lFV;EKtmFM;IAOI,sCAAA;ELkmFV;EKzmFM;IAOI,oCAAA;ELqmFV;EK5mFM;IAOI,kCAAA;ELwmFV;EK/mFM;IAOI,yCAAA;EL2mFV;EKlnFM;IAOI,wCAAA;EL8mFV;EKrnFM;IAOI,wCAAA;ELinFV;EKxnFM;IAOI,kCAAA;ELonFV;EK3nFM;IAOI,gCAAA;ELunFV;EK9nFM;IAOI,8BAAA;EL0nFV;EKjoFM;IAOI,gCAAA;EL6nFV;EKpoFM;IAOI,+BAAA;ELgoFV;EKvoFM;IAOI,oCAAA;ELmoFV;EK1oFM;IAOI,kCAAA;ELsoFV;EK7oFM;IAOI,gCAAA;ELyoFV;EKhpFM;IAOI,uCAAA;EL4oFV;EKnpFM;IAOI,sCAAA;EL+oFV;EKtpFM;IAOI,iCAAA;ELkpFV;EKzpFM;IAOI,2BAAA;ELqpFV;EK5pFM;IAOI,iCAAA;ELwpFV;EK/pFM;IAOI,+BAAA;EL2pFV;EKlqFM;IAOI,6BAAA;EL8pFV;EKrqFM;IAOI,+BAAA;ELiqFV;EKxqFM;IAOI,8BAAA;ELoqFV;EK3qFM;IAOI,oBAAA;ELuqFV;EK9qFM;IAOI,mBAAA;EL0qFV;EKjrFM;IAOI,mBAAA;EL6qFV;EKprFM;IAOI,mBAAA;ELgrFV;EKvrFM;IAOI,mBAAA;ELmrFV;EK1rFM;IAOI,mBAAA;ELsrFV;EK7rFM;IAOI,mBAAA;ELyrFV;EKhsFM;IAOI,mBAAA;EL4rFV;EKnsFM;IAOI,oBAAA;EL+rFV;EKtsFM;IAOI,0BAAA;ELksFV;EKzsFM;IAOI,yBAAA;ELqsFV;EK5sFM;IAOI,uBAAA;ELwsFV;EK/sFM;IAOI,yBAAA;EL2sFV;EKltFM;IAOI,uBAAA;EL8sFV;EKrtFM;IAOI,uBAAA;ELitFV;EKxtFM;IAOI,0BAAA;IAAA,yBAAA;ELqtFV;EK5tFM;IAOI,gCAAA;IAAA,+BAAA;ELytFV;EKhuFM;IAOI,+BAAA;IAAA,8BAAA;EL6tFV;EKpuFM;IAOI,6BAAA;IAAA,4BAAA;ELiuFV;EKxuFM;IAOI,+BAAA;IAAA,8BAAA;ELquFV;EK5uFM;IAOI,6BAAA;IAAA,4BAAA;ELyuFV;EKhvFM;IAOI,6BAAA;IAAA,4BAAA;EL6uFV;EKpvFM;IAOI,wBAAA;IAAA,2BAAA;ELivFV;EKxvFM;IAOI,8BAAA;IAAA,iCAAA;ELqvFV;EK5vFM;IAOI,6BAAA;IAAA,gCAAA;ELyvFV;EKhwFM;IAOI,2BAAA;IAAA,8BAAA;EL6vFV;EKpwFM;IAOI,6BAAA;IAAA,gCAAA;ELiwFV;EKxwFM;IAOI,2BAAA;IAAA,8BAAA;ELqwFV;EK5wFM;IAOI,2BAAA;IAAA,8BAAA;ELywFV;EKhxFM;IAOI,wBAAA;EL4wFV;EKnxFM;IAOI,8BAAA;EL+wFV;EKtxFM;IAOI,6BAAA;ELkxFV;EKzxFM;IAOI,2BAAA;ELqxFV;EK5xFM;IAOI,6BAAA;ELwxFV;EK/xFM;IAOI,2BAAA;EL2xFV;EKlyFM;IAOI,2BAAA;EL8xFV;EKryFM;IAOI,0BAAA;ELiyFV;EKxyFM;IAOI,gCAAA;ELoyFV;EK3yFM;IAOI,+BAAA;ELuyFV;EK9yFM;IAOI,6BAAA;EL0yFV;EKjzFM;IAOI,+BAAA;EL6yFV;EKpzFM;IAOI,6BAAA;ELgzFV;EKvzFM;IAOI,6BAAA;ELmzFV;EK1zFM;IAOI,2BAAA;ELszFV;EK7zFM;IAOI,iCAAA;ELyzFV;EKh0FM;IAOI,gCAAA;EL4zFV;EKn0FM;IAOI,8BAAA;EL+zFV;EKt0FM;IAOI,gCAAA;ELk0FV;EKz0FM;IAOI,8BAAA;ELq0FV;EK50FM;IAOI,8BAAA;ELw0FV;EK/0FM;IAOI,yBAAA;EL20FV;EKl1FM;IAOI,+BAAA;EL80FV;EKr1FM;IAOI,8BAAA;ELi1FV;EKx1FM;IAOI,4BAAA;ELo1FV;EK31FM;IAOI,8BAAA;ELu1FV;EK91FM;IAOI,4BAAA;EL01FV;EKj2FM;IAOI,4BAAA;EL61FV;EKp2FM;IAOI,qBAAA;ELg2FV;EKv2FM;IAOI,2BAAA;ELm2FV;EK12FM;IAOI,0BAAA;ELs2FV;EK72FM;IAOI,wBAAA;ELy2FV;EKh3FM;IAOI,0BAAA;EL42FV;EKn3FM;IAOI,wBAAA;EL+2FV;EKt3FM;IAOI,2BAAA;IAAA,0BAAA;ELm3FV;EK13FM;IAOI,iCAAA;IAAA,gCAAA;ELu3FV;EK93FM;IAOI,gCAAA;IAAA,+BAAA;EL23FV;EKl4FM;IAOI,8BAAA;IAAA,6BAAA;EL+3FV;EKt4FM;IAOI,gCAAA;IAAA,+BAAA;ELm4FV;EK14FM;IAOI,8BAAA;IAAA,6BAAA;ELu4FV;EK94FM;IAOI,yBAAA;IAAA,4BAAA;EL24FV;EKl5FM;IAOI,+BAAA;IAAA,kCAAA;EL+4FV;EKt5FM;IAOI,8BAAA;IAAA,iCAAA;ELm5FV;EK15FM;IAOI,4BAAA;IAAA,+BAAA;ELu5FV;EK95FM;IAOI,8BAAA;IAAA,iCAAA;EL25FV;EKl6FM;IAOI,4BAAA;IAAA,+BAAA;EL+5FV;EKt6FM;IAOI,yBAAA;ELk6FV;EKz6FM;IAOI,+BAAA;ELq6FV;EK56FM;IAOI,8BAAA;ELw6FV;EK/6FM;IAOI,4BAAA;EL26FV;EKl7FM;IAOI,8BAAA;EL86FV;EKr7FM;IAOI,4BAAA;ELi7FV;EKx7FM;IAOI,2BAAA;ELo7FV;EK37FM;IAOI,iCAAA;ELu7FV;EK97FM;IAOI,gCAAA;EL07FV;EKj8FM;IAOI,8BAAA;EL67FV;EKp8FM;IAOI,gCAAA;ELg8FV;EKv8FM;IAOI,8BAAA;ELm8FV;EK18FM;IAOI,4BAAA;ELs8FV;EK78FM;IAOI,kCAAA;ELy8FV;EKh9FM;IAOI,iCAAA;EL48FV;EKn9FM;IAOI,+BAAA;EL+8FV;EKt9FM;IAOI,iCAAA;ELk9FV;EKz9FM;IAOI,+BAAA;ELq9FV;EK59FM;IAOI,0BAAA;ELw9FV;EK/9FM;IAOI,gCAAA;EL29FV;EKl+FM;IAOI,+BAAA;EL89FV;EKr+FM;IAOI,6BAAA;ELi+FV;EKx+FM;IAOI,+BAAA;ELo+FV;EK3+FM;IAOI,6BAAA;ELu+FV;AACF;ACl/FI;EIGI;IAOI,0BAAA;EL4+FV;EKn/FM;IAOI,gCAAA;EL++FV;EKt/FM;IAOI,yBAAA;ELk/FV;EKz/FM;IAOI,wBAAA;ELq/FV;EK5/FM;IAOI,+BAAA;ELw/FV;EK//FM;IAOI,yBAAA;EL2/FV;EKlgGM;IAOI,6BAAA;EL8/FV;EKrgGM;IAOI,8BAAA;ELigGV;EKxgGM;IAOI,wBAAA;ELogGV;EK3gGM;IAOI,+BAAA;ELugGV;EK9gGM;IAOI,wBAAA;EL0gGV;EKjhGM;IAOI,yBAAA;EL6gGV;EKphGM;IAOI,8BAAA;ELghGV;EKvhGM;IAOI,iCAAA;ELmhGV;EK1hGM;IAOI,sCAAA;ELshGV;EK7hGM;IAOI,yCAAA;ELyhGV;EKhiGM;IAOI,uBAAA;EL4hGV;EKniGM;IAOI,uBAAA;EL+hGV;EKtiGM;IAOI,yBAAA;ELkiGV;EKziGM;IAOI,yBAAA;ELqiGV;EK5iGM;IAOI,0BAAA;ELwiGV;EK/iGM;IAOI,4BAAA;EL2iGV;EKljGM;IAOI,kCAAA;EL8iGV;EKrjGM;IAOI,sCAAA;ELijGV;EKxjGM;IAOI,oCAAA;ELojGV;EK3jGM;IAOI,kCAAA;ELujGV;EK9jGM;IAOI,yCAAA;EL0jGV;EKjkGM;IAOI,wCAAA;EL6jGV;EKpkGM;IAOI,wCAAA;ELgkGV;EKvkGM;IAOI,kCAAA;ELmkGV;EK1kGM;IAOI,gCAAA;ELskGV;EK7kGM;IAOI,8BAAA;ELykGV;EKhlGM;IAOI,gCAAA;EL4kGV;EKnlGM;IAOI,+BAAA;EL+kGV;EKtlGM;IAOI,oCAAA;ELklGV;EKzlGM;IAOI,kCAAA;ELqlGV;EK5lGM;IAOI,gCAAA;ELwlGV;EK/lGM;IAOI,uCAAA;EL2lGV;EKlmGM;IAOI,sCAAA;EL8lGV;EKrmGM;IAOI,iCAAA;ELimGV;EKxmGM;IAOI,2BAAA;ELomGV;EK3mGM;IAOI,iCAAA;ELumGV;EK9mGM;IAOI,+BAAA;EL0mGV;EKjnGM;IAOI,6BAAA;EL6mGV;EKpnGM;IAOI,+BAAA;ELgnGV;EKvnGM;IAOI,8BAAA;ELmnGV;EK1nGM;IAOI,oBAAA;ELsnGV;EK7nGM;IAOI,mBAAA;ELynGV;EKhoGM;IAOI,mBAAA;EL4nGV;EKnoGM;IAOI,mBAAA;EL+nGV;EKtoGM;IAOI,mBAAA;ELkoGV;EKzoGM;IAOI,mBAAA;ELqoGV;EK5oGM;IAOI,mBAAA;ELwoGV;EK/oGM;IAOI,mBAAA;EL2oGV;EKlpGM;IAOI,oBAAA;EL8oGV;EKrpGM;IAOI,0BAAA;ELipGV;EKxpGM;IAOI,yBAAA;ELopGV;EK3pGM;IAOI,uBAAA;ELupGV;EK9pGM;IAOI,yBAAA;EL0pGV;EKjqGM;IAOI,uBAAA;EL6pGV;EKpqGM;IAOI,uBAAA;ELgqGV;EKvqGM;IAOI,0BAAA;IAAA,yBAAA;ELoqGV;EK3qGM;IAOI,gCAAA;IAAA,+BAAA;ELwqGV;EK/qGM;IAOI,+BAAA;IAAA,8BAAA;EL4qGV;EKnrGM;IAOI,6BAAA;IAAA,4BAAA;ELgrGV;EKvrGM;IAOI,+BAAA;IAAA,8BAAA;ELorGV;EK3rGM;IAOI,6BAAA;IAAA,4BAAA;ELwrGV;EK/rGM;IAOI,6BAAA;IAAA,4BAAA;EL4rGV;EKnsGM;IAOI,wBAAA;IAAA,2BAAA;ELgsGV;EKvsGM;IAOI,8BAAA;IAAA,iCAAA;ELosGV;EK3sGM;IAOI,6BAAA;IAAA,gCAAA;ELwsGV;EK/sGM;IAOI,2BAAA;IAAA,8BAAA;EL4sGV;EKntGM;IAOI,6BAAA;IAAA,gCAAA;ELgtGV;EKvtGM;IAOI,2BAAA;IAAA,8BAAA;ELotGV;EK3tGM;IAOI,2BAAA;IAAA,8BAAA;ELwtGV;EK/tGM;IAOI,wBAAA;EL2tGV;EKluGM;IAOI,8BAAA;EL8tGV;EKruGM;IAOI,6BAAA;ELiuGV;EKxuGM;IAOI,2BAAA;ELouGV;EK3uGM;IAOI,6BAAA;ELuuGV;EK9uGM;IAOI,2BAAA;EL0uGV;EKjvGM;IAOI,2BAAA;EL6uGV;EKpvGM;IAOI,0BAAA;ELgvGV;EKvvGM;IAOI,gCAAA;ELmvGV;EK1vGM;IAOI,+BAAA;ELsvGV;EK7vGM;IAOI,6BAAA;ELyvGV;EKhwGM;IAOI,+BAAA;EL4vGV;EKnwGM;IAOI,6BAAA;EL+vGV;EKtwGM;IAOI,6BAAA;ELkwGV;EKzwGM;IAOI,2BAAA;ELqwGV;EK5wGM;IAOI,iCAAA;ELwwGV;EK/wGM;IAOI,gCAAA;EL2wGV;EKlxGM;IAOI,8BAAA;EL8wGV;EKrxGM;IAOI,gCAAA;ELixGV;EKxxGM;IAOI,8BAAA;ELoxGV;EK3xGM;IAOI,8BAAA;ELuxGV;EK9xGM;IAOI,yBAAA;EL0xGV;EKjyGM;IAOI,+BAAA;EL6xGV;EKpyGM;IAOI,8BAAA;ELgyGV;EKvyGM;IAOI,4BAAA;ELmyGV;EK1yGM;IAOI,8BAAA;ELsyGV;EK7yGM;IAOI,4BAAA;ELyyGV;EKhzGM;IAOI,4BAAA;EL4yGV;EKnzGM;IAOI,qBAAA;EL+yGV;EKtzGM;IAOI,2BAAA;ELkzGV;EKzzGM;IAOI,0BAAA;ELqzGV;EK5zGM;IAOI,wBAAA;ELwzGV;EK/zGM;IAOI,0BAAA;EL2zGV;EKl0GM;IAOI,wBAAA;EL8zGV;EKr0GM;IAOI,2BAAA;IAAA,0BAAA;ELk0GV;EKz0GM;IAOI,iCAAA;IAAA,gCAAA;ELs0GV;EK70GM;IAOI,gCAAA;IAAA,+BAAA;EL00GV;EKj1GM;IAOI,8BAAA;IAAA,6BAAA;EL80GV;EKr1GM;IAOI,gCAAA;IAAA,+BAAA;ELk1GV;EKz1GM;IAOI,8BAAA;IAAA,6BAAA;ELs1GV;EK71GM;IAOI,yBAAA;IAAA,4BAAA;EL01GV;EKj2GM;IAOI,+BAAA;IAAA,kCAAA;EL81GV;EKr2GM;IAOI,8BAAA;IAAA,iCAAA;ELk2GV;EKz2GM;IAOI,4BAAA;IAAA,+BAAA;ELs2GV;EK72GM;IAOI,8BAAA;IAAA,iCAAA;EL02GV;EKj3GM;IAOI,4BAAA;IAAA,+BAAA;EL82GV;EKr3GM;IAOI,yBAAA;ELi3GV;EKx3GM;IAOI,+BAAA;ELo3GV;EK33GM;IAOI,8BAAA;ELu3GV;EK93GM;IAOI,4BAAA;EL03GV;EKj4GM;IAOI,8BAAA;EL63GV;EKp4GM;IAOI,4BAAA;ELg4GV;EKv4GM;IAOI,2BAAA;ELm4GV;EK14GM;IAOI,iCAAA;ELs4GV;EK74GM;IAOI,gCAAA;ELy4GV;EKh5GM;IAOI,8BAAA;EL44GV;EKn5GM;IAOI,gCAAA;EL+4GV;EKt5GM;IAOI,8BAAA;ELk5GV;EKz5GM;IAOI,4BAAA;ELq5GV;EK55GM;IAOI,kCAAA;ELw5GV;EK/5GM;IAOI,iCAAA;EL25GV;EKl6GM;IAOI,+BAAA;EL85GV;EKr6GM;IAOI,iCAAA;ELi6GV;EKx6GM;IAOI,+BAAA;ELo6GV;EK36GM;IAOI,0BAAA;ELu6GV;EK96GM;IAOI,gCAAA;EL06GV;EKj7GM;IAOI,+BAAA;EL66GV;EKp7GM;IAOI,6BAAA;ELg7GV;EKv7GM;IAOI,+BAAA;ELm7GV;EK17GM;IAOI,6BAAA;ELs7GV;AACF;ACj8GI;EIGI;IAOI,0BAAA;EL27GV;EKl8GM;IAOI,gCAAA;EL87GV;EKr8GM;IAOI,yBAAA;ELi8GV;EKx8GM;IAOI,wBAAA;ELo8GV;EK38GM;IAOI,+BAAA;ELu8GV;EK98GM;IAOI,yBAAA;EL08GV;EKj9GM;IAOI,6BAAA;EL68GV;EKp9GM;IAOI,8BAAA;ELg9GV;EKv9GM;IAOI,wBAAA;ELm9GV;EK19GM;IAOI,+BAAA;ELs9GV;EK79GM;IAOI,wBAAA;ELy9GV;EKh+GM;IAOI,yBAAA;EL49GV;EKn+GM;IAOI,8BAAA;EL+9GV;EKt+GM;IAOI,iCAAA;ELk+GV;EKz+GM;IAOI,sCAAA;ELq+GV;EK5+GM;IAOI,yCAAA;ELw+GV;EK/+GM;IAOI,uBAAA;EL2+GV;EKl/GM;IAOI,uBAAA;EL8+GV;EKr/GM;IAOI,yBAAA;ELi/GV;EKx/GM;IAOI,yBAAA;ELo/GV;EK3/GM;IAOI,0BAAA;ELu/GV;EK9/GM;IAOI,4BAAA;EL0/GV;EKjgHM;IAOI,kCAAA;EL6/GV;EKpgHM;IAOI,sCAAA;ELggHV;EKvgHM;IAOI,oCAAA;ELmgHV;EK1gHM;IAOI,kCAAA;ELsgHV;EK7gHM;IAOI,yCAAA;ELygHV;EKhhHM;IAOI,wCAAA;EL4gHV;EKnhHM;IAOI,wCAAA;EL+gHV;EKthHM;IAOI,kCAAA;ELkhHV;EKzhHM;IAOI,gCAAA;ELqhHV;EK5hHM;IAOI,8BAAA;ELwhHV;EK/hHM;IAOI,gCAAA;EL2hHV;EKliHM;IAOI,+BAAA;EL8hHV;EKriHM;IAOI,oCAAA;ELiiHV;EKxiHM;IAOI,kCAAA;ELoiHV;EK3iHM;IAOI,gCAAA;ELuiHV;EK9iHM;IAOI,uCAAA;EL0iHV;EKjjHM;IAOI,sCAAA;EL6iHV;EKpjHM;IAOI,iCAAA;ELgjHV;EKvjHM;IAOI,2BAAA;ELmjHV;EK1jHM;IAOI,iCAAA;ELsjHV;EK7jHM;IAOI,+BAAA;ELyjHV;EKhkHM;IAOI,6BAAA;EL4jHV;EKnkHM;IAOI,+BAAA;EL+jHV;EKtkHM;IAOI,8BAAA;ELkkHV;EKzkHM;IAOI,oBAAA;ELqkHV;EK5kHM;IAOI,mBAAA;ELwkHV;EK/kHM;IAOI,mBAAA;EL2kHV;EKllHM;IAOI,mBAAA;EL8kHV;EKrlHM;IAOI,mBAAA;ELilHV;EKxlHM;IAOI,mBAAA;ELolHV;EK3lHM;IAOI,mBAAA;ELulHV;EK9lHM;IAOI,mBAAA;EL0lHV;EKjmHM;IAOI,oBAAA;EL6lHV;EKpmHM;IAOI,0BAAA;ELgmHV;EKvmHM;IAOI,yBAAA;ELmmHV;EK1mHM;IAOI,uBAAA;ELsmHV;EK7mHM;IAOI,yBAAA;ELymHV;EKhnHM;IAOI,uBAAA;EL4mHV;EKnnHM;IAOI,uBAAA;EL+mHV;EKtnHM;IAOI,0BAAA;IAAA,yBAAA;ELmnHV;EK1nHM;IAOI,gCAAA;IAAA,+BAAA;ELunHV;EK9nHM;IAOI,+BAAA;IAAA,8BAAA;EL2nHV;EKloHM;IAOI,6BAAA;IAAA,4BAAA;EL+nHV;EKtoHM;IAOI,+BAAA;IAAA,8BAAA;ELmoHV;EK1oHM;IAOI,6BAAA;IAAA,4BAAA;ELuoHV;EK9oHM;IAOI,6BAAA;IAAA,4BAAA;EL2oHV;EKlpHM;IAOI,wBAAA;IAAA,2BAAA;EL+oHV;EKtpHM;IAOI,8BAAA;IAAA,iCAAA;ELmpHV;EK1pHM;IAOI,6BAAA;IAAA,gCAAA;ELupHV;EK9pHM;IAOI,2BAAA;IAAA,8BAAA;EL2pHV;EKlqHM;IAOI,6BAAA;IAAA,gCAAA;EL+pHV;EKtqHM;IAOI,2BAAA;IAAA,8BAAA;ELmqHV;EK1qHM;IAOI,2BAAA;IAAA,8BAAA;ELuqHV;EK9qHM;IAOI,wBAAA;EL0qHV;EKjrHM;IAOI,8BAAA;EL6qHV;EKprHM;IAOI,6BAAA;ELgrHV;EKvrHM;IAOI,2BAAA;ELmrHV;EK1rHM;IAOI,6BAAA;ELsrHV;EK7rHM;IAOI,2BAAA;ELyrHV;EKhsHM;IAOI,2BAAA;EL4rHV;EKnsHM;IAOI,0BAAA;EL+rHV;EKtsHM;IAOI,gCAAA;ELksHV;EKzsHM;IAOI,+BAAA;ELqsHV;EK5sHM;IAOI,6BAAA;ELwsHV;EK/sHM;IAOI,+BAAA;EL2sHV;EKltHM;IAOI,6BAAA;EL8sHV;EKrtHM;IAOI,6BAAA;ELitHV;EKxtHM;IAOI,2BAAA;ELotHV;EK3tHM;IAOI,iCAAA;ELutHV;EK9tHM;IAOI,gCAAA;EL0tHV;EKjuHM;IAOI,8BAAA;EL6tHV;EKpuHM;IAOI,gCAAA;ELguHV;EKvuHM;IAOI,8BAAA;ELmuHV;EK1uHM;IAOI,8BAAA;ELsuHV;EK7uHM;IAOI,yBAAA;ELyuHV;EKhvHM;IAOI,+BAAA;EL4uHV;EKnvHM;IAOI,8BAAA;EL+uHV;EKtvHM;IAOI,4BAAA;ELkvHV;EKzvHM;IAOI,8BAAA;ELqvHV;EK5vHM;IAOI,4BAAA;ELwvHV;EK/vHM;IAOI,4BAAA;EL2vHV;EKlwHM;IAOI,qBAAA;EL8vHV;EKrwHM;IAOI,2BAAA;ELiwHV;EKxwHM;IAOI,0BAAA;ELowHV;EK3wHM;IAOI,wBAAA;ELuwHV;EK9wHM;IAOI,0BAAA;EL0wHV;EKjxHM;IAOI,wBAAA;EL6wHV;EKpxHM;IAOI,2BAAA;IAAA,0BAAA;ELixHV;EKxxHM;IAOI,iCAAA;IAAA,gCAAA;ELqxHV;EK5xHM;IAOI,gCAAA;IAAA,+BAAA;ELyxHV;EKhyHM;IAOI,8BAAA;IAAA,6BAAA;EL6xHV;EKpyHM;IAOI,gCAAA;IAAA,+BAAA;ELiyHV;EKxyHM;IAOI,8BAAA;IAAA,6BAAA;ELqyHV;EK5yHM;IAOI,yBAAA;IAAA,4BAAA;ELyyHV;EKhzHM;IAOI,+BAAA;IAAA,kCAAA;EL6yHV;EKpzHM;IAOI,8BAAA;IAAA,iCAAA;ELizHV;EKxzHM;IAOI,4BAAA;IAAA,+BAAA;ELqzHV;EK5zHM;IAOI,8BAAA;IAAA,iCAAA;ELyzHV;EKh0HM;IAOI,4BAAA;IAAA,+BAAA;EL6zHV;EKp0HM;IAOI,yBAAA;ELg0HV;EKv0HM;IAOI,+BAAA;ELm0HV;EK10HM;IAOI,8BAAA;ELs0HV;EK70HM;IAOI,4BAAA;ELy0HV;EKh1HM;IAOI,8BAAA;EL40HV;EKn1HM;IAOI,4BAAA;EL+0HV;EKt1HM;IAOI,2BAAA;ELk1HV;EKz1HM;IAOI,iCAAA;ELq1HV;EK51HM;IAOI,gCAAA;ELw1HV;EK/1HM;IAOI,8BAAA;EL21HV;EKl2HM;IAOI,gCAAA;EL81HV;EKr2HM;IAOI,8BAAA;ELi2HV;EKx2HM;IAOI,4BAAA;ELo2HV;EK32HM;IAOI,kCAAA;ELu2HV;EK92HM;IAOI,iCAAA;EL02HV;EKj3HM;IAOI,+BAAA;EL62HV;EKp3HM;IAOI,iCAAA;ELg3HV;EKv3HM;IAOI,+BAAA;ELm3HV;EK13HM;IAOI,0BAAA;ELs3HV;EK73HM;IAOI,gCAAA;ELy3HV;EKh4HM;IAOI,+BAAA;EL43HV;EKn4HM;IAOI,6BAAA;EL+3HV;EKt4HM;IAOI,+BAAA;ELk4HV;EKz4HM;IAOI,6BAAA;ELq4HV;AACF;AMz6HA;ED4BQ;IAOI,0BAAA;EL04HV;EKj5HM;IAOI,gCAAA;EL64HV;EKp5HM;IAOI,yBAAA;ELg5HV;EKv5HM;IAOI,wBAAA;ELm5HV;EK15HM;IAOI,+BAAA;ELs5HV;EK75HM;IAOI,yBAAA;ELy5HV;EKh6HM;IAOI,6BAAA;EL45HV;EKn6HM;IAOI,8BAAA;EL+5HV;EKt6HM;IAOI,wBAAA;ELk6HV;EKz6HM;IAOI,+BAAA;ELq6HV;EK56HM;IAOI,wBAAA;ELw6HV;AACF","file":"bootstrap-grid.css","sourcesContent":["@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-container-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n","// Container mixins\n\n@mixin make-container($gutter: $container-padding-x) {\n --#{$prefix}gutter-x: #{$gutter};\n --#{$prefix}gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-right: auto;\n margin-left: auto;\n}\n","/*!\n * Bootstrap Grid v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n:root {\n --bs-breakpoint-xs: 0;\n --bs-breakpoint-sm: 576px;\n --bs-breakpoint-md: 768px;\n --bs-breakpoint-lg: 992px;\n --bs-breakpoint-xl: 1200px;\n --bs-breakpoint-xxl: 1400px;\n}\n\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(-1 * var(--bs-gutter-y));\n margin-right: calc(-0.5 * var(--bs-gutter-x));\n margin-left: calc(-0.5 * var(--bs-gutter-x));\n}\n.row > * {\n box-sizing: border-box;\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0;\n }\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n .g-sm-0,\n .gx-sm-0 {\n --bs-gutter-x: 0;\n }\n .g-sm-0,\n .gy-sm-0 {\n --bs-gutter-y: 0;\n }\n .g-sm-1,\n .gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-sm-1,\n .gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-sm-2,\n .gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-sm-2,\n .gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-sm-3,\n .gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n .g-sm-3,\n .gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n .g-sm-4,\n .gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-sm-4,\n .gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-sm-5,\n .gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n .g-sm-5,\n .gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0;\n }\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n .g-md-0,\n .gx-md-0 {\n --bs-gutter-x: 0;\n }\n .g-md-0,\n .gy-md-0 {\n --bs-gutter-y: 0;\n }\n .g-md-1,\n .gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-md-1,\n .gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-md-2,\n .gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-md-2,\n .gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-md-3,\n .gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n .g-md-3,\n .gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n .g-md-4,\n .gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-md-4,\n .gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-md-5,\n .gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n .g-md-5,\n .gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0;\n }\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n .g-lg-0,\n .gx-lg-0 {\n --bs-gutter-x: 0;\n }\n .g-lg-0,\n .gy-lg-0 {\n --bs-gutter-y: 0;\n }\n .g-lg-1,\n .gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-lg-1,\n .gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-lg-2,\n .gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-lg-2,\n .gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-lg-3,\n .gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n .g-lg-3,\n .gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n .g-lg-4,\n .gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-lg-4,\n .gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-lg-5,\n .gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n .g-lg-5,\n .gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0;\n }\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n .g-xl-0,\n .gx-xl-0 {\n --bs-gutter-x: 0;\n }\n .g-xl-0,\n .gy-xl-0 {\n --bs-gutter-y: 0;\n }\n .g-xl-1,\n .gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xl-1,\n .gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xl-2,\n .gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xl-2,\n .gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xl-3,\n .gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xl-3,\n .gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xl-4,\n .gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xl-4,\n .gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xl-5,\n .gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xl-5,\n .gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0;\n }\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xxl-0 {\n margin-left: 0;\n }\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xxl-3 {\n margin-left: 25%;\n }\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xxl-6 {\n margin-left: 50%;\n }\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xxl-9 {\n margin-left: 75%;\n }\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n .g-xxl-0,\n .gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n .g-xxl-0,\n .gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n .g-xxl-1,\n .gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xxl-1,\n .gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xxl-2,\n .gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xxl-2,\n .gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xxl-3,\n .gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xxl-3,\n .gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xxl-4,\n .gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xxl-4,\n .gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xxl-5,\n .gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xxl-5,\n .gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-inline-grid {\n display: inline-grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-grid {\n display: grid !important;\n }\n .d-sm-inline-grid {\n display: inline-grid !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n .d-sm-none {\n display: none !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n .order-sm-first {\n order: -1 !important;\n }\n .order-sm-0 {\n order: 0 !important;\n }\n .order-sm-1 {\n order: 1 !important;\n }\n .order-sm-2 {\n order: 2 !important;\n }\n .order-sm-3 {\n order: 3 !important;\n }\n .order-sm-4 {\n order: 4 !important;\n }\n .order-sm-5 {\n order: 5 !important;\n }\n .order-sm-last {\n order: 6 !important;\n }\n .m-sm-0 {\n margin: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n .mt-sm-auto {\n margin-top: auto !important;\n }\n .me-sm-0 {\n margin-right: 0 !important;\n }\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n .me-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n .ms-sm-auto {\n margin-left: auto !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 768px) {\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-grid {\n display: grid !important;\n }\n .d-md-inline-grid {\n display: inline-grid !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n .d-md-none {\n display: none !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n .order-md-first {\n order: -1 !important;\n }\n .order-md-0 {\n order: 0 !important;\n }\n .order-md-1 {\n order: 1 !important;\n }\n .order-md-2 {\n order: 2 !important;\n }\n .order-md-3 {\n order: 3 !important;\n }\n .order-md-4 {\n order: 4 !important;\n }\n .order-md-5 {\n order: 5 !important;\n }\n .order-md-last {\n order: 6 !important;\n }\n .m-md-0 {\n margin: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-md-0 {\n margin-top: 0 !important;\n }\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n .mt-md-auto {\n margin-top: auto !important;\n }\n .me-md-0 {\n margin-right: 0 !important;\n }\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n .me-md-3 {\n margin-right: 1rem !important;\n }\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n .me-md-5 {\n margin-right: 3rem !important;\n }\n .me-md-auto {\n margin-right: auto !important;\n }\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n .ms-md-0 {\n margin-left: 0 !important;\n }\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n .ms-md-auto {\n margin-left: auto !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-md-0 {\n padding-top: 0 !important;\n }\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n .pe-md-0 {\n padding-right: 0 !important;\n }\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n .ps-md-0 {\n padding-left: 0 !important;\n }\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 992px) {\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-grid {\n display: grid !important;\n }\n .d-lg-inline-grid {\n display: inline-grid !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n .d-lg-none {\n display: none !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n .order-lg-first {\n order: -1 !important;\n }\n .order-lg-0 {\n order: 0 !important;\n }\n .order-lg-1 {\n order: 1 !important;\n }\n .order-lg-2 {\n order: 2 !important;\n }\n .order-lg-3 {\n order: 3 !important;\n }\n .order-lg-4 {\n order: 4 !important;\n }\n .order-lg-5 {\n order: 5 !important;\n }\n .order-lg-last {\n order: 6 !important;\n }\n .m-lg-0 {\n margin: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n .mt-lg-auto {\n margin-top: auto !important;\n }\n .me-lg-0 {\n margin-right: 0 !important;\n }\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n .me-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n .ms-lg-auto {\n margin-left: auto !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1200px) {\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-grid {\n display: grid !important;\n }\n .d-xl-inline-grid {\n display: inline-grid !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n .d-xl-none {\n display: none !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n .order-xl-first {\n order: -1 !important;\n }\n .order-xl-0 {\n order: 0 !important;\n }\n .order-xl-1 {\n order: 1 !important;\n }\n .order-xl-2 {\n order: 2 !important;\n }\n .order-xl-3 {\n order: 3 !important;\n }\n .order-xl-4 {\n order: 4 !important;\n }\n .order-xl-5 {\n order: 5 !important;\n }\n .order-xl-last {\n order: 6 !important;\n }\n .m-xl-0 {\n margin: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n .mt-xl-auto {\n margin-top: auto !important;\n }\n .me-xl-0 {\n margin-right: 0 !important;\n }\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n .me-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n .ms-xl-auto {\n margin-left: auto !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1400px) {\n .d-xxl-inline {\n display: inline !important;\n }\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n .d-xxl-block {\n display: block !important;\n }\n .d-xxl-grid {\n display: grid !important;\n }\n .d-xxl-inline-grid {\n display: inline-grid !important;\n }\n .d-xxl-table {\n display: table !important;\n }\n .d-xxl-table-row {\n display: table-row !important;\n }\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n .d-xxl-flex {\n display: flex !important;\n }\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n .d-xxl-none {\n display: none !important;\n }\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xxl-row {\n flex-direction: row !important;\n }\n .flex-xxl-column {\n flex-direction: column !important;\n }\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n .align-items-xxl-center {\n align-items: center !important;\n }\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n .align-content-xxl-center {\n align-content: center !important;\n }\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n .align-self-xxl-center {\n align-self: center !important;\n }\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n .order-xxl-first {\n order: -1 !important;\n }\n .order-xxl-0 {\n order: 0 !important;\n }\n .order-xxl-1 {\n order: 1 !important;\n }\n .order-xxl-2 {\n order: 2 !important;\n }\n .order-xxl-3 {\n order: 3 !important;\n }\n .order-xxl-4 {\n order: 4 !important;\n }\n .order-xxl-5 {\n order: 5 !important;\n }\n .order-xxl-last {\n order: 6 !important;\n }\n .m-xxl-0 {\n margin: 0 !important;\n }\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n .m-xxl-3 {\n margin: 1rem !important;\n }\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n .m-xxl-5 {\n margin: 3rem !important;\n }\n .m-xxl-auto {\n margin: auto !important;\n }\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n .me-xxl-auto {\n margin-right: auto !important;\n }\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n .p-xxl-0 {\n padding: 0 !important;\n }\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n .p-xxl-3 {\n padding: 1rem !important;\n }\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n .p-xxl-5 {\n padding: 3rem !important;\n }\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-grid {\n display: grid !important;\n }\n .d-print-inline-grid {\n display: inline-grid !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl xxl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @if not $n {\n @error \"breakpoint `#{$name}` not found in `#{$breakpoints}`\";\n }\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width.\n// The maximum value is reduced by 0.02px to work around the limitations of\n// `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $max: map-get($breakpoints, $name);\n @return if($max and $max > 0, $max - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $next: breakpoint-next($name, $breakpoints);\n $max: breakpoint-max($next, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($next, $breakpoints) {\n @content;\n }\n }\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n// scss-docs-start gray-color-variables\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n// scss-docs-end gray-color-variables\n\n// fusv-disable\n// scss-docs-start gray-colors-map\n$grays: (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n) !default;\n// scss-docs-end gray-colors-map\n// fusv-enable\n\n// scss-docs-start color-variables\n$blue: #0d6efd !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #d63384 !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #198754 !default;\n$teal: #20c997 !default;\n$cyan: #0dcaf0 !default;\n// scss-docs-end color-variables\n\n// scss-docs-start colors-map\n$colors: (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"black\": $black,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n) !default;\n// scss-docs-end colors-map\n\n// The contrast ratio to reach against white, to determine if color changes from \"light\" to \"dark\". Acceptable values for WCAG 2.2 are 3, 4.5 and 7.\n// See https://www.w3.org/TR/WCAG/#contrast-minimum\n$min-contrast-ratio: 4.5 !default;\n\n// Customize the light and dark text colors for use in our color contrast function.\n$color-contrast-dark: $black !default;\n$color-contrast-light: $white !default;\n\n// fusv-disable\n$blue-100: tint-color($blue, 80%) !default;\n$blue-200: tint-color($blue, 60%) !default;\n$blue-300: tint-color($blue, 40%) !default;\n$blue-400: tint-color($blue, 20%) !default;\n$blue-500: $blue !default;\n$blue-600: shade-color($blue, 20%) !default;\n$blue-700: shade-color($blue, 40%) !default;\n$blue-800: shade-color($blue, 60%) !default;\n$blue-900: shade-color($blue, 80%) !default;\n\n$indigo-100: tint-color($indigo, 80%) !default;\n$indigo-200: tint-color($indigo, 60%) !default;\n$indigo-300: tint-color($indigo, 40%) !default;\n$indigo-400: tint-color($indigo, 20%) !default;\n$indigo-500: $indigo !default;\n$indigo-600: shade-color($indigo, 20%) !default;\n$indigo-700: shade-color($indigo, 40%) !default;\n$indigo-800: shade-color($indigo, 60%) !default;\n$indigo-900: shade-color($indigo, 80%) !default;\n\n$purple-100: tint-color($purple, 80%) !default;\n$purple-200: tint-color($purple, 60%) !default;\n$purple-300: tint-color($purple, 40%) !default;\n$purple-400: tint-color($purple, 20%) !default;\n$purple-500: $purple !default;\n$purple-600: shade-color($purple, 20%) !default;\n$purple-700: shade-color($purple, 40%) !default;\n$purple-800: shade-color($purple, 60%) !default;\n$purple-900: shade-color($purple, 80%) !default;\n\n$pink-100: tint-color($pink, 80%) !default;\n$pink-200: tint-color($pink, 60%) !default;\n$pink-300: tint-color($pink, 40%) !default;\n$pink-400: tint-color($pink, 20%) !default;\n$pink-500: $pink !default;\n$pink-600: shade-color($pink, 20%) !default;\n$pink-700: shade-color($pink, 40%) !default;\n$pink-800: shade-color($pink, 60%) !default;\n$pink-900: shade-color($pink, 80%) !default;\n\n$red-100: tint-color($red, 80%) !default;\n$red-200: tint-color($red, 60%) !default;\n$red-300: tint-color($red, 40%) !default;\n$red-400: tint-color($red, 20%) !default;\n$red-500: $red !default;\n$red-600: shade-color($red, 20%) !default;\n$red-700: shade-color($red, 40%) !default;\n$red-800: shade-color($red, 60%) !default;\n$red-900: shade-color($red, 80%) !default;\n\n$orange-100: tint-color($orange, 80%) !default;\n$orange-200: tint-color($orange, 60%) !default;\n$orange-300: tint-color($orange, 40%) !default;\n$orange-400: tint-color($orange, 20%) !default;\n$orange-500: $orange !default;\n$orange-600: shade-color($orange, 20%) !default;\n$orange-700: shade-color($orange, 40%) !default;\n$orange-800: shade-color($orange, 60%) !default;\n$orange-900: shade-color($orange, 80%) !default;\n\n$yellow-100: tint-color($yellow, 80%) !default;\n$yellow-200: tint-color($yellow, 60%) !default;\n$yellow-300: tint-color($yellow, 40%) !default;\n$yellow-400: tint-color($yellow, 20%) !default;\n$yellow-500: $yellow !default;\n$yellow-600: shade-color($yellow, 20%) !default;\n$yellow-700: shade-color($yellow, 40%) !default;\n$yellow-800: shade-color($yellow, 60%) !default;\n$yellow-900: shade-color($yellow, 80%) !default;\n\n$green-100: tint-color($green, 80%) !default;\n$green-200: tint-color($green, 60%) !default;\n$green-300: tint-color($green, 40%) !default;\n$green-400: tint-color($green, 20%) !default;\n$green-500: $green !default;\n$green-600: shade-color($green, 20%) !default;\n$green-700: shade-color($green, 40%) !default;\n$green-800: shade-color($green, 60%) !default;\n$green-900: shade-color($green, 80%) !default;\n\n$teal-100: tint-color($teal, 80%) !default;\n$teal-200: tint-color($teal, 60%) !default;\n$teal-300: tint-color($teal, 40%) !default;\n$teal-400: tint-color($teal, 20%) !default;\n$teal-500: $teal !default;\n$teal-600: shade-color($teal, 20%) !default;\n$teal-700: shade-color($teal, 40%) !default;\n$teal-800: shade-color($teal, 60%) !default;\n$teal-900: shade-color($teal, 80%) !default;\n\n$cyan-100: tint-color($cyan, 80%) !default;\n$cyan-200: tint-color($cyan, 60%) !default;\n$cyan-300: tint-color($cyan, 40%) !default;\n$cyan-400: tint-color($cyan, 20%) !default;\n$cyan-500: $cyan !default;\n$cyan-600: shade-color($cyan, 20%) !default;\n$cyan-700: shade-color($cyan, 40%) !default;\n$cyan-800: shade-color($cyan, 60%) !default;\n$cyan-900: shade-color($cyan, 80%) !default;\n\n$blues: (\n \"blue-100\": $blue-100,\n \"blue-200\": $blue-200,\n \"blue-300\": $blue-300,\n \"blue-400\": $blue-400,\n \"blue-500\": $blue-500,\n \"blue-600\": $blue-600,\n \"blue-700\": $blue-700,\n \"blue-800\": $blue-800,\n \"blue-900\": $blue-900\n) !default;\n\n$indigos: (\n \"indigo-100\": $indigo-100,\n \"indigo-200\": $indigo-200,\n \"indigo-300\": $indigo-300,\n \"indigo-400\": $indigo-400,\n \"indigo-500\": $indigo-500,\n \"indigo-600\": $indigo-600,\n \"indigo-700\": $indigo-700,\n \"indigo-800\": $indigo-800,\n \"indigo-900\": $indigo-900\n) !default;\n\n$purples: (\n \"purple-100\": $purple-100,\n \"purple-200\": $purple-200,\n \"purple-300\": $purple-300,\n \"purple-400\": $purple-400,\n \"purple-500\": $purple-500,\n \"purple-600\": $purple-600,\n \"purple-700\": $purple-700,\n \"purple-800\": $purple-800,\n \"purple-900\": $purple-900\n) !default;\n\n$pinks: (\n \"pink-100\": $pink-100,\n \"pink-200\": $pink-200,\n \"pink-300\": $pink-300,\n \"pink-400\": $pink-400,\n \"pink-500\": $pink-500,\n \"pink-600\": $pink-600,\n \"pink-700\": $pink-700,\n \"pink-800\": $pink-800,\n \"pink-900\": $pink-900\n) !default;\n\n$reds: (\n \"red-100\": $red-100,\n \"red-200\": $red-200,\n \"red-300\": $red-300,\n \"red-400\": $red-400,\n \"red-500\": $red-500,\n \"red-600\": $red-600,\n \"red-700\": $red-700,\n \"red-800\": $red-800,\n \"red-900\": $red-900\n) !default;\n\n$oranges: (\n \"orange-100\": $orange-100,\n \"orange-200\": $orange-200,\n \"orange-300\": $orange-300,\n \"orange-400\": $orange-400,\n \"orange-500\": $orange-500,\n \"orange-600\": $orange-600,\n \"orange-700\": $orange-700,\n \"orange-800\": $orange-800,\n \"orange-900\": $orange-900\n) !default;\n\n$yellows: (\n \"yellow-100\": $yellow-100,\n \"yellow-200\": $yellow-200,\n \"yellow-300\": $yellow-300,\n \"yellow-400\": $yellow-400,\n \"yellow-500\": $yellow-500,\n \"yellow-600\": $yellow-600,\n \"yellow-700\": $yellow-700,\n \"yellow-800\": $yellow-800,\n \"yellow-900\": $yellow-900\n) !default;\n\n$greens: (\n \"green-100\": $green-100,\n \"green-200\": $green-200,\n \"green-300\": $green-300,\n \"green-400\": $green-400,\n \"green-500\": $green-500,\n \"green-600\": $green-600,\n \"green-700\": $green-700,\n \"green-800\": $green-800,\n \"green-900\": $green-900\n) !default;\n\n$teals: (\n \"teal-100\": $teal-100,\n \"teal-200\": $teal-200,\n \"teal-300\": $teal-300,\n \"teal-400\": $teal-400,\n \"teal-500\": $teal-500,\n \"teal-600\": $teal-600,\n \"teal-700\": $teal-700,\n \"teal-800\": $teal-800,\n \"teal-900\": $teal-900\n) !default;\n\n$cyans: (\n \"cyan-100\": $cyan-100,\n \"cyan-200\": $cyan-200,\n \"cyan-300\": $cyan-300,\n \"cyan-400\": $cyan-400,\n \"cyan-500\": $cyan-500,\n \"cyan-600\": $cyan-600,\n \"cyan-700\": $cyan-700,\n \"cyan-800\": $cyan-800,\n \"cyan-900\": $cyan-900\n) !default;\n// fusv-enable\n\n// scss-docs-start theme-color-variables\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-900 !default;\n// scss-docs-end theme-color-variables\n\n// scss-docs-start theme-colors-map\n$theme-colors: (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n) !default;\n// scss-docs-end theme-colors-map\n\n// scss-docs-start theme-text-variables\n$primary-text-emphasis: shade-color($primary, 60%) !default;\n$secondary-text-emphasis: shade-color($secondary, 60%) !default;\n$success-text-emphasis: shade-color($success, 60%) !default;\n$info-text-emphasis: shade-color($info, 60%) !default;\n$warning-text-emphasis: shade-color($warning, 60%) !default;\n$danger-text-emphasis: shade-color($danger, 60%) !default;\n$light-text-emphasis: $gray-700 !default;\n$dark-text-emphasis: $gray-700 !default;\n// scss-docs-end theme-text-variables\n\n// scss-docs-start theme-bg-subtle-variables\n$primary-bg-subtle: tint-color($primary, 80%) !default;\n$secondary-bg-subtle: tint-color($secondary, 80%) !default;\n$success-bg-subtle: tint-color($success, 80%) !default;\n$info-bg-subtle: tint-color($info, 80%) !default;\n$warning-bg-subtle: tint-color($warning, 80%) !default;\n$danger-bg-subtle: tint-color($danger, 80%) !default;\n$light-bg-subtle: mix($gray-100, $white) !default;\n$dark-bg-subtle: $gray-400 !default;\n// scss-docs-end theme-bg-subtle-variables\n\n// scss-docs-start theme-border-subtle-variables\n$primary-border-subtle: tint-color($primary, 60%) !default;\n$secondary-border-subtle: tint-color($secondary, 60%) !default;\n$success-border-subtle: tint-color($success, 60%) !default;\n$info-border-subtle: tint-color($info, 60%) !default;\n$warning-border-subtle: tint-color($warning, 60%) !default;\n$danger-border-subtle: tint-color($danger, 60%) !default;\n$light-border-subtle: $gray-200 !default;\n$dark-border-subtle: $gray-500 !default;\n// scss-docs-end theme-border-subtle-variables\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\", \"%3c\"),\n (\">\", \"%3e\"),\n (\"#\", \"%23\"),\n (\"(\", \"%28\"),\n (\")\", \"%29\"),\n) !default;\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-reduced-motion: true !default;\n$enable-smooth-scroll: true !default;\n$enable-grid-classes: true !default;\n$enable-container-classes: true !default;\n$enable-cssgrid: false !default;\n$enable-button-pointers: true !default;\n$enable-rfs: true !default;\n$enable-validation-icons: true !default;\n$enable-negative-margins: false !default;\n$enable-deprecation-messages: true !default;\n$enable-important-utilities: true !default;\n\n$enable-dark-mode: true !default;\n$color-mode-type: data !default; // `data` or `media-query`\n\n// Prefix for :root CSS variables\n\n$variable-prefix: bs- !default; // Deprecated in v5.2.0 for the shorter `$prefix`\n$prefix: $variable-prefix !default;\n\n// Gradient\n//\n// The gradient which is added to components if `$enable-gradients` is `true`\n// This gradient is also added to elements with `.bg-gradient`\n// scss-docs-start variable-gradient\n$gradient: linear-gradient(180deg, rgba($white, .15), rgba($white, 0)) !default;\n// scss-docs-end variable-gradient\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n// scss-docs-start spacer-variables-maps\n$spacer: 1rem !default;\n$spacers: (\n 0: 0,\n 1: $spacer * .25,\n 2: $spacer * .5,\n 3: $spacer,\n 4: $spacer * 1.5,\n 5: $spacer * 3,\n) !default;\n// scss-docs-end spacer-variables-maps\n\n// Position\n//\n// Define the edge positioning anchors of the position utilities.\n\n// scss-docs-start position-map\n$position-values: (\n 0: 0,\n 50: 50%,\n 100: 100%\n) !default;\n// scss-docs-end position-map\n\n// Body\n//\n// Settings for the `` element.\n\n$body-text-align: null !default;\n$body-color: $gray-900 !default;\n$body-bg: $white !default;\n\n$body-secondary-color: rgba($body-color, .75) !default;\n$body-secondary-bg: $gray-200 !default;\n\n$body-tertiary-color: rgba($body-color, .5) !default;\n$body-tertiary-bg: $gray-100 !default;\n\n$body-emphasis-color: $black !default;\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: $primary !default;\n$link-decoration: underline !default;\n$link-shade-percentage: 20% !default;\n$link-hover-color: shift-color($link-color, $link-shade-percentage) !default;\n$link-hover-decoration: null !default;\n\n$stretched-link-pseudo-element: after !default;\n$stretched-link-z-index: 1 !default;\n\n// Icon links\n// scss-docs-start icon-link-variables\n$icon-link-gap: .375rem !default;\n$icon-link-underline-offset: .25em !default;\n$icon-link-icon-size: 1em !default;\n$icon-link-icon-transition: .2s ease-in-out transform !default;\n$icon-link-icon-transform: translate3d(.25em, 0, 0) !default;\n// scss-docs-end icon-link-variables\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n// scss-docs-start grid-breakpoints\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px,\n xxl: 1400px\n) !default;\n// scss-docs-end grid-breakpoints\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n// scss-docs-start container-max-widths\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px,\n xxl: 1320px\n) !default;\n// scss-docs-end container-max-widths\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 1.5rem !default;\n$grid-row-columns: 6 !default;\n\n// Container padding\n\n$container-padding-x: $grid-gutter-width !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n// scss-docs-start border-variables\n$border-width: 1px !default;\n$border-widths: (\n 1: 1px,\n 2: 2px,\n 3: 3px,\n 4: 4px,\n 5: 5px\n) !default;\n$border-style: solid !default;\n$border-color: $gray-300 !default;\n$border-color-translucent: rgba($black, .175) !default;\n// scss-docs-end border-variables\n\n// scss-docs-start border-radius-variables\n$border-radius: .375rem !default;\n$border-radius-sm: .25rem !default;\n$border-radius-lg: .5rem !default;\n$border-radius-xl: 1rem !default;\n$border-radius-xxl: 2rem !default;\n$border-radius-pill: 50rem !default;\n// scss-docs-end border-radius-variables\n// fusv-disable\n$border-radius-2xl: $border-radius-xxl !default; // Deprecated in v5.3.0\n// fusv-enable\n\n// scss-docs-start box-shadow-variables\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n$box-shadow-inset: inset 0 1px 2px rgba($black, .075) !default;\n// scss-docs-end box-shadow-variables\n\n$component-active-color: $white !default;\n$component-active-bg: $primary !default;\n\n// scss-docs-start focus-ring-variables\n$focus-ring-width: .25rem !default;\n$focus-ring-opacity: .25 !default;\n$focus-ring-color: rgba($primary, $focus-ring-opacity) !default;\n$focus-ring-blur: 0 !default;\n$focus-ring-box-shadow: 0 0 $focus-ring-blur $focus-ring-width $focus-ring-color !default;\n// scss-docs-end focus-ring-variables\n\n// scss-docs-start caret-variables\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n// scss-docs-end caret-variables\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n// scss-docs-start collapse-transition\n$transition-collapse: height .35s ease !default;\n$transition-collapse-width: width .35s ease !default;\n// scss-docs-end collapse-transition\n\n// stylelint-disable function-disallowed-list\n// scss-docs-start aspect-ratios\n$aspect-ratios: (\n \"1x1\": 100%,\n \"4x3\": calc(3 / 4 * 100%),\n \"16x9\": calc(9 / 16 * 100%),\n \"21x9\": calc(9 / 21 * 100%)\n) !default;\n// scss-docs-end aspect-ratios\n// stylelint-enable function-disallowed-list\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// scss-docs-start font-variables\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", \"Liberation Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n// stylelint-enable value-keyword-case\n$font-family-base: var(--#{$prefix}font-sans-serif) !default;\n$font-family-code: var(--#{$prefix}font-monospace) !default;\n\n// $font-size-root affects the value of `rem`, which is used for as well font sizes, paddings, and margins\n// $font-size-base affects the font size of the body text\n$font-size-root: null !default;\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-sm: $font-size-base * .875 !default;\n$font-size-lg: $font-size-base * 1.25 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-medium: 500 !default;\n$font-weight-semibold: 600 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n\n$line-height-base: 1.5 !default;\n$line-height-sm: 1.25 !default;\n$line-height-lg: 2 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n// scss-docs-end font-variables\n\n// scss-docs-start font-sizes\n$font-sizes: (\n 1: $h1-font-size,\n 2: $h2-font-size,\n 3: $h3-font-size,\n 4: $h4-font-size,\n 5: $h5-font-size,\n 6: $h6-font-size\n) !default;\n// scss-docs-end font-sizes\n\n// scss-docs-start headings-variables\n$headings-margin-bottom: $spacer * .5 !default;\n$headings-font-family: null !default;\n$headings-font-style: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: inherit !default;\n// scss-docs-end headings-variables\n\n// scss-docs-start display-headings\n$display-font-sizes: (\n 1: 5rem,\n 2: 4.5rem,\n 3: 4rem,\n 4: 3.5rem,\n 5: 3rem,\n 6: 2.5rem\n) !default;\n\n$display-font-family: null !default;\n$display-font-style: null !default;\n$display-font-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n// scss-docs-end display-headings\n\n// scss-docs-start type-variables\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: .875em !default;\n\n$sub-sup-font-size: .75em !default;\n\n// fusv-disable\n$text-muted: var(--#{$prefix}secondary-color) !default; // Deprecated in 5.3.0\n// fusv-enable\n\n$initialism-font-size: $small-font-size !default;\n\n$blockquote-margin-y: $spacer !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n$blockquote-footer-color: $gray-600 !default;\n$blockquote-footer-font-size: $small-font-size !default;\n\n$hr-margin-y: $spacer !default;\n$hr-color: inherit !default;\n\n// fusv-disable\n$hr-bg-color: null !default; // Deprecated in v5.2.0\n$hr-height: null !default; // Deprecated in v5.2.0\n// fusv-enable\n\n$hr-border-color: null !default; // Allows for inherited colors\n$hr-border-width: var(--#{$prefix}border-width) !default;\n$hr-opacity: .25 !default;\n\n// scss-docs-start vr-variables\n$vr-border-width: var(--#{$prefix}border-width) !default;\n// scss-docs-end vr-variables\n\n$legend-margin-bottom: .5rem !default;\n$legend-font-size: 1.5rem !default;\n$legend-font-weight: null !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-padding: .1875em !default;\n$mark-color: $body-color !default;\n$mark-bg: $yellow-100 !default;\n// scss-docs-end type-variables\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n// scss-docs-start table-variables\n$table-cell-padding-y: .5rem !default;\n$table-cell-padding-x: .5rem !default;\n$table-cell-padding-y-sm: .25rem !default;\n$table-cell-padding-x-sm: .25rem !default;\n\n$table-cell-vertical-align: top !default;\n\n$table-color: var(--#{$prefix}emphasis-color) !default;\n$table-bg: var(--#{$prefix}body-bg) !default;\n$table-accent-bg: transparent !default;\n\n$table-th-font-weight: null !default;\n\n$table-striped-color: $table-color !default;\n$table-striped-bg-factor: .05 !default;\n$table-striped-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-striped-bg-factor) !default;\n\n$table-active-color: $table-color !default;\n$table-active-bg-factor: .1 !default;\n$table-active-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-active-bg-factor) !default;\n\n$table-hover-color: $table-color !default;\n$table-hover-bg-factor: .075 !default;\n$table-hover-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-hover-bg-factor) !default;\n\n$table-border-factor: .2 !default;\n$table-border-width: var(--#{$prefix}border-width) !default;\n$table-border-color: var(--#{$prefix}border-color) !default;\n\n$table-striped-order: odd !default;\n$table-striped-columns-order: even !default;\n\n$table-group-separator-color: currentcolor !default;\n\n$table-caption-color: var(--#{$prefix}secondary-color) !default;\n\n$table-bg-scale: -80% !default;\n// scss-docs-end table-variables\n\n// scss-docs-start table-loop\n$table-variants: (\n \"primary\": shift-color($primary, $table-bg-scale),\n \"secondary\": shift-color($secondary, $table-bg-scale),\n \"success\": shift-color($success, $table-bg-scale),\n \"info\": shift-color($info, $table-bg-scale),\n \"warning\": shift-color($warning, $table-bg-scale),\n \"danger\": shift-color($danger, $table-bg-scale),\n \"light\": $light,\n \"dark\": $dark,\n) !default;\n// scss-docs-end table-loop\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n// scss-docs-start input-btn-variables\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: $focus-ring-width !default;\n$input-btn-focus-color-opacity: $focus-ring-opacity !default;\n$input-btn-focus-color: $focus-ring-color !default;\n$input-btn-focus-blur: $focus-ring-blur !default;\n$input-btn-focus-box-shadow: $focus-ring-box-shadow !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n\n$input-btn-border-width: var(--#{$prefix}border-width) !default;\n// scss-docs-end input-btn-variables\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n// scss-docs-start btn-variables\n$btn-color: var(--#{$prefix}body-color) !default;\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-color: var(--#{$prefix}link-color) !default;\n$btn-link-hover-color: var(--#{$prefix}link-hover-color) !default;\n$btn-link-disabled-color: $gray-600 !default;\n$btn-link-focus-shadow-rgb: to-rgb(mix(color-contrast($link-color), $link-color, 15%)) !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: var(--#{$prefix}border-radius) !default;\n$btn-border-radius-sm: var(--#{$prefix}border-radius-sm) !default;\n$btn-border-radius-lg: var(--#{$prefix}border-radius-lg) !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$btn-hover-bg-shade-amount: 15% !default;\n$btn-hover-bg-tint-amount: 15% !default;\n$btn-hover-border-shade-amount: 20% !default;\n$btn-hover-border-tint-amount: 10% !default;\n$btn-active-bg-shade-amount: 20% !default;\n$btn-active-bg-tint-amount: 20% !default;\n$btn-active-border-shade-amount: 25% !default;\n$btn-active-border-tint-amount: 10% !default;\n// scss-docs-end btn-variables\n\n\n// Forms\n\n// scss-docs-start form-text-variables\n$form-text-margin-top: .25rem !default;\n$form-text-font-size: $small-font-size !default;\n$form-text-font-style: null !default;\n$form-text-font-weight: null !default;\n$form-text-color: var(--#{$prefix}secondary-color) !default;\n// scss-docs-end form-text-variables\n\n// scss-docs-start form-label-variables\n$form-label-margin-bottom: .5rem !default;\n$form-label-font-size: null !default;\n$form-label-font-style: null !default;\n$form-label-font-weight: null !default;\n$form-label-color: null !default;\n// scss-docs-end form-label-variables\n\n// scss-docs-start form-input-variables\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n\n$input-bg: var(--#{$prefix}body-bg) !default;\n$input-disabled-color: null !default;\n$input-disabled-bg: var(--#{$prefix}secondary-bg) !default;\n$input-disabled-border-color: null !default;\n\n$input-color: var(--#{$prefix}body-color) !default;\n$input-border-color: var(--#{$prefix}border-color) !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: var(--#{$prefix}box-shadow-inset) !default;\n\n$input-border-radius: var(--#{$prefix}border-radius) !default;\n$input-border-radius-sm: var(--#{$prefix}border-radius-sm) !default;\n$input-border-radius-lg: var(--#{$prefix}border-radius-lg) !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: tint-color($component-active-bg, 50%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: var(--#{$prefix}secondary-color) !default;\n$input-plaintext-color: var(--#{$prefix}body-color) !default;\n\n$input-height-border: calc(#{$input-border-width} * 2) !default; // stylelint-disable-line function-disallowed-list\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y * .5) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-color-width: 3rem !default;\n// scss-docs-end form-input-variables\n\n// scss-docs-start form-check-variables\n$form-check-input-width: 1em !default;\n$form-check-min-height: $font-size-base * $line-height-base !default;\n$form-check-padding-start: $form-check-input-width + .5em !default;\n$form-check-margin-bottom: .125rem !default;\n$form-check-label-color: null !default;\n$form-check-label-cursor: null !default;\n$form-check-transition: null !default;\n\n$form-check-input-active-filter: brightness(90%) !default;\n\n$form-check-input-bg: $input-bg !default;\n$form-check-input-border: var(--#{$prefix}border-width) solid var(--#{$prefix}border-color) !default;\n$form-check-input-border-radius: .25em !default;\n$form-check-radio-border-radius: 50% !default;\n$form-check-input-focus-border: $input-focus-border-color !default;\n$form-check-input-focus-box-shadow: $focus-ring-box-shadow !default;\n\n$form-check-input-checked-color: $component-active-color !default;\n$form-check-input-checked-bg-color: $component-active-bg !default;\n$form-check-input-checked-border-color: $form-check-input-checked-bg-color !default;\n$form-check-input-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-check-radio-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-check-input-indeterminate-color: $component-active-color !default;\n$form-check-input-indeterminate-bg-color: $component-active-bg !default;\n$form-check-input-indeterminate-border-color: $form-check-input-indeterminate-bg-color !default;\n$form-check-input-indeterminate-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-check-input-disabled-opacity: .5 !default;\n$form-check-label-disabled-opacity: $form-check-input-disabled-opacity !default;\n$form-check-btn-check-disabled-opacity: $btn-disabled-opacity !default;\n\n$form-check-inline-margin-end: 1rem !default;\n// scss-docs-end form-check-variables\n\n// scss-docs-start form-switch-variables\n$form-switch-color: rgba($black, .25) !default;\n$form-switch-width: 2em !default;\n$form-switch-padding-start: $form-switch-width + .5em !default;\n$form-switch-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-switch-border-radius: $form-switch-width !default;\n$form-switch-transition: background-position .15s ease-in-out !default;\n\n$form-switch-focus-color: $input-focus-border-color !default;\n$form-switch-focus-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-switch-checked-color: $component-active-color !default;\n$form-switch-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-switch-checked-bg-position: right center !default;\n// scss-docs-end form-switch-variables\n\n// scss-docs-start input-group-variables\n$input-group-addon-padding-y: $input-padding-y !default;\n$input-group-addon-padding-x: $input-padding-x !default;\n$input-group-addon-font-weight: $input-font-weight !default;\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: var(--#{$prefix}tertiary-bg) !default;\n$input-group-addon-border-color: $input-border-color !default;\n// scss-docs-end input-group-variables\n\n// scss-docs-start form-select-variables\n$form-select-padding-y: $input-padding-y !default;\n$form-select-padding-x: $input-padding-x !default;\n$form-select-font-family: $input-font-family !default;\n$form-select-font-size: $input-font-size !default;\n$form-select-indicator-padding: $form-select-padding-x * 3 !default; // Extra padding for background-image\n$form-select-font-weight: $input-font-weight !default;\n$form-select-line-height: $input-line-height !default;\n$form-select-color: $input-color !default;\n$form-select-bg: $input-bg !default;\n$form-select-disabled-color: null !default;\n$form-select-disabled-bg: $input-disabled-bg !default;\n$form-select-disabled-border-color: $input-disabled-border-color !default;\n$form-select-bg-position: right $form-select-padding-x center !default;\n$form-select-bg-size: 16px 12px !default; // In pixels because image dimensions\n$form-select-indicator-color: $gray-800 !default;\n$form-select-indicator: url(\"data:image/svg+xml,\") !default;\n\n$form-select-feedback-icon-padding-end: $form-select-padding-x * 2.5 + $form-select-indicator-padding !default;\n$form-select-feedback-icon-position: center right $form-select-indicator-padding !default;\n$form-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$form-select-border-width: $input-border-width !default;\n$form-select-border-color: $input-border-color !default;\n$form-select-border-radius: $input-border-radius !default;\n$form-select-box-shadow: var(--#{$prefix}box-shadow-inset) !default;\n\n$form-select-focus-border-color: $input-focus-border-color !default;\n$form-select-focus-width: $input-focus-width !default;\n$form-select-focus-box-shadow: 0 0 0 $form-select-focus-width $input-btn-focus-color !default;\n\n$form-select-padding-y-sm: $input-padding-y-sm !default;\n$form-select-padding-x-sm: $input-padding-x-sm !default;\n$form-select-font-size-sm: $input-font-size-sm !default;\n$form-select-border-radius-sm: $input-border-radius-sm !default;\n\n$form-select-padding-y-lg: $input-padding-y-lg !default;\n$form-select-padding-x-lg: $input-padding-x-lg !default;\n$form-select-font-size-lg: $input-font-size-lg !default;\n$form-select-border-radius-lg: $input-border-radius-lg !default;\n\n$form-select-transition: $input-transition !default;\n// scss-docs-end form-select-variables\n\n// scss-docs-start form-range-variables\n$form-range-track-width: 100% !default;\n$form-range-track-height: .5rem !default;\n$form-range-track-cursor: pointer !default;\n$form-range-track-bg: var(--#{$prefix}secondary-bg) !default;\n$form-range-track-border-radius: 1rem !default;\n$form-range-track-box-shadow: var(--#{$prefix}box-shadow-inset) !default;\n\n$form-range-thumb-width: 1rem !default;\n$form-range-thumb-height: $form-range-thumb-width !default;\n$form-range-thumb-bg: $component-active-bg !default;\n$form-range-thumb-border: 0 !default;\n$form-range-thumb-border-radius: 1rem !default;\n$form-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$form-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$form-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in Edge\n$form-range-thumb-active-bg: tint-color($component-active-bg, 70%) !default;\n$form-range-thumb-disabled-bg: var(--#{$prefix}secondary-color) !default;\n$form-range-thumb-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n// scss-docs-end form-range-variables\n\n// scss-docs-start form-file-variables\n$form-file-button-color: $input-color !default;\n$form-file-button-bg: var(--#{$prefix}tertiary-bg) !default;\n$form-file-button-hover-bg: var(--#{$prefix}secondary-bg) !default;\n// scss-docs-end form-file-variables\n\n// scss-docs-start form-floating-variables\n$form-floating-height: add(3.5rem, $input-height-border) !default;\n$form-floating-line-height: 1.25 !default;\n$form-floating-padding-x: $input-padding-x !default;\n$form-floating-padding-y: 1rem !default;\n$form-floating-input-padding-t: 1.625rem !default;\n$form-floating-input-padding-b: .625rem !default;\n$form-floating-label-height: 1.5em !default;\n$form-floating-label-opacity: .65 !default;\n$form-floating-label-transform: scale(.85) translateY(-.5rem) translateX(.15rem) !default;\n$form-floating-label-disabled-color: $gray-600 !default;\n$form-floating-transition: opacity .1s ease-in-out, transform .1s ease-in-out !default;\n// scss-docs-end form-floating-variables\n\n// Form validation\n\n// scss-docs-start form-feedback-variables\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $form-text-font-size !default;\n$form-feedback-font-style: $form-text-font-style !default;\n$form-feedback-valid-color: $success !default;\n$form-feedback-invalid-color: $danger !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n// scss-docs-end form-feedback-variables\n\n// scss-docs-start form-validation-colors\n$form-valid-color: $form-feedback-valid-color !default;\n$form-valid-border-color: $form-feedback-valid-color !default;\n$form-invalid-color: $form-feedback-invalid-color !default;\n$form-invalid-border-color: $form-feedback-invalid-color !default;\n// scss-docs-end form-validation-colors\n\n// scss-docs-start form-validation-states\n$form-validation-states: (\n \"valid\": (\n \"color\": var(--#{$prefix}form-valid-color),\n \"icon\": $form-feedback-icon-valid,\n \"tooltip-color\": #fff,\n \"tooltip-bg-color\": var(--#{$prefix}success),\n \"focus-box-shadow\": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}success-rgb), $input-btn-focus-color-opacity),\n \"border-color\": var(--#{$prefix}form-valid-border-color),\n ),\n \"invalid\": (\n \"color\": var(--#{$prefix}form-invalid-color),\n \"icon\": $form-feedback-icon-invalid,\n \"tooltip-color\": #fff,\n \"tooltip-bg-color\": var(--#{$prefix}danger),\n \"focus-box-shadow\": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}danger-rgb), $input-btn-focus-color-opacity),\n \"border-color\": var(--#{$prefix}form-invalid-border-color),\n )\n) !default;\n// scss-docs-end form-validation-states\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n// scss-docs-start zindex-stack\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-offcanvas-backdrop: 1040 !default;\n$zindex-offcanvas: 1045 !default;\n$zindex-modal-backdrop: 1050 !default;\n$zindex-modal: 1055 !default;\n$zindex-popover: 1070 !default;\n$zindex-tooltip: 1080 !default;\n$zindex-toast: 1090 !default;\n// scss-docs-end zindex-stack\n\n// scss-docs-start zindex-levels-map\n$zindex-levels: (\n n1: -1,\n 0: 0,\n 1: 1,\n 2: 2,\n 3: 3\n) !default;\n// scss-docs-end zindex-levels-map\n\n\n// Navs\n\n// scss-docs-start nav-variables\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-font-size: null !default;\n$nav-link-font-weight: null !default;\n$nav-link-color: var(--#{$prefix}link-color) !default;\n$nav-link-hover-color: var(--#{$prefix}link-hover-color) !default;\n$nav-link-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out !default;\n$nav-link-disabled-color: var(--#{$prefix}secondary-color) !default;\n$nav-link-focus-box-shadow: $focus-ring-box-shadow !default;\n\n$nav-tabs-border-color: var(--#{$prefix}border-color) !default;\n$nav-tabs-border-width: var(--#{$prefix}border-width) !default;\n$nav-tabs-border-radius: var(--#{$prefix}border-radius) !default;\n$nav-tabs-link-hover-border-color: var(--#{$prefix}secondary-bg) var(--#{$prefix}secondary-bg) $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: var(--#{$prefix}emphasis-color) !default;\n$nav-tabs-link-active-bg: var(--#{$prefix}body-bg) !default;\n$nav-tabs-link-active-border-color: var(--#{$prefix}border-color) var(--#{$prefix}border-color) $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: var(--#{$prefix}border-radius) !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-underline-gap: 1rem !default;\n$nav-underline-border-width: .125rem !default;\n$nav-underline-link-active-color: var(--#{$prefix}emphasis-color) !default;\n// scss-docs-end nav-variables\n\n\n// Navbar\n\n// scss-docs-start navbar-variables\n$navbar-padding-y: $spacer * .5 !default;\n$navbar-padding-x: null !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) * .5 !default;\n$navbar-brand-margin-end: 1rem !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n$navbar-toggler-focus-width: $btn-focus-width !default;\n$navbar-toggler-transition: box-shadow .15s ease-in-out !default;\n\n$navbar-light-color: rgba(var(--#{$prefix}emphasis-color-rgb), .65) !default;\n$navbar-light-hover-color: rgba(var(--#{$prefix}emphasis-color-rgb), .8) !default;\n$navbar-light-active-color: rgba(var(--#{$prefix}emphasis-color-rgb), 1) !default;\n$navbar-light-disabled-color: rgba(var(--#{$prefix}emphasis-color-rgb), .3) !default;\n$navbar-light-icon-color: rgba($body-color, .75) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba(var(--#{$prefix}emphasis-color-rgb), .15) !default;\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n// scss-docs-end navbar-variables\n\n// scss-docs-start navbar-dark-variables\n$navbar-dark-color: rgba($white, .55) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-icon-color: $navbar-dark-color !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n// scss-docs-end navbar-dark-variables\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n// scss-docs-start dropdown-variables\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-x: 0 !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: var(--#{$prefix}body-color) !default;\n$dropdown-bg: var(--#{$prefix}body-bg) !default;\n$dropdown-border-color: var(--#{$prefix}border-color-translucent) !default;\n$dropdown-border-radius: var(--#{$prefix}border-radius) !default;\n$dropdown-border-width: var(--#{$prefix}border-width) !default;\n$dropdown-inner-border-radius: calc(#{$dropdown-border-radius} - #{$dropdown-border-width}) !default; // stylelint-disable-line function-disallowed-list\n$dropdown-divider-bg: $dropdown-border-color !default;\n$dropdown-divider-margin-y: $spacer * .5 !default;\n$dropdown-box-shadow: var(--#{$prefix}box-shadow) !default;\n\n$dropdown-link-color: var(--#{$prefix}body-color) !default;\n$dropdown-link-hover-color: $dropdown-link-color !default;\n$dropdown-link-hover-bg: var(--#{$prefix}tertiary-bg) !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: var(--#{$prefix}tertiary-color) !default;\n\n$dropdown-item-padding-y: $spacer * .25 !default;\n$dropdown-item-padding-x: $spacer !default;\n\n$dropdown-header-color: $gray-600 !default;\n$dropdown-header-padding-x: $dropdown-item-padding-x !default;\n$dropdown-header-padding-y: $dropdown-padding-y !default;\n// fusv-disable\n$dropdown-header-padding: $dropdown-header-padding-y $dropdown-header-padding-x !default; // Deprecated in v5.2.0\n// fusv-enable\n// scss-docs-end dropdown-variables\n\n// scss-docs-start dropdown-dark-variables\n$dropdown-dark-color: $gray-300 !default;\n$dropdown-dark-bg: $gray-800 !default;\n$dropdown-dark-border-color: $dropdown-border-color !default;\n$dropdown-dark-divider-bg: $dropdown-divider-bg !default;\n$dropdown-dark-box-shadow: null !default;\n$dropdown-dark-link-color: $dropdown-dark-color !default;\n$dropdown-dark-link-hover-color: $white !default;\n$dropdown-dark-link-hover-bg: rgba($white, .15) !default;\n$dropdown-dark-link-active-color: $dropdown-link-active-color !default;\n$dropdown-dark-link-active-bg: $dropdown-link-active-bg !default;\n$dropdown-dark-link-disabled-color: $gray-500 !default;\n$dropdown-dark-header-color: $gray-500 !default;\n// scss-docs-end dropdown-dark-variables\n\n\n// Pagination\n\n// scss-docs-start pagination-variables\n$pagination-padding-y: .375rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n\n$pagination-font-size: $font-size-base !default;\n\n$pagination-color: var(--#{$prefix}link-color) !default;\n$pagination-bg: var(--#{$prefix}body-bg) !default;\n$pagination-border-radius: var(--#{$prefix}border-radius) !default;\n$pagination-border-width: var(--#{$prefix}border-width) !default;\n$pagination-margin-start: calc(-1 * #{$pagination-border-width}) !default; // stylelint-disable-line function-disallowed-list\n$pagination-border-color: var(--#{$prefix}border-color) !default;\n\n$pagination-focus-color: var(--#{$prefix}link-hover-color) !default;\n$pagination-focus-bg: var(--#{$prefix}secondary-bg) !default;\n$pagination-focus-box-shadow: $focus-ring-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: var(--#{$prefix}link-hover-color) !default;\n$pagination-hover-bg: var(--#{$prefix}tertiary-bg) !default;\n$pagination-hover-border-color: var(--#{$prefix}border-color) !default; // Todo in v6: remove this?\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $component-active-bg !default;\n\n$pagination-disabled-color: var(--#{$prefix}secondary-color) !default;\n$pagination-disabled-bg: var(--#{$prefix}secondary-bg) !default;\n$pagination-disabled-border-color: var(--#{$prefix}border-color) !default;\n\n$pagination-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$pagination-border-radius-sm: var(--#{$prefix}border-radius-sm) !default;\n$pagination-border-radius-lg: var(--#{$prefix}border-radius-lg) !default;\n// scss-docs-end pagination-variables\n\n\n// Placeholders\n\n// scss-docs-start placeholders\n$placeholder-opacity-max: .5 !default;\n$placeholder-opacity-min: .2 !default;\n// scss-docs-end placeholders\n\n// Cards\n\n// scss-docs-start card-variables\n$card-spacer-y: $spacer !default;\n$card-spacer-x: $spacer !default;\n$card-title-spacer-y: $spacer * .5 !default;\n$card-title-color: null !default;\n$card-subtitle-color: null !default;\n$card-border-width: var(--#{$prefix}border-width) !default;\n$card-border-color: var(--#{$prefix}border-color-translucent) !default;\n$card-border-radius: var(--#{$prefix}border-radius) !default;\n$card-box-shadow: null !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-padding-y: $card-spacer-y * .5 !default;\n$card-cap-padding-x: $card-spacer-x !default;\n$card-cap-bg: rgba(var(--#{$prefix}body-color-rgb), .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: var(--#{$prefix}body-bg) !default;\n$card-img-overlay-padding: $spacer !default;\n$card-group-margin: $grid-gutter-width * .5 !default;\n// scss-docs-end card-variables\n\n// Accordion\n\n// scss-docs-start accordion-variables\n$accordion-padding-y: 1rem !default;\n$accordion-padding-x: 1.25rem !default;\n$accordion-color: var(--#{$prefix}body-color) !default;\n$accordion-bg: var(--#{$prefix}body-bg) !default;\n$accordion-border-width: var(--#{$prefix}border-width) !default;\n$accordion-border-color: var(--#{$prefix}border-color) !default;\n$accordion-border-radius: var(--#{$prefix}border-radius) !default;\n$accordion-inner-border-radius: subtract($accordion-border-radius, $accordion-border-width) !default;\n\n$accordion-body-padding-y: $accordion-padding-y !default;\n$accordion-body-padding-x: $accordion-padding-x !default;\n\n$accordion-button-padding-y: $accordion-padding-y !default;\n$accordion-button-padding-x: $accordion-padding-x !default;\n$accordion-button-color: var(--#{$prefix}body-color) !default;\n$accordion-button-bg: var(--#{$prefix}accordion-bg) !default;\n$accordion-transition: $btn-transition, border-radius .15s ease !default;\n$accordion-button-active-bg: var(--#{$prefix}primary-bg-subtle) !default;\n$accordion-button-active-color: var(--#{$prefix}primary-text-emphasis) !default;\n\n// fusv-disable\n$accordion-button-focus-border-color: $input-focus-border-color !default; // Deprecated in v5.3.3\n// fusv-enable\n$accordion-button-focus-box-shadow: $btn-focus-box-shadow !default;\n\n$accordion-icon-width: 1.25rem !default;\n$accordion-icon-color: $body-color !default;\n$accordion-icon-active-color: $primary-text-emphasis !default;\n$accordion-icon-transition: transform .2s ease-in-out !default;\n$accordion-icon-transform: rotate(-180deg) !default;\n\n$accordion-button-icon: url(\"data:image/svg+xml,\") !default;\n$accordion-button-active-icon: url(\"data:image/svg+xml,\") !default;\n// scss-docs-end accordion-variables\n\n// Tooltips\n\n// scss-docs-start tooltip-variables\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: var(--#{$prefix}body-bg) !default;\n$tooltip-bg: var(--#{$prefix}emphasis-color) !default;\n$tooltip-border-radius: var(--#{$prefix}border-radius) !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: $spacer * .25 !default;\n$tooltip-padding-x: $spacer * .5 !default;\n$tooltip-margin: null !default; // TODO: remove this in v6\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n// fusv-disable\n$tooltip-arrow-color: null !default; // Deprecated in Bootstrap 5.2.0 for CSS variables\n// fusv-enable\n// scss-docs-end tooltip-variables\n\n// Form tooltips must come after regular tooltips\n// scss-docs-start tooltip-feedback-variables\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: null !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n// scss-docs-end tooltip-feedback-variables\n\n\n// Popovers\n\n// scss-docs-start popover-variables\n$popover-font-size: $font-size-sm !default;\n$popover-bg: var(--#{$prefix}body-bg) !default;\n$popover-max-width: 276px !default;\n$popover-border-width: var(--#{$prefix}border-width) !default;\n$popover-border-color: var(--#{$prefix}border-color-translucent) !default;\n$popover-border-radius: var(--#{$prefix}border-radius-lg) !default;\n$popover-inner-border-radius: calc(#{$popover-border-radius} - #{$popover-border-width}) !default; // stylelint-disable-line function-disallowed-list\n$popover-box-shadow: var(--#{$prefix}box-shadow) !default;\n\n$popover-header-font-size: $font-size-base !default;\n$popover-header-bg: var(--#{$prefix}secondary-bg) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: $spacer !default;\n\n$popover-body-color: var(--#{$prefix}body-color) !default;\n$popover-body-padding-y: $spacer !default;\n$popover-body-padding-x: $spacer !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n// scss-docs-end popover-variables\n\n// fusv-disable\n// Deprecated in Bootstrap 5.2.0 for CSS variables\n$popover-arrow-color: $popover-bg !default;\n$popover-arrow-outer-color: var(--#{$prefix}border-color-translucent) !default;\n// fusv-enable\n\n\n// Toasts\n\n// scss-docs-start toast-variables\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .5rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default;\n$toast-border-width: var(--#{$prefix}border-width) !default;\n$toast-border-color: var(--#{$prefix}border-color-translucent) !default;\n$toast-border-radius: var(--#{$prefix}border-radius) !default;\n$toast-box-shadow: var(--#{$prefix}box-shadow) !default;\n$toast-spacing: $container-padding-x !default;\n\n$toast-header-color: var(--#{$prefix}secondary-color) !default;\n$toast-header-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default;\n$toast-header-border-color: $toast-border-color !default;\n// scss-docs-end toast-variables\n\n\n// Badges\n\n// scss-docs-start badge-variables\n$badge-font-size: .75em !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-color: $white !default;\n$badge-padding-y: .35em !default;\n$badge-padding-x: .65em !default;\n$badge-border-radius: var(--#{$prefix}border-radius) !default;\n// scss-docs-end badge-variables\n\n\n// Modals\n\n// scss-docs-start modal-variables\n$modal-inner-padding: $spacer !default;\n\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: var(--#{$prefix}body-color) !default;\n$modal-content-bg: var(--#{$prefix}body-bg) !default;\n$modal-content-border-color: var(--#{$prefix}border-color-translucent) !default;\n$modal-content-border-width: var(--#{$prefix}border-width) !default;\n$modal-content-border-radius: var(--#{$prefix}border-radius-lg) !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: var(--#{$prefix}box-shadow-sm) !default;\n$modal-content-box-shadow-sm-up: var(--#{$prefix}box-shadow) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n\n$modal-header-border-color: var(--#{$prefix}border-color) !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-header-padding-y: $modal-inner-padding !default;\n$modal-header-padding-x: $modal-inner-padding !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-footer-bg: null !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n\n$modal-sm: 300px !default;\n$modal-md: 500px !default;\n$modal-lg: 800px !default;\n$modal-xl: 1140px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n// scss-docs-end modal-variables\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n// scss-docs-start alert-variables\n$alert-padding-y: $spacer !default;\n$alert-padding-x: $spacer !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: var(--#{$prefix}border-radius) !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: var(--#{$prefix}border-width) !default;\n$alert-dismissible-padding-r: $alert-padding-x * 3 !default; // 3x covers width of x plus default padding on either side\n// scss-docs-end alert-variables\n\n// fusv-disable\n$alert-bg-scale: -80% !default; // Deprecated in v5.2.0, to be removed in v6\n$alert-border-scale: -70% !default; // Deprecated in v5.2.0, to be removed in v6\n$alert-color-scale: 40% !default; // Deprecated in v5.2.0, to be removed in v6\n// fusv-enable\n\n// Progress bars\n\n// scss-docs-start progress-variables\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: var(--#{$prefix}secondary-bg) !default;\n$progress-border-radius: var(--#{$prefix}border-radius) !default;\n$progress-box-shadow: var(--#{$prefix}box-shadow-inset) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: $primary !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n// scss-docs-end progress-variables\n\n\n// List group\n\n// scss-docs-start list-group-variables\n$list-group-color: var(--#{$prefix}body-color) !default;\n$list-group-bg: var(--#{$prefix}body-bg) !default;\n$list-group-border-color: var(--#{$prefix}border-color) !default;\n$list-group-border-width: var(--#{$prefix}border-width) !default;\n$list-group-border-radius: var(--#{$prefix}border-radius) !default;\n\n$list-group-item-padding-y: $spacer * .5 !default;\n$list-group-item-padding-x: $spacer !default;\n// fusv-disable\n$list-group-item-bg-scale: -80% !default; // Deprecated in v5.3.0\n$list-group-item-color-scale: 40% !default; // Deprecated in v5.3.0\n// fusv-enable\n\n$list-group-hover-bg: var(--#{$prefix}tertiary-bg) !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: var(--#{$prefix}secondary-color) !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: var(--#{$prefix}secondary-color) !default;\n$list-group-action-hover-color: var(--#{$prefix}emphasis-color) !default;\n\n$list-group-action-active-color: var(--#{$prefix}body-color) !default;\n$list-group-action-active-bg: var(--#{$prefix}secondary-bg) !default;\n// scss-docs-end list-group-variables\n\n\n// Image thumbnails\n\n// scss-docs-start thumbnail-variables\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: var(--#{$prefix}body-bg) !default;\n$thumbnail-border-width: var(--#{$prefix}border-width) !default;\n$thumbnail-border-color: var(--#{$prefix}border-color) !default;\n$thumbnail-border-radius: var(--#{$prefix}border-radius) !default;\n$thumbnail-box-shadow: var(--#{$prefix}box-shadow-sm) !default;\n// scss-docs-end thumbnail-variables\n\n\n// Figures\n\n// scss-docs-start figure-variables\n$figure-caption-font-size: $small-font-size !default;\n$figure-caption-color: var(--#{$prefix}secondary-color) !default;\n// scss-docs-end figure-variables\n\n\n// Breadcrumbs\n\n// scss-docs-start breadcrumb-variables\n$breadcrumb-font-size: null !default;\n$breadcrumb-padding-y: 0 !default;\n$breadcrumb-padding-x: 0 !default;\n$breadcrumb-item-padding-x: .5rem !default;\n$breadcrumb-margin-bottom: 1rem !default;\n$breadcrumb-bg: null !default;\n$breadcrumb-divider-color: var(--#{$prefix}secondary-color) !default;\n$breadcrumb-active-color: var(--#{$prefix}secondary-color) !default;\n$breadcrumb-divider: quote(\"/\") !default;\n$breadcrumb-divider-flipped: $breadcrumb-divider !default;\n$breadcrumb-border-radius: null !default;\n// scss-docs-end breadcrumb-variables\n\n// Carousel\n\n// scss-docs-start carousel-variables\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n$carousel-control-icon-filter: null !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-opacity: .5 !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-active-opacity: 1 !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n$carousel-caption-padding-y: 1.25rem !default;\n$carousel-caption-spacer: 1.25rem !default;\n\n$carousel-control-icon-width: 2rem !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n// scss-docs-end carousel-variables\n\n// scss-docs-start carousel-dark-variables\n$carousel-dark-indicator-active-bg: $black !default; // Deprecated in v5.3.4\n$carousel-dark-caption-color: $black !default; // Deprecated in v5.3.4\n$carousel-dark-control-icon-filter: invert(1) grayscale(100) !default; // Deprecated in v5.3.4\n// scss-docs-end carousel-dark-variables\n\n\n// Spinners\n\n// scss-docs-start spinner-variables\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-vertical-align: -.125em !default;\n$spinner-border-width: .25em !default;\n$spinner-animation-speed: .75s !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n// scss-docs-end spinner-variables\n\n\n// Close\n\n// scss-docs-start close-variables\n$btn-close-width: 1em !default;\n$btn-close-height: $btn-close-width !default;\n$btn-close-padding-x: .25em !default;\n$btn-close-padding-y: $btn-close-padding-x !default;\n$btn-close-color: $black !default;\n$btn-close-bg: url(\"data:image/svg+xml,\") !default;\n$btn-close-focus-shadow: $focus-ring-box-shadow !default;\n$btn-close-opacity: .5 !default;\n$btn-close-hover-opacity: .75 !default;\n$btn-close-focus-opacity: 1 !default;\n$btn-close-disabled-opacity: .25 !default;\n$btn-close-filter: null !default;\n$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default; // Deprecated in v5.3.4\n// scss-docs-end close-variables\n\n\n// Offcanvas\n\n// scss-docs-start offcanvas-variables\n$offcanvas-padding-y: $modal-inner-padding !default;\n$offcanvas-padding-x: $modal-inner-padding !default;\n$offcanvas-horizontal-width: 400px !default;\n$offcanvas-vertical-height: 30vh !default;\n$offcanvas-transition-duration: .3s !default;\n$offcanvas-border-color: $modal-content-border-color !default;\n$offcanvas-border-width: $modal-content-border-width !default;\n$offcanvas-title-line-height: $modal-title-line-height !default;\n$offcanvas-bg-color: var(--#{$prefix}body-bg) !default;\n$offcanvas-color: var(--#{$prefix}body-color) !default;\n$offcanvas-box-shadow: $modal-content-box-shadow-xs !default;\n$offcanvas-backdrop-bg: $modal-backdrop-bg !default;\n$offcanvas-backdrop-opacity: $modal-backdrop-opacity !default;\n// scss-docs-end offcanvas-variables\n\n// Code\n\n$code-font-size: $small-font-size !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .1875rem !default;\n$kbd-padding-x: .375rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: var(--#{$prefix}body-bg) !default;\n$kbd-bg: var(--#{$prefix}body-color) !default;\n$nested-kbd-font-weight: null !default; // Deprecated in v5.2.0, removing in v6\n\n$pre-color: null !default;\n\n@import \"variables-dark\"; // TODO: can be removed safely in v6, only here to avoid breaking changes in v5.3\n","// Row\n//\n// Rows contain your columns.\n\n:root {\n @each $name, $value in $grid-breakpoints {\n --#{$prefix}breakpoint-#{$name}: #{$value};\n }\n}\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n\n > * {\n @include make-col-ready();\n }\n }\n}\n\n@if $enable-cssgrid {\n .grid {\n display: grid;\n grid-template-rows: repeat(var(--#{$prefix}rows, 1), 1fr);\n grid-template-columns: repeat(var(--#{$prefix}columns, #{$grid-columns}), 1fr);\n gap: var(--#{$prefix}gap, #{$grid-gutter-width});\n\n @include make-cssgrid();\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-row($gutter: $grid-gutter-width) {\n --#{$prefix}gutter-x: #{$gutter};\n --#{$prefix}gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n // TODO: Revisit calc order after https://github.com/react-bootstrap/react-bootstrap/issues/6039 is fixed\n margin-top: calc(-1 * var(--#{$prefix}gutter-y)); // stylelint-disable-line function-disallowed-list\n margin-right: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list\n margin-left: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list\n}\n\n@mixin make-col-ready() {\n // Add box sizing if only the grid is loaded\n box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null);\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we set the width\n // later on to override this initial width.\n flex-shrink: 0;\n width: 100%;\n max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid\n padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-top: var(--#{$prefix}gutter-y);\n}\n\n@mixin make-col($size: false, $columns: $grid-columns) {\n @if $size {\n flex: 0 0 auto;\n width: percentage(divide($size, $columns));\n\n } @else {\n flex: 1 1 0;\n max-width: 100%;\n }\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: divide($size, $columns);\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// number of columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 auto;\n width: percentage(divide(1, $count));\n }\n}\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex: 1 0 0;\n }\n\n .row-cols#{$infix}-auto > * {\n @include make-col-auto();\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n\n // Gutters\n //\n // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns.\n @each $key, $value in $gutters {\n .g#{$infix}-#{$key},\n .gx#{$infix}-#{$key} {\n --#{$prefix}gutter-x: #{$value};\n }\n\n .g#{$infix}-#{$key},\n .gy#{$infix}-#{$key} {\n --#{$prefix}gutter-y: #{$value};\n }\n }\n }\n }\n}\n\n@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .g-col#{$infix}-#{$i} {\n grid-column: auto / span $i;\n }\n }\n\n // Start with `1` because `0` is an invalid value.\n // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible.\n @for $i from 1 through ($columns - 1) {\n .g-start#{$infix}-#{$i} {\n grid-column-start: $i;\n }\n }\n }\n }\n }\n}\n","// Utility generator\n// Used to generate utilities & print utilities\n@mixin generate-utility($utility, $infix: \"\", $is-rfs-media-query: false) {\n $values: map-get($utility, values);\n\n // If the values are a list or string, convert it into a map\n @if type-of($values) == \"string\" or type-of(nth($values, 1)) != \"list\" {\n $values: zip($values, $values);\n }\n\n @each $key, $value in $values {\n $properties: map-get($utility, property);\n\n // Multiple properties are possible, for example with vertical or horizontal margins or paddings\n @if type-of($properties) == \"string\" {\n $properties: append((), $properties);\n }\n\n // Use custom class if present\n $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));\n $property-class: if($property-class == null, \"\", $property-class);\n\n // Use custom CSS variable name if present, otherwise default to `class`\n $css-variable-name: if(map-has-key($utility, css-variable-name), map-get($utility, css-variable-name), map-get($utility, class));\n\n // State params to generate pseudo-classes\n $state: if(map-has-key($utility, state), map-get($utility, state), ());\n\n $infix: if($property-class == \"\" and str-slice($infix, 1, 1) == \"-\", str-slice($infix, 2), $infix);\n\n // Don't prefix if value key is null (e.g. with shadow class)\n $property-class-modifier: if($key, if($property-class == \"\" and $infix == \"\", \"\", \"-\") + $key, \"\");\n\n @if map-get($utility, rfs) {\n // Inside the media query\n @if $is-rfs-media-query {\n $val: rfs-value($value);\n\n // Do not render anything if fluid and non fluid values are the same\n $value: if($val == rfs-fluid-value($value), null, $val);\n }\n @else {\n $value: rfs-fluid-value($value);\n }\n }\n\n $is-css-var: map-get($utility, css-var);\n $is-local-vars: map-get($utility, local-vars);\n $is-rtl: map-get($utility, rtl);\n\n @if $value != null {\n @if $is-rtl == false {\n /* rtl:begin:remove */\n }\n\n @if $is-css-var {\n .#{$property-class + $infix + $property-class-modifier} {\n --#{$prefix}#{$css-variable-name}: #{$value};\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n --#{$prefix}#{$css-variable-name}: #{$value};\n }\n }\n } @else {\n .#{$property-class + $infix + $property-class-modifier} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $variable in $is-local-vars {\n --#{$prefix}#{$local-var}: #{$variable};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $variable in $is-local-vars {\n --#{$prefix}#{$local-var}: #{$variable};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n }\n }\n\n @if $is-rtl == false {\n /* rtl:end:remove */\n }\n }\n }\n}\n","// Loop over each breakpoint\n@each $breakpoint in map-keys($grid-breakpoints) {\n\n // Generate media query if needed\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix);\n }\n }\n }\n}\n\n// RFS rescaling\n@media (min-width: $rfs-mq-value) {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) {\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix, true);\n }\n }\n }\n }\n}\n\n\n// Print utilities\n@media print {\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Then check if the utility needs print styles\n @if type-of($utility) == \"map\" and map-get($utility, print) == true {\n @include generate-utility($utility, \"-print\");\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css new file mode 100644 index 0000000..82bb1bc --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap Grid v5.3.8 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}@media (min-width:576px){.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}}@media (min-width:768px){.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}}@media (min-width:992px){.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}}@media (min-width:1200px){.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}}@media (min-width:1400px){.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap-grid.min.css.map */ \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map new file mode 100644 index 0000000..45041a9 --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_containers.scss","dist/css/bootstrap-grid.css","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"AACE;;;;ACKA,WCAF,iBAGA,cACA,cACA,cAHA,cADA,eCJE,cAAA,OACA,cAAA,EACA,MAAA,KACA,cAAA,8BACA,aAAA,8BACA,aAAA,KACA,YAAA,KCsDE,yBH5CE,WAAA,cACE,UAAA,OG2CJ,yBH5CE,WAAA,cAAA,cACE,UAAA,OG2CJ,yBH5CE,WAAA,cAAA,cAAA,cACE,UAAA,OG2CJ,0BH5CE,WAAA,cAAA,cAAA,cAAA,cACE,UAAA,QG2CJ,0BH5CE,WAAA,cAAA,cAAA,cAAA,cAAA,eACE,UAAA,QIhBR,MAEI,mBAAA,EAAA,mBAAA,MAAA,mBAAA,MAAA,mBAAA,MAAA,mBAAA,OAAA,oBAAA,OAKF,KCNA,cAAA,OACA,cAAA,EACA,QAAA,KACA,UAAA,KAEA,WAAA,8BACA,aAAA,+BACA,YAAA,+BDEE,OCGF,WAAA,WAIA,YAAA,EACA,MAAA,KACA,UAAA,KACA,cAAA,8BACA,aAAA,8BACA,WAAA,mBA+CI,KACE,KAAA,EAAA,EAAA,EAGF,iBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,cACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,UAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,UAxDV,YAAA,YAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,IAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,IAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,aAwDU,UAxDV,YAAA,IAwDU,WAxDV,YAAA,aAwDU,WAxDV,YAAA,aAmEM,KJ6GR,MI3GU,cAAA,EAGF,KJ6GR,MI3GU,cAAA,EAPF,KJuHR,MIrHU,cAAA,QAGF,KJuHR,MIrHU,cAAA,QAPF,KJiIR,MI/HU,cAAA,OAGF,KJiIR,MI/HU,cAAA,OAPF,KJ2IR,MIzIU,cAAA,KAGF,KJ2IR,MIzIU,cAAA,KAPF,KJqJR,MInJU,cAAA,OAGF,KJqJR,MInJU,cAAA,OAPF,KJ+JR,MI7JU,cAAA,KAGF,KJ+JR,MI7JU,cAAA,KF1DN,yBEUE,QACE,KAAA,EAAA,EAAA,EAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,YAAA,EAwDU,aAxDV,YAAA,YAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAmEM,QJiSN,SI/RQ,cAAA,EAGF,QJgSN,SI9RQ,cAAA,EAPF,QJySN,SIvSQ,cAAA,QAGF,QJwSN,SItSQ,cAAA,QAPF,QJiTN,SI/SQ,cAAA,OAGF,QJgTN,SI9SQ,cAAA,OAPF,QJyTN,SIvTQ,cAAA,KAGF,QJwTN,SItTQ,cAAA,KAPF,QJiUN,SI/TQ,cAAA,OAGF,QJgUN,SI9TQ,cAAA,OAPF,QJyUN,SIvUQ,cAAA,KAGF,QJwUN,SItUQ,cAAA,MF1DN,yBEUE,QACE,KAAA,EAAA,EAAA,EAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,YAAA,EAwDU,aAxDV,YAAA,YAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAmEM,QJ0cN,SIxcQ,cAAA,EAGF,QJycN,SIvcQ,cAAA,EAPF,QJkdN,SIhdQ,cAAA,QAGF,QJidN,SI/cQ,cAAA,QAPF,QJ0dN,SIxdQ,cAAA,OAGF,QJydN,SIvdQ,cAAA,OAPF,QJkeN,SIheQ,cAAA,KAGF,QJieN,SI/dQ,cAAA,KAPF,QJ0eN,SIxeQ,cAAA,OAGF,QJyeN,SIveQ,cAAA,OAPF,QJkfN,SIhfQ,cAAA,KAGF,QJifN,SI/eQ,cAAA,MF1DN,yBEUE,QACE,KAAA,EAAA,EAAA,EAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,YAAA,EAwDU,aAxDV,YAAA,YAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAmEM,QJmnBN,SIjnBQ,cAAA,EAGF,QJknBN,SIhnBQ,cAAA,EAPF,QJ2nBN,SIznBQ,cAAA,QAGF,QJ0nBN,SIxnBQ,cAAA,QAPF,QJmoBN,SIjoBQ,cAAA,OAGF,QJkoBN,SIhoBQ,cAAA,OAPF,QJ2oBN,SIzoBQ,cAAA,KAGF,QJ0oBN,SIxoBQ,cAAA,KAPF,QJmpBN,SIjpBQ,cAAA,OAGF,QJkpBN,SIhpBQ,cAAA,OAPF,QJ2pBN,SIzpBQ,cAAA,KAGF,QJ0pBN,SIxpBQ,cAAA,MF1DN,0BEUE,QACE,KAAA,EAAA,EAAA,EAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,YAAA,EAwDU,aAxDV,YAAA,YAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,aAwDU,aAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAmEM,QJ4xBN,SI1xBQ,cAAA,EAGF,QJ2xBN,SIzxBQ,cAAA,EAPF,QJoyBN,SIlyBQ,cAAA,QAGF,QJmyBN,SIjyBQ,cAAA,QAPF,QJ4yBN,SI1yBQ,cAAA,OAGF,QJ2yBN,SIzyBQ,cAAA,OAPF,QJozBN,SIlzBQ,cAAA,KAGF,QJmzBN,SIjzBQ,cAAA,KAPF,QJ4zBN,SI1zBQ,cAAA,OAGF,QJ2zBN,SIzzBQ,cAAA,OAPF,QJo0BN,SIl0BQ,cAAA,KAGF,QJm0BN,SIj0BQ,cAAA,MF1DN,0BEUE,SACE,KAAA,EAAA,EAAA,EAGF,qBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,cAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,cAxDV,YAAA,EAwDU,cAxDV,YAAA,YAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,IAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,aAwDU,cAxDV,YAAA,IAwDU,eAxDV,YAAA,aAwDU,eAxDV,YAAA,aAmEM,SJq8BN,UIn8BQ,cAAA,EAGF,SJo8BN,UIl8BQ,cAAA,EAPF,SJ68BN,UI38BQ,cAAA,QAGF,SJ48BN,UI18BQ,cAAA,QAPF,SJq9BN,UIn9BQ,cAAA,OAGF,SJo9BN,UIl9BQ,cAAA,OAPF,SJ69BN,UI39BQ,cAAA,KAGF,SJ49BN,UI19BQ,cAAA,KAPF,SJq+BN,UIn+BQ,cAAA,OAGF,SJo+BN,UIl+BQ,cAAA,OAPF,SJ6+BN,UI3+BQ,cAAA,KAGF,SJ4+BN,UI1+BQ,cAAA,MCvDF,UAOI,QAAA,iBAPJ,gBAOI,QAAA,uBAPJ,SAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,eAOI,QAAA,sBAPJ,SAOI,QAAA,gBAPJ,aAOI,QAAA,oBAPJ,cAOI,QAAA,qBAPJ,QAOI,QAAA,eAPJ,eAOI,QAAA,sBAPJ,QAOI,QAAA,eAPJ,WAOI,KAAA,EAAA,EAAA,eAPJ,UAOI,eAAA,cAPJ,aAOI,eAAA,iBAPJ,kBAOI,eAAA,sBAPJ,qBAOI,eAAA,yBAPJ,aAOI,UAAA,YAPJ,aAOI,UAAA,YAPJ,eAOI,YAAA,YAPJ,eAOI,YAAA,YAPJ,WAOI,UAAA,eAPJ,aAOI,UAAA,iBAPJ,mBAOI,UAAA,uBAPJ,uBAOI,gBAAA,qBAPJ,qBAOI,gBAAA,mBAPJ,wBAOI,gBAAA,iBAPJ,yBAOI,gBAAA,wBAPJ,wBAOI,gBAAA,uBAPJ,wBAOI,gBAAA,uBAPJ,mBAOI,YAAA,qBAPJ,iBAOI,YAAA,mBAPJ,oBAOI,YAAA,iBAPJ,sBAOI,YAAA,mBAPJ,qBAOI,YAAA,kBAPJ,qBAOI,cAAA,qBAPJ,mBAOI,cAAA,mBAPJ,sBAOI,cAAA,iBAPJ,uBAOI,cAAA,wBAPJ,sBAOI,cAAA,uBAPJ,uBAOI,cAAA,kBAPJ,iBAOI,WAAA,eAPJ,kBAOI,WAAA,qBAPJ,gBAOI,WAAA,mBAPJ,mBAOI,WAAA,iBAPJ,qBAOI,WAAA,mBAPJ,oBAOI,WAAA,kBAPJ,aAOI,MAAA,aAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,KAOI,OAAA,YAPJ,KAOI,OAAA,iBAPJ,KAOI,OAAA,gBAPJ,KAOI,OAAA,eAPJ,KAOI,OAAA,iBAPJ,KAOI,OAAA,eAPJ,QAOI,OAAA,eAPJ,MAOI,aAAA,YAAA,YAAA,YAPJ,MAOI,aAAA,iBAAA,YAAA,iBAPJ,MAOI,aAAA,gBAAA,YAAA,gBAPJ,MAOI,aAAA,eAAA,YAAA,eAPJ,MAOI,aAAA,iBAAA,YAAA,iBAPJ,MAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,MAOI,WAAA,YAAA,cAAA,YAPJ,MAOI,WAAA,iBAAA,cAAA,iBAPJ,MAOI,WAAA,gBAAA,cAAA,gBAPJ,MAOI,WAAA,eAAA,cAAA,eAPJ,MAOI,WAAA,iBAAA,cAAA,iBAPJ,MAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,MAOI,WAAA,YAPJ,MAOI,WAAA,iBAPJ,MAOI,WAAA,gBAPJ,MAOI,WAAA,eAPJ,MAOI,WAAA,iBAPJ,MAOI,WAAA,eAPJ,SAOI,WAAA,eAPJ,MAOI,aAAA,YAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,gBAPJ,MAOI,aAAA,eAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,eAPJ,SAOI,aAAA,eAPJ,MAOI,cAAA,YAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,gBAPJ,MAOI,cAAA,eAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,eAPJ,SAOI,cAAA,eAPJ,MAOI,YAAA,YAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,gBAPJ,MAOI,YAAA,eAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,eAPJ,SAOI,YAAA,eAPJ,KAOI,QAAA,YAPJ,KAOI,QAAA,iBAPJ,KAOI,QAAA,gBAPJ,KAOI,QAAA,eAPJ,KAOI,QAAA,iBAPJ,KAOI,QAAA,eAPJ,MAOI,cAAA,YAAA,aAAA,YAPJ,MAOI,cAAA,iBAAA,aAAA,iBAPJ,MAOI,cAAA,gBAAA,aAAA,gBAPJ,MAOI,cAAA,eAAA,aAAA,eAPJ,MAOI,cAAA,iBAAA,aAAA,iBAPJ,MAOI,cAAA,eAAA,aAAA,eAPJ,MAOI,YAAA,YAAA,eAAA,YAPJ,MAOI,YAAA,iBAAA,eAAA,iBAPJ,MAOI,YAAA,gBAAA,eAAA,gBAPJ,MAOI,YAAA,eAAA,eAAA,eAPJ,MAOI,YAAA,iBAAA,eAAA,iBAPJ,MAOI,YAAA,eAAA,eAAA,eAPJ,MAOI,YAAA,YAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,gBAPJ,MAOI,YAAA,eAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,eAPJ,MAOI,cAAA,YAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,gBAPJ,MAOI,cAAA,eAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,eAPJ,MAOI,eAAA,YAPJ,MAOI,eAAA,iBAPJ,MAOI,eAAA,gBAPJ,MAOI,eAAA,eAPJ,MAOI,eAAA,iBAPJ,MAOI,eAAA,eAPJ,MAOI,aAAA,YAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,gBAPJ,MAOI,aAAA,eAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,eHVR,yBGGI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,aAAA,YAAA,YAAA,YAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,gBAAA,YAAA,gBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,YAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,cAAA,YAAA,aAAA,YAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,gBAAA,aAAA,gBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBHVR,yBGGI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,aAAA,YAAA,YAAA,YAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,gBAAA,YAAA,gBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,YAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,cAAA,YAAA,aAAA,YAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,gBAAA,aAAA,gBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBHVR,yBGGI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,aAAA,YAAA,YAAA,YAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,gBAAA,YAAA,gBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,YAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,cAAA,YAAA,aAAA,YAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,gBAAA,aAAA,gBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBHVR,0BGGI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,aAAA,YAAA,YAAA,YAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,gBAAA,YAAA,gBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,aAAA,iBAAA,YAAA,iBAPJ,SAOI,aAAA,eAAA,YAAA,eAPJ,YAOI,aAAA,eAAA,YAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,cAAA,YAAA,aAAA,YAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,gBAAA,aAAA,gBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,cAAA,iBAAA,aAAA,iBAPJ,SAOI,cAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBHVR,0BGGI,cAOI,QAAA,iBAPJ,oBAOI,QAAA,uBAPJ,aAOI,QAAA,gBAPJ,YAOI,QAAA,eAPJ,mBAOI,QAAA,sBAPJ,aAOI,QAAA,gBAPJ,iBAOI,QAAA,oBAPJ,kBAOI,QAAA,qBAPJ,YAOI,QAAA,eAPJ,mBAOI,QAAA,sBAPJ,YAOI,QAAA,eAPJ,eAOI,KAAA,EAAA,EAAA,eAPJ,cAOI,eAAA,cAPJ,iBAOI,eAAA,iBAPJ,sBAOI,eAAA,sBAPJ,yBAOI,eAAA,yBAPJ,iBAOI,UAAA,YAPJ,iBAOI,UAAA,YAPJ,mBAOI,YAAA,YAPJ,mBAOI,YAAA,YAPJ,eAOI,UAAA,eAPJ,iBAOI,UAAA,iBAPJ,uBAOI,UAAA,uBAPJ,2BAOI,gBAAA,qBAPJ,yBAOI,gBAAA,mBAPJ,4BAOI,gBAAA,iBAPJ,6BAOI,gBAAA,wBAPJ,4BAOI,gBAAA,uBAPJ,4BAOI,gBAAA,uBAPJ,uBAOI,YAAA,qBAPJ,qBAOI,YAAA,mBAPJ,wBAOI,YAAA,iBAPJ,0BAOI,YAAA,mBAPJ,yBAOI,YAAA,kBAPJ,yBAOI,cAAA,qBAPJ,uBAOI,cAAA,mBAPJ,0BAOI,cAAA,iBAPJ,2BAOI,cAAA,wBAPJ,0BAOI,cAAA,uBAPJ,2BAOI,cAAA,kBAPJ,qBAOI,WAAA,eAPJ,sBAOI,WAAA,qBAPJ,oBAOI,WAAA,mBAPJ,uBAOI,WAAA,iBAPJ,yBAOI,WAAA,mBAPJ,wBAOI,WAAA,kBAPJ,iBAOI,MAAA,aAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,gBAOI,MAAA,YAPJ,SAOI,OAAA,YAPJ,SAOI,OAAA,iBAPJ,SAOI,OAAA,gBAPJ,SAOI,OAAA,eAPJ,SAOI,OAAA,iBAPJ,SAOI,OAAA,eAPJ,YAOI,OAAA,eAPJ,UAOI,aAAA,YAAA,YAAA,YAPJ,UAOI,aAAA,iBAAA,YAAA,iBAPJ,UAOI,aAAA,gBAAA,YAAA,gBAPJ,UAOI,aAAA,eAAA,YAAA,eAPJ,UAOI,aAAA,iBAAA,YAAA,iBAPJ,UAOI,aAAA,eAAA,YAAA,eAPJ,aAOI,aAAA,eAAA,YAAA,eAPJ,UAOI,WAAA,YAAA,cAAA,YAPJ,UAOI,WAAA,iBAAA,cAAA,iBAPJ,UAOI,WAAA,gBAAA,cAAA,gBAPJ,UAOI,WAAA,eAAA,cAAA,eAPJ,UAOI,WAAA,iBAAA,cAAA,iBAPJ,UAOI,WAAA,eAAA,cAAA,eAPJ,aAOI,WAAA,eAAA,cAAA,eAPJ,UAOI,WAAA,YAPJ,UAOI,WAAA,iBAPJ,UAOI,WAAA,gBAPJ,UAOI,WAAA,eAPJ,UAOI,WAAA,iBAPJ,UAOI,WAAA,eAPJ,aAOI,WAAA,eAPJ,UAOI,aAAA,YAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,gBAPJ,UAOI,aAAA,eAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,eAPJ,aAOI,aAAA,eAPJ,UAOI,cAAA,YAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBAPJ,UAOI,cAAA,eAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,eAPJ,aAOI,cAAA,eAPJ,UAOI,YAAA,YAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,gBAPJ,UAOI,YAAA,eAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,eAPJ,aAOI,YAAA,eAPJ,SAOI,QAAA,YAPJ,SAOI,QAAA,iBAPJ,SAOI,QAAA,gBAPJ,SAOI,QAAA,eAPJ,SAOI,QAAA,iBAPJ,SAOI,QAAA,eAPJ,UAOI,cAAA,YAAA,aAAA,YAPJ,UAOI,cAAA,iBAAA,aAAA,iBAPJ,UAOI,cAAA,gBAAA,aAAA,gBAPJ,UAOI,cAAA,eAAA,aAAA,eAPJ,UAOI,cAAA,iBAAA,aAAA,iBAPJ,UAOI,cAAA,eAAA,aAAA,eAPJ,UAOI,YAAA,YAAA,eAAA,YAPJ,UAOI,YAAA,iBAAA,eAAA,iBAPJ,UAOI,YAAA,gBAAA,eAAA,gBAPJ,UAOI,YAAA,eAAA,eAAA,eAPJ,UAOI,YAAA,iBAAA,eAAA,iBAPJ,UAOI,YAAA,eAAA,eAAA,eAPJ,UAOI,YAAA,YAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,gBAPJ,UAOI,YAAA,eAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,eAPJ,UAOI,cAAA,YAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBAPJ,UAOI,cAAA,eAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,eAPJ,UAOI,eAAA,YAPJ,UAOI,eAAA,iBAPJ,UAOI,eAAA,gBAPJ,UAOI,eAAA,eAPJ,UAOI,eAAA,iBAPJ,UAOI,eAAA,eAPJ,UAOI,aAAA,YAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,gBAPJ,UAOI,aAAA,eAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,gBCnCZ,aD4BQ,gBAOI,QAAA,iBAPJ,sBAOI,QAAA,uBAPJ,eAOI,QAAA,gBAPJ,cAOI,QAAA,eAPJ,qBAOI,QAAA,sBAPJ,eAOI,QAAA,gBAPJ,mBAOI,QAAA,oBAPJ,oBAOI,QAAA,qBAPJ,cAOI,QAAA,eAPJ,qBAOI,QAAA,sBAPJ,cAOI,QAAA","sourcesContent":["@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-container-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n","/*!\n * Bootstrap Grid v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n:root {\n --bs-breakpoint-xs: 0;\n --bs-breakpoint-sm: 576px;\n --bs-breakpoint-md: 768px;\n --bs-breakpoint-lg: 992px;\n --bs-breakpoint-xl: 1200px;\n --bs-breakpoint-xxl: 1400px;\n}\n\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(-1 * var(--bs-gutter-y));\n margin-right: calc(-0.5 * var(--bs-gutter-x));\n margin-left: calc(-0.5 * var(--bs-gutter-x));\n}\n.row > * {\n box-sizing: border-box;\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0;\n }\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n .g-sm-0,\n .gx-sm-0 {\n --bs-gutter-x: 0;\n }\n .g-sm-0,\n .gy-sm-0 {\n --bs-gutter-y: 0;\n }\n .g-sm-1,\n .gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-sm-1,\n .gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-sm-2,\n .gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-sm-2,\n .gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-sm-3,\n .gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n .g-sm-3,\n .gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n .g-sm-4,\n .gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-sm-4,\n .gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-sm-5,\n .gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n .g-sm-5,\n .gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0;\n }\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n .g-md-0,\n .gx-md-0 {\n --bs-gutter-x: 0;\n }\n .g-md-0,\n .gy-md-0 {\n --bs-gutter-y: 0;\n }\n .g-md-1,\n .gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-md-1,\n .gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-md-2,\n .gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-md-2,\n .gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-md-3,\n .gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n .g-md-3,\n .gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n .g-md-4,\n .gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-md-4,\n .gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-md-5,\n .gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n .g-md-5,\n .gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0;\n }\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n .g-lg-0,\n .gx-lg-0 {\n --bs-gutter-x: 0;\n }\n .g-lg-0,\n .gy-lg-0 {\n --bs-gutter-y: 0;\n }\n .g-lg-1,\n .gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-lg-1,\n .gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-lg-2,\n .gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-lg-2,\n .gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-lg-3,\n .gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n .g-lg-3,\n .gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n .g-lg-4,\n .gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-lg-4,\n .gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-lg-5,\n .gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n .g-lg-5,\n .gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0;\n }\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n .g-xl-0,\n .gx-xl-0 {\n --bs-gutter-x: 0;\n }\n .g-xl-0,\n .gy-xl-0 {\n --bs-gutter-y: 0;\n }\n .g-xl-1,\n .gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xl-1,\n .gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xl-2,\n .gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xl-2,\n .gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xl-3,\n .gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xl-3,\n .gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xl-4,\n .gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xl-4,\n .gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xl-5,\n .gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xl-5,\n .gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0;\n }\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xxl-0 {\n margin-left: 0;\n }\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xxl-3 {\n margin-left: 25%;\n }\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xxl-6 {\n margin-left: 50%;\n }\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xxl-9 {\n margin-left: 75%;\n }\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n .g-xxl-0,\n .gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n .g-xxl-0,\n .gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n .g-xxl-1,\n .gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xxl-1,\n .gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xxl-2,\n .gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xxl-2,\n .gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xxl-3,\n .gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xxl-3,\n .gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xxl-4,\n .gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xxl-4,\n .gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xxl-5,\n .gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xxl-5,\n .gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-inline-grid {\n display: inline-grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-grid {\n display: grid !important;\n }\n .d-sm-inline-grid {\n display: inline-grid !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n .d-sm-none {\n display: none !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n .order-sm-first {\n order: -1 !important;\n }\n .order-sm-0 {\n order: 0 !important;\n }\n .order-sm-1 {\n order: 1 !important;\n }\n .order-sm-2 {\n order: 2 !important;\n }\n .order-sm-3 {\n order: 3 !important;\n }\n .order-sm-4 {\n order: 4 !important;\n }\n .order-sm-5 {\n order: 5 !important;\n }\n .order-sm-last {\n order: 6 !important;\n }\n .m-sm-0 {\n margin: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n .mt-sm-auto {\n margin-top: auto !important;\n }\n .me-sm-0 {\n margin-right: 0 !important;\n }\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n .me-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n .ms-sm-auto {\n margin-left: auto !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 768px) {\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-grid {\n display: grid !important;\n }\n .d-md-inline-grid {\n display: inline-grid !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n .d-md-none {\n display: none !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n .order-md-first {\n order: -1 !important;\n }\n .order-md-0 {\n order: 0 !important;\n }\n .order-md-1 {\n order: 1 !important;\n }\n .order-md-2 {\n order: 2 !important;\n }\n .order-md-3 {\n order: 3 !important;\n }\n .order-md-4 {\n order: 4 !important;\n }\n .order-md-5 {\n order: 5 !important;\n }\n .order-md-last {\n order: 6 !important;\n }\n .m-md-0 {\n margin: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-md-0 {\n margin-top: 0 !important;\n }\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n .mt-md-auto {\n margin-top: auto !important;\n }\n .me-md-0 {\n margin-right: 0 !important;\n }\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n .me-md-3 {\n margin-right: 1rem !important;\n }\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n .me-md-5 {\n margin-right: 3rem !important;\n }\n .me-md-auto {\n margin-right: auto !important;\n }\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n .ms-md-0 {\n margin-left: 0 !important;\n }\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n .ms-md-auto {\n margin-left: auto !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-md-0 {\n padding-top: 0 !important;\n }\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n .pe-md-0 {\n padding-right: 0 !important;\n }\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n .ps-md-0 {\n padding-left: 0 !important;\n }\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 992px) {\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-grid {\n display: grid !important;\n }\n .d-lg-inline-grid {\n display: inline-grid !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n .d-lg-none {\n display: none !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n .order-lg-first {\n order: -1 !important;\n }\n .order-lg-0 {\n order: 0 !important;\n }\n .order-lg-1 {\n order: 1 !important;\n }\n .order-lg-2 {\n order: 2 !important;\n }\n .order-lg-3 {\n order: 3 !important;\n }\n .order-lg-4 {\n order: 4 !important;\n }\n .order-lg-5 {\n order: 5 !important;\n }\n .order-lg-last {\n order: 6 !important;\n }\n .m-lg-0 {\n margin: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n .mt-lg-auto {\n margin-top: auto !important;\n }\n .me-lg-0 {\n margin-right: 0 !important;\n }\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n .me-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n .ms-lg-auto {\n margin-left: auto !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1200px) {\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-grid {\n display: grid !important;\n }\n .d-xl-inline-grid {\n display: inline-grid !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n .d-xl-none {\n display: none !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n .order-xl-first {\n order: -1 !important;\n }\n .order-xl-0 {\n order: 0 !important;\n }\n .order-xl-1 {\n order: 1 !important;\n }\n .order-xl-2 {\n order: 2 !important;\n }\n .order-xl-3 {\n order: 3 !important;\n }\n .order-xl-4 {\n order: 4 !important;\n }\n .order-xl-5 {\n order: 5 !important;\n }\n .order-xl-last {\n order: 6 !important;\n }\n .m-xl-0 {\n margin: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n .mt-xl-auto {\n margin-top: auto !important;\n }\n .me-xl-0 {\n margin-right: 0 !important;\n }\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n .me-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n .ms-xl-auto {\n margin-left: auto !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1400px) {\n .d-xxl-inline {\n display: inline !important;\n }\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n .d-xxl-block {\n display: block !important;\n }\n .d-xxl-grid {\n display: grid !important;\n }\n .d-xxl-inline-grid {\n display: inline-grid !important;\n }\n .d-xxl-table {\n display: table !important;\n }\n .d-xxl-table-row {\n display: table-row !important;\n }\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n .d-xxl-flex {\n display: flex !important;\n }\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n .d-xxl-none {\n display: none !important;\n }\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xxl-row {\n flex-direction: row !important;\n }\n .flex-xxl-column {\n flex-direction: column !important;\n }\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n .align-items-xxl-center {\n align-items: center !important;\n }\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n .align-content-xxl-center {\n align-content: center !important;\n }\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n .align-self-xxl-center {\n align-self: center !important;\n }\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n .order-xxl-first {\n order: -1 !important;\n }\n .order-xxl-0 {\n order: 0 !important;\n }\n .order-xxl-1 {\n order: 1 !important;\n }\n .order-xxl-2 {\n order: 2 !important;\n }\n .order-xxl-3 {\n order: 3 !important;\n }\n .order-xxl-4 {\n order: 4 !important;\n }\n .order-xxl-5 {\n order: 5 !important;\n }\n .order-xxl-last {\n order: 6 !important;\n }\n .m-xxl-0 {\n margin: 0 !important;\n }\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n .m-xxl-3 {\n margin: 1rem !important;\n }\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n .m-xxl-5 {\n margin: 3rem !important;\n }\n .m-xxl-auto {\n margin: auto !important;\n }\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n .me-xxl-auto {\n margin-right: auto !important;\n }\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n .p-xxl-0 {\n padding: 0 !important;\n }\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n .p-xxl-3 {\n padding: 1rem !important;\n }\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n .p-xxl-5 {\n padding: 3rem !important;\n }\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-grid {\n display: grid !important;\n }\n .d-print-inline-grid {\n display: inline-grid !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */","// Container mixins\n\n@mixin make-container($gutter: $container-padding-x) {\n --#{$prefix}gutter-x: #{$gutter};\n --#{$prefix}gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-right: auto;\n margin-left: auto;\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl xxl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @if not $n {\n @error \"breakpoint `#{$name}` not found in `#{$breakpoints}`\";\n }\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width.\n// The maximum value is reduced by 0.02px to work around the limitations of\n// `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $max: map-get($breakpoints, $name);\n @return if($max and $max > 0, $max - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $next: breakpoint-next($name, $breakpoints);\n $max: breakpoint-max($next, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($next, $breakpoints) {\n @content;\n }\n }\n}\n","// Row\n//\n// Rows contain your columns.\n\n:root {\n @each $name, $value in $grid-breakpoints {\n --#{$prefix}breakpoint-#{$name}: #{$value};\n }\n}\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n\n > * {\n @include make-col-ready();\n }\n }\n}\n\n@if $enable-cssgrid {\n .grid {\n display: grid;\n grid-template-rows: repeat(var(--#{$prefix}rows, 1), 1fr);\n grid-template-columns: repeat(var(--#{$prefix}columns, #{$grid-columns}), 1fr);\n gap: var(--#{$prefix}gap, #{$grid-gutter-width});\n\n @include make-cssgrid();\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-row($gutter: $grid-gutter-width) {\n --#{$prefix}gutter-x: #{$gutter};\n --#{$prefix}gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n // TODO: Revisit calc order after https://github.com/react-bootstrap/react-bootstrap/issues/6039 is fixed\n margin-top: calc(-1 * var(--#{$prefix}gutter-y)); // stylelint-disable-line function-disallowed-list\n margin-right: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list\n margin-left: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list\n}\n\n@mixin make-col-ready() {\n // Add box sizing if only the grid is loaded\n box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null);\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we set the width\n // later on to override this initial width.\n flex-shrink: 0;\n width: 100%;\n max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid\n padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-top: var(--#{$prefix}gutter-y);\n}\n\n@mixin make-col($size: false, $columns: $grid-columns) {\n @if $size {\n flex: 0 0 auto;\n width: percentage(divide($size, $columns));\n\n } @else {\n flex: 1 1 0;\n max-width: 100%;\n }\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: divide($size, $columns);\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// number of columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 auto;\n width: percentage(divide(1, $count));\n }\n}\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex: 1 0 0;\n }\n\n .row-cols#{$infix}-auto > * {\n @include make-col-auto();\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n\n // Gutters\n //\n // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns.\n @each $key, $value in $gutters {\n .g#{$infix}-#{$key},\n .gx#{$infix}-#{$key} {\n --#{$prefix}gutter-x: #{$value};\n }\n\n .g#{$infix}-#{$key},\n .gy#{$infix}-#{$key} {\n --#{$prefix}gutter-y: #{$value};\n }\n }\n }\n }\n}\n\n@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .g-col#{$infix}-#{$i} {\n grid-column: auto / span $i;\n }\n }\n\n // Start with `1` because `0` is an invalid value.\n // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible.\n @for $i from 1 through ($columns - 1) {\n .g-start#{$infix}-#{$i} {\n grid-column-start: $i;\n }\n }\n }\n }\n }\n}\n","// Utility generator\n// Used to generate utilities & print utilities\n@mixin generate-utility($utility, $infix: \"\", $is-rfs-media-query: false) {\n $values: map-get($utility, values);\n\n // If the values are a list or string, convert it into a map\n @if type-of($values) == \"string\" or type-of(nth($values, 1)) != \"list\" {\n $values: zip($values, $values);\n }\n\n @each $key, $value in $values {\n $properties: map-get($utility, property);\n\n // Multiple properties are possible, for example with vertical or horizontal margins or paddings\n @if type-of($properties) == \"string\" {\n $properties: append((), $properties);\n }\n\n // Use custom class if present\n $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));\n $property-class: if($property-class == null, \"\", $property-class);\n\n // Use custom CSS variable name if present, otherwise default to `class`\n $css-variable-name: if(map-has-key($utility, css-variable-name), map-get($utility, css-variable-name), map-get($utility, class));\n\n // State params to generate pseudo-classes\n $state: if(map-has-key($utility, state), map-get($utility, state), ());\n\n $infix: if($property-class == \"\" and str-slice($infix, 1, 1) == \"-\", str-slice($infix, 2), $infix);\n\n // Don't prefix if value key is null (e.g. with shadow class)\n $property-class-modifier: if($key, if($property-class == \"\" and $infix == \"\", \"\", \"-\") + $key, \"\");\n\n @if map-get($utility, rfs) {\n // Inside the media query\n @if $is-rfs-media-query {\n $val: rfs-value($value);\n\n // Do not render anything if fluid and non fluid values are the same\n $value: if($val == rfs-fluid-value($value), null, $val);\n }\n @else {\n $value: rfs-fluid-value($value);\n }\n }\n\n $is-css-var: map-get($utility, css-var);\n $is-local-vars: map-get($utility, local-vars);\n $is-rtl: map-get($utility, rtl);\n\n @if $value != null {\n @if $is-rtl == false {\n /* rtl:begin:remove */\n }\n\n @if $is-css-var {\n .#{$property-class + $infix + $property-class-modifier} {\n --#{$prefix}#{$css-variable-name}: #{$value};\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n --#{$prefix}#{$css-variable-name}: #{$value};\n }\n }\n } @else {\n .#{$property-class + $infix + $property-class-modifier} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $variable in $is-local-vars {\n --#{$prefix}#{$local-var}: #{$variable};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $variable in $is-local-vars {\n --#{$prefix}#{$local-var}: #{$variable};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n }\n }\n\n @if $is-rtl == false {\n /* rtl:end:remove */\n }\n }\n }\n}\n","// Loop over each breakpoint\n@each $breakpoint in map-keys($grid-breakpoints) {\n\n // Generate media query if needed\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix);\n }\n }\n }\n}\n\n// RFS rescaling\n@media (min-width: $rfs-mq-value) {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) {\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix, true);\n }\n }\n }\n }\n}\n\n\n// Print utilities\n@media print {\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Then check if the utility needs print styles\n @if type-of($utility) == \"map\" and map-get($utility, print) == true {\n @include generate-utility($utility, \"-print\");\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css new file mode 100644 index 0000000..d891d3e --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css @@ -0,0 +1,4084 @@ +/*! + * Bootstrap Grid v5.3.8 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +.container, +.container-fluid, +.container-xxl, +.container-xl, +.container-lg, +.container-md, +.container-sm { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + width: 100%; + padding-left: calc(var(--bs-gutter-x) * 0.5); + padding-right: calc(var(--bs-gutter-x) * 0.5); + margin-left: auto; + margin-right: auto; +} + +@media (min-width: 576px) { + .container-sm, .container { + max-width: 540px; + } +} +@media (min-width: 768px) { + .container-md, .container-sm, .container { + max-width: 720px; + } +} +@media (min-width: 992px) { + .container-lg, .container-md, .container-sm, .container { + max-width: 960px; + } +} +@media (min-width: 1200px) { + .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1140px; + } +} +@media (min-width: 1400px) { + .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1320px; + } +} +:root { + --bs-breakpoint-xs: 0; + --bs-breakpoint-sm: 576px; + --bs-breakpoint-md: 768px; + --bs-breakpoint-lg: 992px; + --bs-breakpoint-xl: 1200px; + --bs-breakpoint-xxl: 1400px; +} + +.row { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + display: flex; + flex-wrap: wrap; + margin-top: calc(-1 * var(--bs-gutter-y)); + margin-left: calc(-0.5 * var(--bs-gutter-x)); + margin-right: calc(-0.5 * var(--bs-gutter-x)); +} +.row > * { + box-sizing: border-box; + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-left: calc(var(--bs-gutter-x) * 0.5); + padding-right: calc(var(--bs-gutter-x) * 0.5); + margin-top: var(--bs-gutter-y); +} + +.col { + flex: 1 0 0; +} + +.row-cols-auto > * { + flex: 0 0 auto; + width: auto; +} + +.row-cols-1 > * { + flex: 0 0 auto; + width: 100%; +} + +.row-cols-2 > * { + flex: 0 0 auto; + width: 50%; +} + +.row-cols-3 > * { + flex: 0 0 auto; + width: 33.33333333%; +} + +.row-cols-4 > * { + flex: 0 0 auto; + width: 25%; +} + +.row-cols-5 > * { + flex: 0 0 auto; + width: 20%; +} + +.row-cols-6 > * { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; +} + +.col-1 { + flex: 0 0 auto; + width: 8.33333333%; +} + +.col-2 { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-3 { + flex: 0 0 auto; + width: 25%; +} + +.col-4 { + flex: 0 0 auto; + width: 33.33333333%; +} + +.col-5 { + flex: 0 0 auto; + width: 41.66666667%; +} + +.col-6 { + flex: 0 0 auto; + width: 50%; +} + +.col-7 { + flex: 0 0 auto; + width: 58.33333333%; +} + +.col-8 { + flex: 0 0 auto; + width: 66.66666667%; +} + +.col-9 { + flex: 0 0 auto; + width: 75%; +} + +.col-10 { + flex: 0 0 auto; + width: 83.33333333%; +} + +.col-11 { + flex: 0 0 auto; + width: 91.66666667%; +} + +.col-12 { + flex: 0 0 auto; + width: 100%; +} + +.offset-1 { + margin-right: 8.33333333%; +} + +.offset-2 { + margin-right: 16.66666667%; +} + +.offset-3 { + margin-right: 25%; +} + +.offset-4 { + margin-right: 33.33333333%; +} + +.offset-5 { + margin-right: 41.66666667%; +} + +.offset-6 { + margin-right: 50%; +} + +.offset-7 { + margin-right: 58.33333333%; +} + +.offset-8 { + margin-right: 66.66666667%; +} + +.offset-9 { + margin-right: 75%; +} + +.offset-10 { + margin-right: 83.33333333%; +} + +.offset-11 { + margin-right: 91.66666667%; +} + +.g-0, +.gx-0 { + --bs-gutter-x: 0; +} + +.g-0, +.gy-0 { + --bs-gutter-y: 0; +} + +.g-1, +.gx-1 { + --bs-gutter-x: 0.25rem; +} + +.g-1, +.gy-1 { + --bs-gutter-y: 0.25rem; +} + +.g-2, +.gx-2 { + --bs-gutter-x: 0.5rem; +} + +.g-2, +.gy-2 { + --bs-gutter-y: 0.5rem; +} + +.g-3, +.gx-3 { + --bs-gutter-x: 1rem; +} + +.g-3, +.gy-3 { + --bs-gutter-y: 1rem; +} + +.g-4, +.gx-4 { + --bs-gutter-x: 1.5rem; +} + +.g-4, +.gy-4 { + --bs-gutter-y: 1.5rem; +} + +.g-5, +.gx-5 { + --bs-gutter-x: 3rem; +} + +.g-5, +.gy-5 { + --bs-gutter-y: 3rem; +} + +@media (min-width: 576px) { + .col-sm { + flex: 1 0 0; + } + .row-cols-sm-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-sm-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-sm-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-sm-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-sm-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-sm-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-sm-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-sm-auto { + flex: 0 0 auto; + width: auto; + } + .col-sm-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-sm-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-sm-3 { + flex: 0 0 auto; + width: 25%; + } + .col-sm-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-sm-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-sm-6 { + flex: 0 0 auto; + width: 50%; + } + .col-sm-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-sm-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-sm-9 { + flex: 0 0 auto; + width: 75%; + } + .col-sm-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-sm-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-sm-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-sm-0 { + margin-right: 0; + } + .offset-sm-1 { + margin-right: 8.33333333%; + } + .offset-sm-2 { + margin-right: 16.66666667%; + } + .offset-sm-3 { + margin-right: 25%; + } + .offset-sm-4 { + margin-right: 33.33333333%; + } + .offset-sm-5 { + margin-right: 41.66666667%; + } + .offset-sm-6 { + margin-right: 50%; + } + .offset-sm-7 { + margin-right: 58.33333333%; + } + .offset-sm-8 { + margin-right: 66.66666667%; + } + .offset-sm-9 { + margin-right: 75%; + } + .offset-sm-10 { + margin-right: 83.33333333%; + } + .offset-sm-11 { + margin-right: 91.66666667%; + } + .g-sm-0, + .gx-sm-0 { + --bs-gutter-x: 0; + } + .g-sm-0, + .gy-sm-0 { + --bs-gutter-y: 0; + } + .g-sm-1, + .gx-sm-1 { + --bs-gutter-x: 0.25rem; + } + .g-sm-1, + .gy-sm-1 { + --bs-gutter-y: 0.25rem; + } + .g-sm-2, + .gx-sm-2 { + --bs-gutter-x: 0.5rem; + } + .g-sm-2, + .gy-sm-2 { + --bs-gutter-y: 0.5rem; + } + .g-sm-3, + .gx-sm-3 { + --bs-gutter-x: 1rem; + } + .g-sm-3, + .gy-sm-3 { + --bs-gutter-y: 1rem; + } + .g-sm-4, + .gx-sm-4 { + --bs-gutter-x: 1.5rem; + } + .g-sm-4, + .gy-sm-4 { + --bs-gutter-y: 1.5rem; + } + .g-sm-5, + .gx-sm-5 { + --bs-gutter-x: 3rem; + } + .g-sm-5, + .gy-sm-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 768px) { + .col-md { + flex: 1 0 0; + } + .row-cols-md-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-md-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-md-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-md-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-md-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-md-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-md-auto { + flex: 0 0 auto; + width: auto; + } + .col-md-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-md-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-md-3 { + flex: 0 0 auto; + width: 25%; + } + .col-md-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-md-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-md-6 { + flex: 0 0 auto; + width: 50%; + } + .col-md-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-md-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-md-9 { + flex: 0 0 auto; + width: 75%; + } + .col-md-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-md-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-md-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-md-0 { + margin-right: 0; + } + .offset-md-1 { + margin-right: 8.33333333%; + } + .offset-md-2 { + margin-right: 16.66666667%; + } + .offset-md-3 { + margin-right: 25%; + } + .offset-md-4 { + margin-right: 33.33333333%; + } + .offset-md-5 { + margin-right: 41.66666667%; + } + .offset-md-6 { + margin-right: 50%; + } + .offset-md-7 { + margin-right: 58.33333333%; + } + .offset-md-8 { + margin-right: 66.66666667%; + } + .offset-md-9 { + margin-right: 75%; + } + .offset-md-10 { + margin-right: 83.33333333%; + } + .offset-md-11 { + margin-right: 91.66666667%; + } + .g-md-0, + .gx-md-0 { + --bs-gutter-x: 0; + } + .g-md-0, + .gy-md-0 { + --bs-gutter-y: 0; + } + .g-md-1, + .gx-md-1 { + --bs-gutter-x: 0.25rem; + } + .g-md-1, + .gy-md-1 { + --bs-gutter-y: 0.25rem; + } + .g-md-2, + .gx-md-2 { + --bs-gutter-x: 0.5rem; + } + .g-md-2, + .gy-md-2 { + --bs-gutter-y: 0.5rem; + } + .g-md-3, + .gx-md-3 { + --bs-gutter-x: 1rem; + } + .g-md-3, + .gy-md-3 { + --bs-gutter-y: 1rem; + } + .g-md-4, + .gx-md-4 { + --bs-gutter-x: 1.5rem; + } + .g-md-4, + .gy-md-4 { + --bs-gutter-y: 1.5rem; + } + .g-md-5, + .gx-md-5 { + --bs-gutter-x: 3rem; + } + .g-md-5, + .gy-md-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 992px) { + .col-lg { + flex: 1 0 0; + } + .row-cols-lg-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-lg-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-lg-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-lg-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-lg-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-lg-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-lg-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-lg-auto { + flex: 0 0 auto; + width: auto; + } + .col-lg-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-lg-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-lg-3 { + flex: 0 0 auto; + width: 25%; + } + .col-lg-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-lg-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-lg-6 { + flex: 0 0 auto; + width: 50%; + } + .col-lg-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-lg-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-lg-9 { + flex: 0 0 auto; + width: 75%; + } + .col-lg-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-lg-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-lg-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-lg-0 { + margin-right: 0; + } + .offset-lg-1 { + margin-right: 8.33333333%; + } + .offset-lg-2 { + margin-right: 16.66666667%; + } + .offset-lg-3 { + margin-right: 25%; + } + .offset-lg-4 { + margin-right: 33.33333333%; + } + .offset-lg-5 { + margin-right: 41.66666667%; + } + .offset-lg-6 { + margin-right: 50%; + } + .offset-lg-7 { + margin-right: 58.33333333%; + } + .offset-lg-8 { + margin-right: 66.66666667%; + } + .offset-lg-9 { + margin-right: 75%; + } + .offset-lg-10 { + margin-right: 83.33333333%; + } + .offset-lg-11 { + margin-right: 91.66666667%; + } + .g-lg-0, + .gx-lg-0 { + --bs-gutter-x: 0; + } + .g-lg-0, + .gy-lg-0 { + --bs-gutter-y: 0; + } + .g-lg-1, + .gx-lg-1 { + --bs-gutter-x: 0.25rem; + } + .g-lg-1, + .gy-lg-1 { + --bs-gutter-y: 0.25rem; + } + .g-lg-2, + .gx-lg-2 { + --bs-gutter-x: 0.5rem; + } + .g-lg-2, + .gy-lg-2 { + --bs-gutter-y: 0.5rem; + } + .g-lg-3, + .gx-lg-3 { + --bs-gutter-x: 1rem; + } + .g-lg-3, + .gy-lg-3 { + --bs-gutter-y: 1rem; + } + .g-lg-4, + .gx-lg-4 { + --bs-gutter-x: 1.5rem; + } + .g-lg-4, + .gy-lg-4 { + --bs-gutter-y: 1.5rem; + } + .g-lg-5, + .gx-lg-5 { + --bs-gutter-x: 3rem; + } + .g-lg-5, + .gy-lg-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1200px) { + .col-xl { + flex: 1 0 0; + } + .row-cols-xl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xl-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-xl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xl-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xl-0 { + margin-right: 0; + } + .offset-xl-1 { + margin-right: 8.33333333%; + } + .offset-xl-2 { + margin-right: 16.66666667%; + } + .offset-xl-3 { + margin-right: 25%; + } + .offset-xl-4 { + margin-right: 33.33333333%; + } + .offset-xl-5 { + margin-right: 41.66666667%; + } + .offset-xl-6 { + margin-right: 50%; + } + .offset-xl-7 { + margin-right: 58.33333333%; + } + .offset-xl-8 { + margin-right: 66.66666667%; + } + .offset-xl-9 { + margin-right: 75%; + } + .offset-xl-10 { + margin-right: 83.33333333%; + } + .offset-xl-11 { + margin-right: 91.66666667%; + } + .g-xl-0, + .gx-xl-0 { + --bs-gutter-x: 0; + } + .g-xl-0, + .gy-xl-0 { + --bs-gutter-y: 0; + } + .g-xl-1, + .gx-xl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xl-1, + .gy-xl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xl-2, + .gx-xl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xl-2, + .gy-xl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xl-3, + .gx-xl-3 { + --bs-gutter-x: 1rem; + } + .g-xl-3, + .gy-xl-3 { + --bs-gutter-y: 1rem; + } + .g-xl-4, + .gx-xl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xl-4, + .gy-xl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xl-5, + .gx-xl-5 { + --bs-gutter-x: 3rem; + } + .g-xl-5, + .gy-xl-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1400px) { + .col-xxl { + flex: 1 0 0; + } + .row-cols-xxl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xxl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xxl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xxl-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-xxl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xxl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xxl-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xxl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xxl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xxl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xxl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xxl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xxl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xxl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xxl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xxl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xxl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xxl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xxl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xxl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xxl-0 { + margin-right: 0; + } + .offset-xxl-1 { + margin-right: 8.33333333%; + } + .offset-xxl-2 { + margin-right: 16.66666667%; + } + .offset-xxl-3 { + margin-right: 25%; + } + .offset-xxl-4 { + margin-right: 33.33333333%; + } + .offset-xxl-5 { + margin-right: 41.66666667%; + } + .offset-xxl-6 { + margin-right: 50%; + } + .offset-xxl-7 { + margin-right: 58.33333333%; + } + .offset-xxl-8 { + margin-right: 66.66666667%; + } + .offset-xxl-9 { + margin-right: 75%; + } + .offset-xxl-10 { + margin-right: 83.33333333%; + } + .offset-xxl-11 { + margin-right: 91.66666667%; + } + .g-xxl-0, + .gx-xxl-0 { + --bs-gutter-x: 0; + } + .g-xxl-0, + .gy-xxl-0 { + --bs-gutter-y: 0; + } + .g-xxl-1, + .gx-xxl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xxl-1, + .gy-xxl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xxl-2, + .gx-xxl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xxl-2, + .gy-xxl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xxl-3, + .gx-xxl-3 { + --bs-gutter-x: 1rem; + } + .g-xxl-3, + .gy-xxl-3 { + --bs-gutter-y: 1rem; + } + .g-xxl-4, + .gx-xxl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xxl-4, + .gy-xxl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xxl-5, + .gx-xxl-5 { + --bs-gutter-x: 3rem; + } + .g-xxl-5, + .gy-xxl-5 { + --bs-gutter-y: 3rem; + } +} +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-grid { + display: grid !important; +} + +.d-inline-grid { + display: inline-grid !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +.d-none { + display: none !important; +} + +.flex-fill { + flex: 1 1 auto !important; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-grow-0 { + flex-grow: 0 !important; +} + +.flex-grow-1 { + flex-grow: 1 !important; +} + +.flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + flex-shrink: 1 !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.justify-content-evenly { + justify-content: space-evenly !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +.order-first { + order: -1 !important; +} + +.order-0 { + order: 0 !important; +} + +.order-1 { + order: 1 !important; +} + +.order-2 { + order: 2 !important; +} + +.order-3 { + order: 3 !important; +} + +.order-4 { + order: 4 !important; +} + +.order-5 { + order: 5 !important; +} + +.order-last { + order: 6 !important; +} + +.m-0 { + margin: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mx-0 { + margin-left: 0 !important; + margin-right: 0 !important; +} + +.mx-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; +} + +.mx-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; +} + +.mx-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; +} + +.mx-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; +} + +.mx-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; +} + +.mx-auto { + margin-left: auto !important; + margin-right: auto !important; +} + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mt-2 { + margin-top: 0.5rem !important; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.mt-4 { + margin-top: 1.5rem !important; +} + +.mt-5 { + margin-top: 3rem !important; +} + +.mt-auto { + margin-top: auto !important; +} + +.me-0 { + margin-left: 0 !important; +} + +.me-1 { + margin-left: 0.25rem !important; +} + +.me-2 { + margin-left: 0.5rem !important; +} + +.me-3 { + margin-left: 1rem !important; +} + +.me-4 { + margin-left: 1.5rem !important; +} + +.me-5 { + margin-left: 3rem !important; +} + +.me-auto { + margin-left: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 3rem !important; +} + +.mb-auto { + margin-bottom: auto !important; +} + +.ms-0 { + margin-right: 0 !important; +} + +.ms-1 { + margin-right: 0.25rem !important; +} + +.ms-2 { + margin-right: 0.5rem !important; +} + +.ms-3 { + margin-right: 1rem !important; +} + +.ms-4 { + margin-right: 1.5rem !important; +} + +.ms-5 { + margin-right: 3rem !important; +} + +.ms-auto { + margin-right: auto !important; +} + +.p-0 { + padding: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.px-0 { + padding-left: 0 !important; + padding-right: 0 !important; +} + +.px-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; +} + +.px-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; +} + +.px-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; +} + +.px-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; +} + +.px-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; +} + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pt-0 { + padding-top: 0 !important; +} + +.pt-1 { + padding-top: 0.25rem !important; +} + +.pt-2 { + padding-top: 0.5rem !important; +} + +.pt-3 { + padding-top: 1rem !important; +} + +.pt-4 { + padding-top: 1.5rem !important; +} + +.pt-5 { + padding-top: 3rem !important; +} + +.pe-0 { + padding-left: 0 !important; +} + +.pe-1 { + padding-left: 0.25rem !important; +} + +.pe-2 { + padding-left: 0.5rem !important; +} + +.pe-3 { + padding-left: 1rem !important; +} + +.pe-4 { + padding-left: 1.5rem !important; +} + +.pe-5 { + padding-left: 3rem !important; +} + +.pb-0 { + padding-bottom: 0 !important; +} + +.pb-1 { + padding-bottom: 0.25rem !important; +} + +.pb-2 { + padding-bottom: 0.5rem !important; +} + +.pb-3 { + padding-bottom: 1rem !important; +} + +.pb-4 { + padding-bottom: 1.5rem !important; +} + +.pb-5 { + padding-bottom: 3rem !important; +} + +.ps-0 { + padding-right: 0 !important; +} + +.ps-1 { + padding-right: 0.25rem !important; +} + +.ps-2 { + padding-right: 0.5rem !important; +} + +.ps-3 { + padding-right: 1rem !important; +} + +.ps-4 { + padding-right: 1.5rem !important; +} + +.ps-5 { + padding-right: 3rem !important; +} + +@media (min-width: 576px) { + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-grid { + display: grid !important; + } + .d-sm-inline-grid { + display: inline-grid !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: flex !important; + } + .d-sm-inline-flex { + display: inline-flex !important; + } + .d-sm-none { + display: none !important; + } + .flex-sm-fill { + flex: 1 1 auto !important; + } + .flex-sm-row { + flex-direction: row !important; + } + .flex-sm-column { + flex-direction: column !important; + } + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + .flex-sm-grow-0 { + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + flex-shrink: 1 !important; + } + .flex-sm-wrap { + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-sm-start { + justify-content: flex-start !important; + } + .justify-content-sm-end { + justify-content: flex-end !important; + } + .justify-content-sm-center { + justify-content: center !important; + } + .justify-content-sm-between { + justify-content: space-between !important; + } + .justify-content-sm-around { + justify-content: space-around !important; + } + .justify-content-sm-evenly { + justify-content: space-evenly !important; + } + .align-items-sm-start { + align-items: flex-start !important; + } + .align-items-sm-end { + align-items: flex-end !important; + } + .align-items-sm-center { + align-items: center !important; + } + .align-items-sm-baseline { + align-items: baseline !important; + } + .align-items-sm-stretch { + align-items: stretch !important; + } + .align-content-sm-start { + align-content: flex-start !important; + } + .align-content-sm-end { + align-content: flex-end !important; + } + .align-content-sm-center { + align-content: center !important; + } + .align-content-sm-between { + align-content: space-between !important; + } + .align-content-sm-around { + align-content: space-around !important; + } + .align-content-sm-stretch { + align-content: stretch !important; + } + .align-self-sm-auto { + align-self: auto !important; + } + .align-self-sm-start { + align-self: flex-start !important; + } + .align-self-sm-end { + align-self: flex-end !important; + } + .align-self-sm-center { + align-self: center !important; + } + .align-self-sm-baseline { + align-self: baseline !important; + } + .align-self-sm-stretch { + align-self: stretch !important; + } + .order-sm-first { + order: -1 !important; + } + .order-sm-0 { + order: 0 !important; + } + .order-sm-1 { + order: 1 !important; + } + .order-sm-2 { + order: 2 !important; + } + .order-sm-3 { + order: 3 !important; + } + .order-sm-4 { + order: 4 !important; + } + .order-sm-5 { + order: 5 !important; + } + .order-sm-last { + order: 6 !important; + } + .m-sm-0 { + margin: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mx-sm-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + .mx-sm-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + .mx-sm-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + .mx-sm-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + .mx-sm-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + .mx-sm-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + .mx-sm-auto { + margin-left: auto !important; + margin-right: auto !important; + } + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-sm-0 { + margin-top: 0 !important; + } + .mt-sm-1 { + margin-top: 0.25rem !important; + } + .mt-sm-2 { + margin-top: 0.5rem !important; + } + .mt-sm-3 { + margin-top: 1rem !important; + } + .mt-sm-4 { + margin-top: 1.5rem !important; + } + .mt-sm-5 { + margin-top: 3rem !important; + } + .mt-sm-auto { + margin-top: auto !important; + } + .me-sm-0 { + margin-left: 0 !important; + } + .me-sm-1 { + margin-left: 0.25rem !important; + } + .me-sm-2 { + margin-left: 0.5rem !important; + } + .me-sm-3 { + margin-left: 1rem !important; + } + .me-sm-4 { + margin-left: 1.5rem !important; + } + .me-sm-5 { + margin-left: 3rem !important; + } + .me-sm-auto { + margin-left: auto !important; + } + .mb-sm-0 { + margin-bottom: 0 !important; + } + .mb-sm-1 { + margin-bottom: 0.25rem !important; + } + .mb-sm-2 { + margin-bottom: 0.5rem !important; + } + .mb-sm-3 { + margin-bottom: 1rem !important; + } + .mb-sm-4 { + margin-bottom: 1.5rem !important; + } + .mb-sm-5 { + margin-bottom: 3rem !important; + } + .mb-sm-auto { + margin-bottom: auto !important; + } + .ms-sm-0 { + margin-right: 0 !important; + } + .ms-sm-1 { + margin-right: 0.25rem !important; + } + .ms-sm-2 { + margin-right: 0.5rem !important; + } + .ms-sm-3 { + margin-right: 1rem !important; + } + .ms-sm-4 { + margin-right: 1.5rem !important; + } + .ms-sm-5 { + margin-right: 3rem !important; + } + .ms-sm-auto { + margin-right: auto !important; + } + .p-sm-0 { + padding: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .px-sm-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + .px-sm-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + .px-sm-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + .px-sm-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + .px-sm-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + .px-sm-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-sm-0 { + padding-top: 0 !important; + } + .pt-sm-1 { + padding-top: 0.25rem !important; + } + .pt-sm-2 { + padding-top: 0.5rem !important; + } + .pt-sm-3 { + padding-top: 1rem !important; + } + .pt-sm-4 { + padding-top: 1.5rem !important; + } + .pt-sm-5 { + padding-top: 3rem !important; + } + .pe-sm-0 { + padding-left: 0 !important; + } + .pe-sm-1 { + padding-left: 0.25rem !important; + } + .pe-sm-2 { + padding-left: 0.5rem !important; + } + .pe-sm-3 { + padding-left: 1rem !important; + } + .pe-sm-4 { + padding-left: 1.5rem !important; + } + .pe-sm-5 { + padding-left: 3rem !important; + } + .pb-sm-0 { + padding-bottom: 0 !important; + } + .pb-sm-1 { + padding-bottom: 0.25rem !important; + } + .pb-sm-2 { + padding-bottom: 0.5rem !important; + } + .pb-sm-3 { + padding-bottom: 1rem !important; + } + .pb-sm-4 { + padding-bottom: 1.5rem !important; + } + .pb-sm-5 { + padding-bottom: 3rem !important; + } + .ps-sm-0 { + padding-right: 0 !important; + } + .ps-sm-1 { + padding-right: 0.25rem !important; + } + .ps-sm-2 { + padding-right: 0.5rem !important; + } + .ps-sm-3 { + padding-right: 1rem !important; + } + .ps-sm-4 { + padding-right: 1.5rem !important; + } + .ps-sm-5 { + padding-right: 3rem !important; + } +} +@media (min-width: 768px) { + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-grid { + display: grid !important; + } + .d-md-inline-grid { + display: inline-grid !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: flex !important; + } + .d-md-inline-flex { + display: inline-flex !important; + } + .d-md-none { + display: none !important; + } + .flex-md-fill { + flex: 1 1 auto !important; + } + .flex-md-row { + flex-direction: row !important; + } + .flex-md-column { + flex-direction: column !important; + } + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + .flex-md-grow-0 { + flex-grow: 0 !important; + } + .flex-md-grow-1 { + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + flex-shrink: 1 !important; + } + .flex-md-wrap { + flex-wrap: wrap !important; + } + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-md-start { + justify-content: flex-start !important; + } + .justify-content-md-end { + justify-content: flex-end !important; + } + .justify-content-md-center { + justify-content: center !important; + } + .justify-content-md-between { + justify-content: space-between !important; + } + .justify-content-md-around { + justify-content: space-around !important; + } + .justify-content-md-evenly { + justify-content: space-evenly !important; + } + .align-items-md-start { + align-items: flex-start !important; + } + .align-items-md-end { + align-items: flex-end !important; + } + .align-items-md-center { + align-items: center !important; + } + .align-items-md-baseline { + align-items: baseline !important; + } + .align-items-md-stretch { + align-items: stretch !important; + } + .align-content-md-start { + align-content: flex-start !important; + } + .align-content-md-end { + align-content: flex-end !important; + } + .align-content-md-center { + align-content: center !important; + } + .align-content-md-between { + align-content: space-between !important; + } + .align-content-md-around { + align-content: space-around !important; + } + .align-content-md-stretch { + align-content: stretch !important; + } + .align-self-md-auto { + align-self: auto !important; + } + .align-self-md-start { + align-self: flex-start !important; + } + .align-self-md-end { + align-self: flex-end !important; + } + .align-self-md-center { + align-self: center !important; + } + .align-self-md-baseline { + align-self: baseline !important; + } + .align-self-md-stretch { + align-self: stretch !important; + } + .order-md-first { + order: -1 !important; + } + .order-md-0 { + order: 0 !important; + } + .order-md-1 { + order: 1 !important; + } + .order-md-2 { + order: 2 !important; + } + .order-md-3 { + order: 3 !important; + } + .order-md-4 { + order: 4 !important; + } + .order-md-5 { + order: 5 !important; + } + .order-md-last { + order: 6 !important; + } + .m-md-0 { + margin: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mx-md-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + .mx-md-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + .mx-md-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + .mx-md-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + .mx-md-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + .mx-md-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + .mx-md-auto { + margin-left: auto !important; + margin-right: auto !important; + } + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-md-0 { + margin-top: 0 !important; + } + .mt-md-1 { + margin-top: 0.25rem !important; + } + .mt-md-2 { + margin-top: 0.5rem !important; + } + .mt-md-3 { + margin-top: 1rem !important; + } + .mt-md-4 { + margin-top: 1.5rem !important; + } + .mt-md-5 { + margin-top: 3rem !important; + } + .mt-md-auto { + margin-top: auto !important; + } + .me-md-0 { + margin-left: 0 !important; + } + .me-md-1 { + margin-left: 0.25rem !important; + } + .me-md-2 { + margin-left: 0.5rem !important; + } + .me-md-3 { + margin-left: 1rem !important; + } + .me-md-4 { + margin-left: 1.5rem !important; + } + .me-md-5 { + margin-left: 3rem !important; + } + .me-md-auto { + margin-left: auto !important; + } + .mb-md-0 { + margin-bottom: 0 !important; + } + .mb-md-1 { + margin-bottom: 0.25rem !important; + } + .mb-md-2 { + margin-bottom: 0.5rem !important; + } + .mb-md-3 { + margin-bottom: 1rem !important; + } + .mb-md-4 { + margin-bottom: 1.5rem !important; + } + .mb-md-5 { + margin-bottom: 3rem !important; + } + .mb-md-auto { + margin-bottom: auto !important; + } + .ms-md-0 { + margin-right: 0 !important; + } + .ms-md-1 { + margin-right: 0.25rem !important; + } + .ms-md-2 { + margin-right: 0.5rem !important; + } + .ms-md-3 { + margin-right: 1rem !important; + } + .ms-md-4 { + margin-right: 1.5rem !important; + } + .ms-md-5 { + margin-right: 3rem !important; + } + .ms-md-auto { + margin-right: auto !important; + } + .p-md-0 { + padding: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .px-md-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + .px-md-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + .px-md-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + .px-md-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + .px-md-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + .px-md-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-md-0 { + padding-top: 0 !important; + } + .pt-md-1 { + padding-top: 0.25rem !important; + } + .pt-md-2 { + padding-top: 0.5rem !important; + } + .pt-md-3 { + padding-top: 1rem !important; + } + .pt-md-4 { + padding-top: 1.5rem !important; + } + .pt-md-5 { + padding-top: 3rem !important; + } + .pe-md-0 { + padding-left: 0 !important; + } + .pe-md-1 { + padding-left: 0.25rem !important; + } + .pe-md-2 { + padding-left: 0.5rem !important; + } + .pe-md-3 { + padding-left: 1rem !important; + } + .pe-md-4 { + padding-left: 1.5rem !important; + } + .pe-md-5 { + padding-left: 3rem !important; + } + .pb-md-0 { + padding-bottom: 0 !important; + } + .pb-md-1 { + padding-bottom: 0.25rem !important; + } + .pb-md-2 { + padding-bottom: 0.5rem !important; + } + .pb-md-3 { + padding-bottom: 1rem !important; + } + .pb-md-4 { + padding-bottom: 1.5rem !important; + } + .pb-md-5 { + padding-bottom: 3rem !important; + } + .ps-md-0 { + padding-right: 0 !important; + } + .ps-md-1 { + padding-right: 0.25rem !important; + } + .ps-md-2 { + padding-right: 0.5rem !important; + } + .ps-md-3 { + padding-right: 1rem !important; + } + .ps-md-4 { + padding-right: 1.5rem !important; + } + .ps-md-5 { + padding-right: 3rem !important; + } +} +@media (min-width: 992px) { + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-grid { + display: grid !important; + } + .d-lg-inline-grid { + display: inline-grid !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: flex !important; + } + .d-lg-inline-flex { + display: inline-flex !important; + } + .d-lg-none { + display: none !important; + } + .flex-lg-fill { + flex: 1 1 auto !important; + } + .flex-lg-row { + flex-direction: row !important; + } + .flex-lg-column { + flex-direction: column !important; + } + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + .flex-lg-grow-0 { + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + flex-shrink: 1 !important; + } + .flex-lg-wrap { + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-lg-start { + justify-content: flex-start !important; + } + .justify-content-lg-end { + justify-content: flex-end !important; + } + .justify-content-lg-center { + justify-content: center !important; + } + .justify-content-lg-between { + justify-content: space-between !important; + } + .justify-content-lg-around { + justify-content: space-around !important; + } + .justify-content-lg-evenly { + justify-content: space-evenly !important; + } + .align-items-lg-start { + align-items: flex-start !important; + } + .align-items-lg-end { + align-items: flex-end !important; + } + .align-items-lg-center { + align-items: center !important; + } + .align-items-lg-baseline { + align-items: baseline !important; + } + .align-items-lg-stretch { + align-items: stretch !important; + } + .align-content-lg-start { + align-content: flex-start !important; + } + .align-content-lg-end { + align-content: flex-end !important; + } + .align-content-lg-center { + align-content: center !important; + } + .align-content-lg-between { + align-content: space-between !important; + } + .align-content-lg-around { + align-content: space-around !important; + } + .align-content-lg-stretch { + align-content: stretch !important; + } + .align-self-lg-auto { + align-self: auto !important; + } + .align-self-lg-start { + align-self: flex-start !important; + } + .align-self-lg-end { + align-self: flex-end !important; + } + .align-self-lg-center { + align-self: center !important; + } + .align-self-lg-baseline { + align-self: baseline !important; + } + .align-self-lg-stretch { + align-self: stretch !important; + } + .order-lg-first { + order: -1 !important; + } + .order-lg-0 { + order: 0 !important; + } + .order-lg-1 { + order: 1 !important; + } + .order-lg-2 { + order: 2 !important; + } + .order-lg-3 { + order: 3 !important; + } + .order-lg-4 { + order: 4 !important; + } + .order-lg-5 { + order: 5 !important; + } + .order-lg-last { + order: 6 !important; + } + .m-lg-0 { + margin: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mx-lg-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + .mx-lg-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + .mx-lg-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + .mx-lg-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + .mx-lg-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + .mx-lg-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + .mx-lg-auto { + margin-left: auto !important; + margin-right: auto !important; + } + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-lg-0 { + margin-top: 0 !important; + } + .mt-lg-1 { + margin-top: 0.25rem !important; + } + .mt-lg-2 { + margin-top: 0.5rem !important; + } + .mt-lg-3 { + margin-top: 1rem !important; + } + .mt-lg-4 { + margin-top: 1.5rem !important; + } + .mt-lg-5 { + margin-top: 3rem !important; + } + .mt-lg-auto { + margin-top: auto !important; + } + .me-lg-0 { + margin-left: 0 !important; + } + .me-lg-1 { + margin-left: 0.25rem !important; + } + .me-lg-2 { + margin-left: 0.5rem !important; + } + .me-lg-3 { + margin-left: 1rem !important; + } + .me-lg-4 { + margin-left: 1.5rem !important; + } + .me-lg-5 { + margin-left: 3rem !important; + } + .me-lg-auto { + margin-left: auto !important; + } + .mb-lg-0 { + margin-bottom: 0 !important; + } + .mb-lg-1 { + margin-bottom: 0.25rem !important; + } + .mb-lg-2 { + margin-bottom: 0.5rem !important; + } + .mb-lg-3 { + margin-bottom: 1rem !important; + } + .mb-lg-4 { + margin-bottom: 1.5rem !important; + } + .mb-lg-5 { + margin-bottom: 3rem !important; + } + .mb-lg-auto { + margin-bottom: auto !important; + } + .ms-lg-0 { + margin-right: 0 !important; + } + .ms-lg-1 { + margin-right: 0.25rem !important; + } + .ms-lg-2 { + margin-right: 0.5rem !important; + } + .ms-lg-3 { + margin-right: 1rem !important; + } + .ms-lg-4 { + margin-right: 1.5rem !important; + } + .ms-lg-5 { + margin-right: 3rem !important; + } + .ms-lg-auto { + margin-right: auto !important; + } + .p-lg-0 { + padding: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .px-lg-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + .px-lg-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + .px-lg-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + .px-lg-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + .px-lg-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + .px-lg-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-lg-0 { + padding-top: 0 !important; + } + .pt-lg-1 { + padding-top: 0.25rem !important; + } + .pt-lg-2 { + padding-top: 0.5rem !important; + } + .pt-lg-3 { + padding-top: 1rem !important; + } + .pt-lg-4 { + padding-top: 1.5rem !important; + } + .pt-lg-5 { + padding-top: 3rem !important; + } + .pe-lg-0 { + padding-left: 0 !important; + } + .pe-lg-1 { + padding-left: 0.25rem !important; + } + .pe-lg-2 { + padding-left: 0.5rem !important; + } + .pe-lg-3 { + padding-left: 1rem !important; + } + .pe-lg-4 { + padding-left: 1.5rem !important; + } + .pe-lg-5 { + padding-left: 3rem !important; + } + .pb-lg-0 { + padding-bottom: 0 !important; + } + .pb-lg-1 { + padding-bottom: 0.25rem !important; + } + .pb-lg-2 { + padding-bottom: 0.5rem !important; + } + .pb-lg-3 { + padding-bottom: 1rem !important; + } + .pb-lg-4 { + padding-bottom: 1.5rem !important; + } + .pb-lg-5 { + padding-bottom: 3rem !important; + } + .ps-lg-0 { + padding-right: 0 !important; + } + .ps-lg-1 { + padding-right: 0.25rem !important; + } + .ps-lg-2 { + padding-right: 0.5rem !important; + } + .ps-lg-3 { + padding-right: 1rem !important; + } + .ps-lg-4 { + padding-right: 1.5rem !important; + } + .ps-lg-5 { + padding-right: 3rem !important; + } +} +@media (min-width: 1200px) { + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-grid { + display: grid !important; + } + .d-xl-inline-grid { + display: inline-grid !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: flex !important; + } + .d-xl-inline-flex { + display: inline-flex !important; + } + .d-xl-none { + display: none !important; + } + .flex-xl-fill { + flex: 1 1 auto !important; + } + .flex-xl-row { + flex-direction: row !important; + } + .flex-xl-column { + flex-direction: column !important; + } + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xl-grow-0 { + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xl-wrap { + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xl-start { + justify-content: flex-start !important; + } + .justify-content-xl-end { + justify-content: flex-end !important; + } + .justify-content-xl-center { + justify-content: center !important; + } + .justify-content-xl-between { + justify-content: space-between !important; + } + .justify-content-xl-around { + justify-content: space-around !important; + } + .justify-content-xl-evenly { + justify-content: space-evenly !important; + } + .align-items-xl-start { + align-items: flex-start !important; + } + .align-items-xl-end { + align-items: flex-end !important; + } + .align-items-xl-center { + align-items: center !important; + } + .align-items-xl-baseline { + align-items: baseline !important; + } + .align-items-xl-stretch { + align-items: stretch !important; + } + .align-content-xl-start { + align-content: flex-start !important; + } + .align-content-xl-end { + align-content: flex-end !important; + } + .align-content-xl-center { + align-content: center !important; + } + .align-content-xl-between { + align-content: space-between !important; + } + .align-content-xl-around { + align-content: space-around !important; + } + .align-content-xl-stretch { + align-content: stretch !important; + } + .align-self-xl-auto { + align-self: auto !important; + } + .align-self-xl-start { + align-self: flex-start !important; + } + .align-self-xl-end { + align-self: flex-end !important; + } + .align-self-xl-center { + align-self: center !important; + } + .align-self-xl-baseline { + align-self: baseline !important; + } + .align-self-xl-stretch { + align-self: stretch !important; + } + .order-xl-first { + order: -1 !important; + } + .order-xl-0 { + order: 0 !important; + } + .order-xl-1 { + order: 1 !important; + } + .order-xl-2 { + order: 2 !important; + } + .order-xl-3 { + order: 3 !important; + } + .order-xl-4 { + order: 4 !important; + } + .order-xl-5 { + order: 5 !important; + } + .order-xl-last { + order: 6 !important; + } + .m-xl-0 { + margin: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mx-xl-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + .mx-xl-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + .mx-xl-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + .mx-xl-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + .mx-xl-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + .mx-xl-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + .mx-xl-auto { + margin-left: auto !important; + margin-right: auto !important; + } + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xl-0 { + margin-top: 0 !important; + } + .mt-xl-1 { + margin-top: 0.25rem !important; + } + .mt-xl-2 { + margin-top: 0.5rem !important; + } + .mt-xl-3 { + margin-top: 1rem !important; + } + .mt-xl-4 { + margin-top: 1.5rem !important; + } + .mt-xl-5 { + margin-top: 3rem !important; + } + .mt-xl-auto { + margin-top: auto !important; + } + .me-xl-0 { + margin-left: 0 !important; + } + .me-xl-1 { + margin-left: 0.25rem !important; + } + .me-xl-2 { + margin-left: 0.5rem !important; + } + .me-xl-3 { + margin-left: 1rem !important; + } + .me-xl-4 { + margin-left: 1.5rem !important; + } + .me-xl-5 { + margin-left: 3rem !important; + } + .me-xl-auto { + margin-left: auto !important; + } + .mb-xl-0 { + margin-bottom: 0 !important; + } + .mb-xl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xl-3 { + margin-bottom: 1rem !important; + } + .mb-xl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xl-5 { + margin-bottom: 3rem !important; + } + .mb-xl-auto { + margin-bottom: auto !important; + } + .ms-xl-0 { + margin-right: 0 !important; + } + .ms-xl-1 { + margin-right: 0.25rem !important; + } + .ms-xl-2 { + margin-right: 0.5rem !important; + } + .ms-xl-3 { + margin-right: 1rem !important; + } + .ms-xl-4 { + margin-right: 1.5rem !important; + } + .ms-xl-5 { + margin-right: 3rem !important; + } + .ms-xl-auto { + margin-right: auto !important; + } + .p-xl-0 { + padding: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .px-xl-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + .px-xl-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + .px-xl-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + .px-xl-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + .px-xl-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + .px-xl-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xl-0 { + padding-top: 0 !important; + } + .pt-xl-1 { + padding-top: 0.25rem !important; + } + .pt-xl-2 { + padding-top: 0.5rem !important; + } + .pt-xl-3 { + padding-top: 1rem !important; + } + .pt-xl-4 { + padding-top: 1.5rem !important; + } + .pt-xl-5 { + padding-top: 3rem !important; + } + .pe-xl-0 { + padding-left: 0 !important; + } + .pe-xl-1 { + padding-left: 0.25rem !important; + } + .pe-xl-2 { + padding-left: 0.5rem !important; + } + .pe-xl-3 { + padding-left: 1rem !important; + } + .pe-xl-4 { + padding-left: 1.5rem !important; + } + .pe-xl-5 { + padding-left: 3rem !important; + } + .pb-xl-0 { + padding-bottom: 0 !important; + } + .pb-xl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xl-3 { + padding-bottom: 1rem !important; + } + .pb-xl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xl-5 { + padding-bottom: 3rem !important; + } + .ps-xl-0 { + padding-right: 0 !important; + } + .ps-xl-1 { + padding-right: 0.25rem !important; + } + .ps-xl-2 { + padding-right: 0.5rem !important; + } + .ps-xl-3 { + padding-right: 1rem !important; + } + .ps-xl-4 { + padding-right: 1.5rem !important; + } + .ps-xl-5 { + padding-right: 3rem !important; + } +} +@media (min-width: 1400px) { + .d-xxl-inline { + display: inline !important; + } + .d-xxl-inline-block { + display: inline-block !important; + } + .d-xxl-block { + display: block !important; + } + .d-xxl-grid { + display: grid !important; + } + .d-xxl-inline-grid { + display: inline-grid !important; + } + .d-xxl-table { + display: table !important; + } + .d-xxl-table-row { + display: table-row !important; + } + .d-xxl-table-cell { + display: table-cell !important; + } + .d-xxl-flex { + display: flex !important; + } + .d-xxl-inline-flex { + display: inline-flex !important; + } + .d-xxl-none { + display: none !important; + } + .flex-xxl-fill { + flex: 1 1 auto !important; + } + .flex-xxl-row { + flex-direction: row !important; + } + .flex-xxl-column { + flex-direction: column !important; + } + .flex-xxl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xxl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xxl-grow-0 { + flex-grow: 0 !important; + } + .flex-xxl-grow-1 { + flex-grow: 1 !important; + } + .flex-xxl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xxl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xxl-wrap { + flex-wrap: wrap !important; + } + .flex-xxl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xxl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xxl-start { + justify-content: flex-start !important; + } + .justify-content-xxl-end { + justify-content: flex-end !important; + } + .justify-content-xxl-center { + justify-content: center !important; + } + .justify-content-xxl-between { + justify-content: space-between !important; + } + .justify-content-xxl-around { + justify-content: space-around !important; + } + .justify-content-xxl-evenly { + justify-content: space-evenly !important; + } + .align-items-xxl-start { + align-items: flex-start !important; + } + .align-items-xxl-end { + align-items: flex-end !important; + } + .align-items-xxl-center { + align-items: center !important; + } + .align-items-xxl-baseline { + align-items: baseline !important; + } + .align-items-xxl-stretch { + align-items: stretch !important; + } + .align-content-xxl-start { + align-content: flex-start !important; + } + .align-content-xxl-end { + align-content: flex-end !important; + } + .align-content-xxl-center { + align-content: center !important; + } + .align-content-xxl-between { + align-content: space-between !important; + } + .align-content-xxl-around { + align-content: space-around !important; + } + .align-content-xxl-stretch { + align-content: stretch !important; + } + .align-self-xxl-auto { + align-self: auto !important; + } + .align-self-xxl-start { + align-self: flex-start !important; + } + .align-self-xxl-end { + align-self: flex-end !important; + } + .align-self-xxl-center { + align-self: center !important; + } + .align-self-xxl-baseline { + align-self: baseline !important; + } + .align-self-xxl-stretch { + align-self: stretch !important; + } + .order-xxl-first { + order: -1 !important; + } + .order-xxl-0 { + order: 0 !important; + } + .order-xxl-1 { + order: 1 !important; + } + .order-xxl-2 { + order: 2 !important; + } + .order-xxl-3 { + order: 3 !important; + } + .order-xxl-4 { + order: 4 !important; + } + .order-xxl-5 { + order: 5 !important; + } + .order-xxl-last { + order: 6 !important; + } + .m-xxl-0 { + margin: 0 !important; + } + .m-xxl-1 { + margin: 0.25rem !important; + } + .m-xxl-2 { + margin: 0.5rem !important; + } + .m-xxl-3 { + margin: 1rem !important; + } + .m-xxl-4 { + margin: 1.5rem !important; + } + .m-xxl-5 { + margin: 3rem !important; + } + .m-xxl-auto { + margin: auto !important; + } + .mx-xxl-0 { + margin-left: 0 !important; + margin-right: 0 !important; + } + .mx-xxl-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; + } + .mx-xxl-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; + } + .mx-xxl-3 { + margin-left: 1rem !important; + margin-right: 1rem !important; + } + .mx-xxl-4 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; + } + .mx-xxl-5 { + margin-left: 3rem !important; + margin-right: 3rem !important; + } + .mx-xxl-auto { + margin-left: auto !important; + margin-right: auto !important; + } + .my-xxl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xxl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xxl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xxl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xxl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xxl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xxl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xxl-0 { + margin-top: 0 !important; + } + .mt-xxl-1 { + margin-top: 0.25rem !important; + } + .mt-xxl-2 { + margin-top: 0.5rem !important; + } + .mt-xxl-3 { + margin-top: 1rem !important; + } + .mt-xxl-4 { + margin-top: 1.5rem !important; + } + .mt-xxl-5 { + margin-top: 3rem !important; + } + .mt-xxl-auto { + margin-top: auto !important; + } + .me-xxl-0 { + margin-left: 0 !important; + } + .me-xxl-1 { + margin-left: 0.25rem !important; + } + .me-xxl-2 { + margin-left: 0.5rem !important; + } + .me-xxl-3 { + margin-left: 1rem !important; + } + .me-xxl-4 { + margin-left: 1.5rem !important; + } + .me-xxl-5 { + margin-left: 3rem !important; + } + .me-xxl-auto { + margin-left: auto !important; + } + .mb-xxl-0 { + margin-bottom: 0 !important; + } + .mb-xxl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xxl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xxl-3 { + margin-bottom: 1rem !important; + } + .mb-xxl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xxl-5 { + margin-bottom: 3rem !important; + } + .mb-xxl-auto { + margin-bottom: auto !important; + } + .ms-xxl-0 { + margin-right: 0 !important; + } + .ms-xxl-1 { + margin-right: 0.25rem !important; + } + .ms-xxl-2 { + margin-right: 0.5rem !important; + } + .ms-xxl-3 { + margin-right: 1rem !important; + } + .ms-xxl-4 { + margin-right: 1.5rem !important; + } + .ms-xxl-5 { + margin-right: 3rem !important; + } + .ms-xxl-auto { + margin-right: auto !important; + } + .p-xxl-0 { + padding: 0 !important; + } + .p-xxl-1 { + padding: 0.25rem !important; + } + .p-xxl-2 { + padding: 0.5rem !important; + } + .p-xxl-3 { + padding: 1rem !important; + } + .p-xxl-4 { + padding: 1.5rem !important; + } + .p-xxl-5 { + padding: 3rem !important; + } + .px-xxl-0 { + padding-left: 0 !important; + padding-right: 0 !important; + } + .px-xxl-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; + } + .px-xxl-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; + } + .px-xxl-3 { + padding-left: 1rem !important; + padding-right: 1rem !important; + } + .px-xxl-4 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; + } + .px-xxl-5 { + padding-left: 3rem !important; + padding-right: 3rem !important; + } + .py-xxl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xxl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xxl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xxl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xxl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xxl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xxl-0 { + padding-top: 0 !important; + } + .pt-xxl-1 { + padding-top: 0.25rem !important; + } + .pt-xxl-2 { + padding-top: 0.5rem !important; + } + .pt-xxl-3 { + padding-top: 1rem !important; + } + .pt-xxl-4 { + padding-top: 1.5rem !important; + } + .pt-xxl-5 { + padding-top: 3rem !important; + } + .pe-xxl-0 { + padding-left: 0 !important; + } + .pe-xxl-1 { + padding-left: 0.25rem !important; + } + .pe-xxl-2 { + padding-left: 0.5rem !important; + } + .pe-xxl-3 { + padding-left: 1rem !important; + } + .pe-xxl-4 { + padding-left: 1.5rem !important; + } + .pe-xxl-5 { + padding-left: 3rem !important; + } + .pb-xxl-0 { + padding-bottom: 0 !important; + } + .pb-xxl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xxl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xxl-3 { + padding-bottom: 1rem !important; + } + .pb-xxl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xxl-5 { + padding-bottom: 3rem !important; + } + .ps-xxl-0 { + padding-right: 0 !important; + } + .ps-xxl-1 { + padding-right: 0.25rem !important; + } + .ps-xxl-2 { + padding-right: 0.5rem !important; + } + .ps-xxl-3 { + padding-right: 1rem !important; + } + .ps-xxl-4 { + padding-right: 1.5rem !important; + } + .ps-xxl-5 { + padding-right: 3rem !important; + } +} +@media print { + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-grid { + display: grid !important; + } + .d-print-inline-grid { + display: inline-grid !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: flex !important; + } + .d-print-inline-flex { + display: inline-flex !important; + } + .d-print-none { + display: none !important; + } +} +/*# sourceMappingURL=bootstrap-grid.rtl.css.map */ \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map new file mode 100644 index 0000000..2d94299 --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","bootstrap-grid.css","../../scss/mixins/_breakpoints.scss","../../scss/_variables.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"AACE;;;;EAAA;ACKA;;;;;;;ECHA,qBAAA;EACA,gBAAA;EACA,WAAA;EACA,4CAAA;EACA,6CAAA;EACA,iBAAA;EACA,kBAAA;ACUF;;AC4CI;EH5CE;IACE,gBIkee;EF9drB;AACF;ACsCI;EH5CE;IACE,gBIkee;EFzdrB;AACF;ACiCI;EH5CE;IACE,gBIkee;EFpdrB;AACF;AC4BI;EH5CE;IACE,iBIkee;EF/crB;AACF;ACuBI;EH5CE;IACE,iBIkee;EF1crB;AACF;AGzCA;EAEI,qBAAA;EAAA,yBAAA;EAAA,yBAAA;EAAA,yBAAA;EAAA,0BAAA;EAAA,2BAAA;AH+CJ;;AG1CE;ECNA,qBAAA;EACA,gBAAA;EACA,aAAA;EACA,eAAA;EAEA,yCAAA;EACA,4CAAA;EACA,6CAAA;AJmDF;AGjDI;ECGF,sBAAA;EAIA,cAAA;EACA,WAAA;EACA,eAAA;EACA,4CAAA;EACA,6CAAA;EACA,8BAAA;AJ8CF;;AICM;EACE,WAAA;AJER;;AICM;EApCJ,cAAA;EACA,WAAA;AJuCF;;AIzBE;EACE,cAAA;EACA,WAAA;AJ4BJ;;AI9BE;EACE,cAAA;EACA,UAAA;AJiCJ;;AInCE;EACE,cAAA;EACA,mBAAA;AJsCJ;;AIxCE;EACE,cAAA;EACA,UAAA;AJ2CJ;;AI7CE;EACE,cAAA;EACA,UAAA;AJgDJ;;AIlDE;EACE,cAAA;EACA,mBAAA;AJqDJ;;AItBM;EAhDJ,cAAA;EACA,WAAA;AJ0EF;;AIrBU;EAhEN,cAAA;EACA,kBAAA;AJyFJ;;AI1BU;EAhEN,cAAA;EACA,mBAAA;AJ8FJ;;AI/BU;EAhEN,cAAA;EACA,UAAA;AJmGJ;;AIpCU;EAhEN,cAAA;EACA,mBAAA;AJwGJ;;AIzCU;EAhEN,cAAA;EACA,mBAAA;AJ6GJ;;AI9CU;EAhEN,cAAA;EACA,UAAA;AJkHJ;;AInDU;EAhEN,cAAA;EACA,mBAAA;AJuHJ;;AIxDU;EAhEN,cAAA;EACA,mBAAA;AJ4HJ;;AI7DU;EAhEN,cAAA;EACA,UAAA;AJiIJ;;AIlEU;EAhEN,cAAA;EACA,mBAAA;AJsIJ;;AIvEU;EAhEN,cAAA;EACA,mBAAA;AJ2IJ;;AI5EU;EAhEN,cAAA;EACA,WAAA;AJgJJ;;AIzEY;EAxDV,yBAAA;AJqIF;;AI7EY;EAxDV,0BAAA;AJyIF;;AIjFY;EAxDV,iBAAA;AJ6IF;;AIrFY;EAxDV,0BAAA;AJiJF;;AIzFY;EAxDV,0BAAA;AJqJF;;AI7FY;EAxDV,iBAAA;AJyJF;;AIjGY;EAxDV,0BAAA;AJ6JF;;AIrGY;EAxDV,0BAAA;AJiKF;;AIzGY;EAxDV,iBAAA;AJqKF;;AI7GY;EAxDV,0BAAA;AJyKF;;AIjHY;EAxDV,0BAAA;AJ6KF;;AI1GQ;;EAEE,gBAAA;AJ6GV;;AI1GQ;;EAEE,gBAAA;AJ6GV;;AIpHQ;;EAEE,sBAAA;AJuHV;;AIpHQ;;EAEE,sBAAA;AJuHV;;AI9HQ;;EAEE,qBAAA;AJiIV;;AI9HQ;;EAEE,qBAAA;AJiIV;;AIxIQ;;EAEE,mBAAA;AJ2IV;;AIxIQ;;EAEE,mBAAA;AJ2IV;;AIlJQ;;EAEE,qBAAA;AJqJV;;AIlJQ;;EAEE,qBAAA;AJqJV;;AI5JQ;;EAEE,mBAAA;AJ+JV;;AI5JQ;;EAEE,mBAAA;AJ+JV;;ACzNI;EGUE;IACE,WAAA;EJmNN;EIhNI;IApCJ,cAAA;IACA,WAAA;EJuPA;EIzOA;IACE,cAAA;IACA,WAAA;EJ2OF;EI7OA;IACE,cAAA;IACA,UAAA;EJ+OF;EIjPA;IACE,cAAA;IACA,mBAAA;EJmPF;EIrPA;IACE,cAAA;IACA,UAAA;EJuPF;EIzPA;IACE,cAAA;IACA,UAAA;EJ2PF;EI7PA;IACE,cAAA;IACA,mBAAA;EJ+PF;EIhOI;IAhDJ,cAAA;IACA,WAAA;EJmRA;EI9NQ;IAhEN,cAAA;IACA,kBAAA;EJiSF;EIlOQ;IAhEN,cAAA;IACA,mBAAA;EJqSF;EItOQ;IAhEN,cAAA;IACA,UAAA;EJySF;EI1OQ;IAhEN,cAAA;IACA,mBAAA;EJ6SF;EI9OQ;IAhEN,cAAA;IACA,mBAAA;EJiTF;EIlPQ;IAhEN,cAAA;IACA,UAAA;EJqTF;EItPQ;IAhEN,cAAA;IACA,mBAAA;EJyTF;EI1PQ;IAhEN,cAAA;IACA,mBAAA;EJ6TF;EI9PQ;IAhEN,cAAA;IACA,UAAA;EJiUF;EIlQQ;IAhEN,cAAA;IACA,mBAAA;EJqUF;EItQQ;IAhEN,cAAA;IACA,mBAAA;EJyUF;EI1QQ;IAhEN,cAAA;IACA,WAAA;EJ6UF;EItQU;IAxDV,eAAA;EJiUA;EIzQU;IAxDV,yBAAA;EJoUA;EI5QU;IAxDV,0BAAA;EJuUA;EI/QU;IAxDV,iBAAA;EJ0UA;EIlRU;IAxDV,0BAAA;EJ6UA;EIrRU;IAxDV,0BAAA;EJgVA;EIxRU;IAxDV,iBAAA;EJmVA;EI3RU;IAxDV,0BAAA;EJsVA;EI9RU;IAxDV,0BAAA;EJyVA;EIjSU;IAxDV,iBAAA;EJ4VA;EIpSU;IAxDV,0BAAA;EJ+VA;EIvSU;IAxDV,0BAAA;EJkWA;EI/RM;;IAEE,gBAAA;EJiSR;EI9RM;;IAEE,gBAAA;EJgSR;EIvSM;;IAEE,sBAAA;EJySR;EItSM;;IAEE,sBAAA;EJwSR;EI/SM;;IAEE,qBAAA;EJiTR;EI9SM;;IAEE,qBAAA;EJgTR;EIvTM;;IAEE,mBAAA;EJyTR;EItTM;;IAEE,mBAAA;EJwTR;EI/TM;;IAEE,qBAAA;EJiUR;EI9TM;;IAEE,qBAAA;EJgUR;EIvUM;;IAEE,mBAAA;EJyUR;EItUM;;IAEE,mBAAA;EJwUR;AACF;ACnYI;EGUE;IACE,WAAA;EJ4XN;EIzXI;IApCJ,cAAA;IACA,WAAA;EJgaA;EIlZA;IACE,cAAA;IACA,WAAA;EJoZF;EItZA;IACE,cAAA;IACA,UAAA;EJwZF;EI1ZA;IACE,cAAA;IACA,mBAAA;EJ4ZF;EI9ZA;IACE,cAAA;IACA,UAAA;EJgaF;EIlaA;IACE,cAAA;IACA,UAAA;EJoaF;EItaA;IACE,cAAA;IACA,mBAAA;EJwaF;EIzYI;IAhDJ,cAAA;IACA,WAAA;EJ4bA;EIvYQ;IAhEN,cAAA;IACA,kBAAA;EJ0cF;EI3YQ;IAhEN,cAAA;IACA,mBAAA;EJ8cF;EI/YQ;IAhEN,cAAA;IACA,UAAA;EJkdF;EInZQ;IAhEN,cAAA;IACA,mBAAA;EJsdF;EIvZQ;IAhEN,cAAA;IACA,mBAAA;EJ0dF;EI3ZQ;IAhEN,cAAA;IACA,UAAA;EJ8dF;EI/ZQ;IAhEN,cAAA;IACA,mBAAA;EJkeF;EInaQ;IAhEN,cAAA;IACA,mBAAA;EJseF;EIvaQ;IAhEN,cAAA;IACA,UAAA;EJ0eF;EI3aQ;IAhEN,cAAA;IACA,mBAAA;EJ8eF;EI/aQ;IAhEN,cAAA;IACA,mBAAA;EJkfF;EInbQ;IAhEN,cAAA;IACA,WAAA;EJsfF;EI/aU;IAxDV,eAAA;EJ0eA;EIlbU;IAxDV,yBAAA;EJ6eA;EIrbU;IAxDV,0BAAA;EJgfA;EIxbU;IAxDV,iBAAA;EJmfA;EI3bU;IAxDV,0BAAA;EJsfA;EI9bU;IAxDV,0BAAA;EJyfA;EIjcU;IAxDV,iBAAA;EJ4fA;EIpcU;IAxDV,0BAAA;EJ+fA;EIvcU;IAxDV,0BAAA;EJkgBA;EI1cU;IAxDV,iBAAA;EJqgBA;EI7cU;IAxDV,0BAAA;EJwgBA;EIhdU;IAxDV,0BAAA;EJ2gBA;EIxcM;;IAEE,gBAAA;EJ0cR;EIvcM;;IAEE,gBAAA;EJycR;EIhdM;;IAEE,sBAAA;EJkdR;EI/cM;;IAEE,sBAAA;EJidR;EIxdM;;IAEE,qBAAA;EJ0dR;EIvdM;;IAEE,qBAAA;EJydR;EIheM;;IAEE,mBAAA;EJkeR;EI/dM;;IAEE,mBAAA;EJieR;EIxeM;;IAEE,qBAAA;EJ0eR;EIveM;;IAEE,qBAAA;EJyeR;EIhfM;;IAEE,mBAAA;EJkfR;EI/eM;;IAEE,mBAAA;EJifR;AACF;AC5iBI;EGUE;IACE,WAAA;EJqiBN;EIliBI;IApCJ,cAAA;IACA,WAAA;EJykBA;EI3jBA;IACE,cAAA;IACA,WAAA;EJ6jBF;EI/jBA;IACE,cAAA;IACA,UAAA;EJikBF;EInkBA;IACE,cAAA;IACA,mBAAA;EJqkBF;EIvkBA;IACE,cAAA;IACA,UAAA;EJykBF;EI3kBA;IACE,cAAA;IACA,UAAA;EJ6kBF;EI/kBA;IACE,cAAA;IACA,mBAAA;EJilBF;EIljBI;IAhDJ,cAAA;IACA,WAAA;EJqmBA;EIhjBQ;IAhEN,cAAA;IACA,kBAAA;EJmnBF;EIpjBQ;IAhEN,cAAA;IACA,mBAAA;EJunBF;EIxjBQ;IAhEN,cAAA;IACA,UAAA;EJ2nBF;EI5jBQ;IAhEN,cAAA;IACA,mBAAA;EJ+nBF;EIhkBQ;IAhEN,cAAA;IACA,mBAAA;EJmoBF;EIpkBQ;IAhEN,cAAA;IACA,UAAA;EJuoBF;EIxkBQ;IAhEN,cAAA;IACA,mBAAA;EJ2oBF;EI5kBQ;IAhEN,cAAA;IACA,mBAAA;EJ+oBF;EIhlBQ;IAhEN,cAAA;IACA,UAAA;EJmpBF;EIplBQ;IAhEN,cAAA;IACA,mBAAA;EJupBF;EIxlBQ;IAhEN,cAAA;IACA,mBAAA;EJ2pBF;EI5lBQ;IAhEN,cAAA;IACA,WAAA;EJ+pBF;EIxlBU;IAxDV,eAAA;EJmpBA;EI3lBU;IAxDV,yBAAA;EJspBA;EI9lBU;IAxDV,0BAAA;EJypBA;EIjmBU;IAxDV,iBAAA;EJ4pBA;EIpmBU;IAxDV,0BAAA;EJ+pBA;EIvmBU;IAxDV,0BAAA;EJkqBA;EI1mBU;IAxDV,iBAAA;EJqqBA;EI7mBU;IAxDV,0BAAA;EJwqBA;EIhnBU;IAxDV,0BAAA;EJ2qBA;EInnBU;IAxDV,iBAAA;EJ8qBA;EItnBU;IAxDV,0BAAA;EJirBA;EIznBU;IAxDV,0BAAA;EJorBA;EIjnBM;;IAEE,gBAAA;EJmnBR;EIhnBM;;IAEE,gBAAA;EJknBR;EIznBM;;IAEE,sBAAA;EJ2nBR;EIxnBM;;IAEE,sBAAA;EJ0nBR;EIjoBM;;IAEE,qBAAA;EJmoBR;EIhoBM;;IAEE,qBAAA;EJkoBR;EIzoBM;;IAEE,mBAAA;EJ2oBR;EIxoBM;;IAEE,mBAAA;EJ0oBR;EIjpBM;;IAEE,qBAAA;EJmpBR;EIhpBM;;IAEE,qBAAA;EJkpBR;EIzpBM;;IAEE,mBAAA;EJ2pBR;EIxpBM;;IAEE,mBAAA;EJ0pBR;AACF;ACrtBI;EGUE;IACE,WAAA;EJ8sBN;EI3sBI;IApCJ,cAAA;IACA,WAAA;EJkvBA;EIpuBA;IACE,cAAA;IACA,WAAA;EJsuBF;EIxuBA;IACE,cAAA;IACA,UAAA;EJ0uBF;EI5uBA;IACE,cAAA;IACA,mBAAA;EJ8uBF;EIhvBA;IACE,cAAA;IACA,UAAA;EJkvBF;EIpvBA;IACE,cAAA;IACA,UAAA;EJsvBF;EIxvBA;IACE,cAAA;IACA,mBAAA;EJ0vBF;EI3tBI;IAhDJ,cAAA;IACA,WAAA;EJ8wBA;EIztBQ;IAhEN,cAAA;IACA,kBAAA;EJ4xBF;EI7tBQ;IAhEN,cAAA;IACA,mBAAA;EJgyBF;EIjuBQ;IAhEN,cAAA;IACA,UAAA;EJoyBF;EIruBQ;IAhEN,cAAA;IACA,mBAAA;EJwyBF;EIzuBQ;IAhEN,cAAA;IACA,mBAAA;EJ4yBF;EI7uBQ;IAhEN,cAAA;IACA,UAAA;EJgzBF;EIjvBQ;IAhEN,cAAA;IACA,mBAAA;EJozBF;EIrvBQ;IAhEN,cAAA;IACA,mBAAA;EJwzBF;EIzvBQ;IAhEN,cAAA;IACA,UAAA;EJ4zBF;EI7vBQ;IAhEN,cAAA;IACA,mBAAA;EJg0BF;EIjwBQ;IAhEN,cAAA;IACA,mBAAA;EJo0BF;EIrwBQ;IAhEN,cAAA;IACA,WAAA;EJw0BF;EIjwBU;IAxDV,eAAA;EJ4zBA;EIpwBU;IAxDV,yBAAA;EJ+zBA;EIvwBU;IAxDV,0BAAA;EJk0BA;EI1wBU;IAxDV,iBAAA;EJq0BA;EI7wBU;IAxDV,0BAAA;EJw0BA;EIhxBU;IAxDV,0BAAA;EJ20BA;EInxBU;IAxDV,iBAAA;EJ80BA;EItxBU;IAxDV,0BAAA;EJi1BA;EIzxBU;IAxDV,0BAAA;EJo1BA;EI5xBU;IAxDV,iBAAA;EJu1BA;EI/xBU;IAxDV,0BAAA;EJ01BA;EIlyBU;IAxDV,0BAAA;EJ61BA;EI1xBM;;IAEE,gBAAA;EJ4xBR;EIzxBM;;IAEE,gBAAA;EJ2xBR;EIlyBM;;IAEE,sBAAA;EJoyBR;EIjyBM;;IAEE,sBAAA;EJmyBR;EI1yBM;;IAEE,qBAAA;EJ4yBR;EIzyBM;;IAEE,qBAAA;EJ2yBR;EIlzBM;;IAEE,mBAAA;EJozBR;EIjzBM;;IAEE,mBAAA;EJmzBR;EI1zBM;;IAEE,qBAAA;EJ4zBR;EIzzBM;;IAEE,qBAAA;EJ2zBR;EIl0BM;;IAEE,mBAAA;EJo0BR;EIj0BM;;IAEE,mBAAA;EJm0BR;AACF;AC93BI;EGUE;IACE,WAAA;EJu3BN;EIp3BI;IApCJ,cAAA;IACA,WAAA;EJ25BA;EI74BA;IACE,cAAA;IACA,WAAA;EJ+4BF;EIj5BA;IACE,cAAA;IACA,UAAA;EJm5BF;EIr5BA;IACE,cAAA;IACA,mBAAA;EJu5BF;EIz5BA;IACE,cAAA;IACA,UAAA;EJ25BF;EI75BA;IACE,cAAA;IACA,UAAA;EJ+5BF;EIj6BA;IACE,cAAA;IACA,mBAAA;EJm6BF;EIp4BI;IAhDJ,cAAA;IACA,WAAA;EJu7BA;EIl4BQ;IAhEN,cAAA;IACA,kBAAA;EJq8BF;EIt4BQ;IAhEN,cAAA;IACA,mBAAA;EJy8BF;EI14BQ;IAhEN,cAAA;IACA,UAAA;EJ68BF;EI94BQ;IAhEN,cAAA;IACA,mBAAA;EJi9BF;EIl5BQ;IAhEN,cAAA;IACA,mBAAA;EJq9BF;EIt5BQ;IAhEN,cAAA;IACA,UAAA;EJy9BF;EI15BQ;IAhEN,cAAA;IACA,mBAAA;EJ69BF;EI95BQ;IAhEN,cAAA;IACA,mBAAA;EJi+BF;EIl6BQ;IAhEN,cAAA;IACA,UAAA;EJq+BF;EIt6BQ;IAhEN,cAAA;IACA,mBAAA;EJy+BF;EI16BQ;IAhEN,cAAA;IACA,mBAAA;EJ6+BF;EI96BQ;IAhEN,cAAA;IACA,WAAA;EJi/BF;EI16BU;IAxDV,eAAA;EJq+BA;EI76BU;IAxDV,yBAAA;EJw+BA;EIh7BU;IAxDV,0BAAA;EJ2+BA;EIn7BU;IAxDV,iBAAA;EJ8+BA;EIt7BU;IAxDV,0BAAA;EJi/BA;EIz7BU;IAxDV,0BAAA;EJo/BA;EI57BU;IAxDV,iBAAA;EJu/BA;EI/7BU;IAxDV,0BAAA;EJ0/BA;EIl8BU;IAxDV,0BAAA;EJ6/BA;EIr8BU;IAxDV,iBAAA;EJggCA;EIx8BU;IAxDV,0BAAA;EJmgCA;EI38BU;IAxDV,0BAAA;EJsgCA;EIn8BM;;IAEE,gBAAA;EJq8BR;EIl8BM;;IAEE,gBAAA;EJo8BR;EI38BM;;IAEE,sBAAA;EJ68BR;EI18BM;;IAEE,sBAAA;EJ48BR;EIn9BM;;IAEE,qBAAA;EJq9BR;EIl9BM;;IAEE,qBAAA;EJo9BR;EI39BM;;IAEE,mBAAA;EJ69BR;EI19BM;;IAEE,mBAAA;EJ49BR;EIn+BM;;IAEE,qBAAA;EJq+BR;EIl+BM;;IAEE,qBAAA;EJo+BR;EI3+BM;;IAEE,mBAAA;EJ6+BR;EI1+BM;;IAEE,mBAAA;EJ4+BR;AACF;AKpiCQ;EAOI,0BAAA;ALgiCZ;;AKviCQ;EAOI,gCAAA;ALoiCZ;;AK3iCQ;EAOI,yBAAA;ALwiCZ;;AK/iCQ;EAOI,wBAAA;AL4iCZ;;AKnjCQ;EAOI,+BAAA;ALgjCZ;;AKvjCQ;EAOI,yBAAA;ALojCZ;;AK3jCQ;EAOI,6BAAA;ALwjCZ;;AK/jCQ;EAOI,8BAAA;AL4jCZ;;AKnkCQ;EAOI,wBAAA;ALgkCZ;;AKvkCQ;EAOI,+BAAA;ALokCZ;;AK3kCQ;EAOI,wBAAA;ALwkCZ;;AK/kCQ;EAOI,yBAAA;AL4kCZ;;AKnlCQ;EAOI,8BAAA;ALglCZ;;AKvlCQ;EAOI,iCAAA;ALolCZ;;AK3lCQ;EAOI,sCAAA;ALwlCZ;;AK/lCQ;EAOI,yCAAA;AL4lCZ;;AKnmCQ;EAOI,uBAAA;ALgmCZ;;AKvmCQ;EAOI,uBAAA;ALomCZ;;AK3mCQ;EAOI,yBAAA;ALwmCZ;;AK/mCQ;EAOI,yBAAA;AL4mCZ;;AKnnCQ;EAOI,0BAAA;ALgnCZ;;AKvnCQ;EAOI,4BAAA;ALonCZ;;AK3nCQ;EAOI,kCAAA;ALwnCZ;;AK/nCQ;EAOI,sCAAA;AL4nCZ;;AKnoCQ;EAOI,oCAAA;ALgoCZ;;AKvoCQ;EAOI,kCAAA;ALooCZ;;AK3oCQ;EAOI,yCAAA;ALwoCZ;;AK/oCQ;EAOI,wCAAA;AL4oCZ;;AKnpCQ;EAOI,wCAAA;ALgpCZ;;AKvpCQ;EAOI,kCAAA;ALopCZ;;AK3pCQ;EAOI,gCAAA;ALwpCZ;;AK/pCQ;EAOI,8BAAA;AL4pCZ;;AKnqCQ;EAOI,gCAAA;ALgqCZ;;AKvqCQ;EAOI,+BAAA;ALoqCZ;;AK3qCQ;EAOI,oCAAA;ALwqCZ;;AK/qCQ;EAOI,kCAAA;AL4qCZ;;AKnrCQ;EAOI,gCAAA;ALgrCZ;;AKvrCQ;EAOI,uCAAA;ALorCZ;;AK3rCQ;EAOI,sCAAA;ALwrCZ;;AK/rCQ;EAOI,iCAAA;AL4rCZ;;AKnsCQ;EAOI,2BAAA;ALgsCZ;;AKvsCQ;EAOI,iCAAA;ALosCZ;;AK3sCQ;EAOI,+BAAA;ALwsCZ;;AK/sCQ;EAOI,6BAAA;AL4sCZ;;AKntCQ;EAOI,+BAAA;ALgtCZ;;AKvtCQ;EAOI,8BAAA;ALotCZ;;AK3tCQ;EAOI,oBAAA;ALwtCZ;;AK/tCQ;EAOI,mBAAA;AL4tCZ;;AKnuCQ;EAOI,mBAAA;ALguCZ;;AKvuCQ;EAOI,mBAAA;ALouCZ;;AK3uCQ;EAOI,mBAAA;ALwuCZ;;AK/uCQ;EAOI,mBAAA;AL4uCZ;;AKnvCQ;EAOI,mBAAA;ALgvCZ;;AKvvCQ;EAOI,mBAAA;ALovCZ;;AK3vCQ;EAOI,oBAAA;ALwvCZ;;AK/vCQ;EAOI,0BAAA;AL4vCZ;;AKnwCQ;EAOI,yBAAA;ALgwCZ;;AKvwCQ;EAOI,uBAAA;ALowCZ;;AK3wCQ;EAOI,yBAAA;ALwwCZ;;AK/wCQ;EAOI,uBAAA;AL4wCZ;;AKnxCQ;EAOI,uBAAA;ALgxCZ;;AKvxCQ;EAOI,yBAAA;EAAA,0BAAA;ALqxCZ;;AK5xCQ;EAOI,+BAAA;EAAA,gCAAA;AL0xCZ;;AKjyCQ;EAOI,8BAAA;EAAA,+BAAA;AL+xCZ;;AKtyCQ;EAOI,4BAAA;EAAA,6BAAA;ALoyCZ;;AK3yCQ;EAOI,8BAAA;EAAA,+BAAA;ALyyCZ;;AKhzCQ;EAOI,4BAAA;EAAA,6BAAA;AL8yCZ;;AKrzCQ;EAOI,4BAAA;EAAA,6BAAA;ALmzCZ;;AK1zCQ;EAOI,wBAAA;EAAA,2BAAA;ALwzCZ;;AK/zCQ;EAOI,8BAAA;EAAA,iCAAA;AL6zCZ;;AKp0CQ;EAOI,6BAAA;EAAA,gCAAA;ALk0CZ;;AKz0CQ;EAOI,2BAAA;EAAA,8BAAA;ALu0CZ;;AK90CQ;EAOI,6BAAA;EAAA,gCAAA;AL40CZ;;AKn1CQ;EAOI,2BAAA;EAAA,8BAAA;ALi1CZ;;AKx1CQ;EAOI,2BAAA;EAAA,8BAAA;ALs1CZ;;AK71CQ;EAOI,wBAAA;AL01CZ;;AKj2CQ;EAOI,8BAAA;AL81CZ;;AKr2CQ;EAOI,6BAAA;ALk2CZ;;AKz2CQ;EAOI,2BAAA;ALs2CZ;;AK72CQ;EAOI,6BAAA;AL02CZ;;AKj3CQ;EAOI,2BAAA;AL82CZ;;AKr3CQ;EAOI,2BAAA;ALk3CZ;;AKz3CQ;EAOI,yBAAA;ALs3CZ;;AK73CQ;EAOI,+BAAA;AL03CZ;;AKj4CQ;EAOI,8BAAA;AL83CZ;;AKr4CQ;EAOI,4BAAA;ALk4CZ;;AKz4CQ;EAOI,8BAAA;ALs4CZ;;AK74CQ;EAOI,4BAAA;AL04CZ;;AKj5CQ;EAOI,4BAAA;AL84CZ;;AKr5CQ;EAOI,2BAAA;ALk5CZ;;AKz5CQ;EAOI,iCAAA;ALs5CZ;;AK75CQ;EAOI,gCAAA;AL05CZ;;AKj6CQ;EAOI,8BAAA;AL85CZ;;AKr6CQ;EAOI,gCAAA;ALk6CZ;;AKz6CQ;EAOI,8BAAA;ALs6CZ;;AK76CQ;EAOI,8BAAA;AL06CZ;;AKj7CQ;EAOI,0BAAA;AL86CZ;;AKr7CQ;EAOI,gCAAA;ALk7CZ;;AKz7CQ;EAOI,+BAAA;ALs7CZ;;AK77CQ;EAOI,6BAAA;AL07CZ;;AKj8CQ;EAOI,+BAAA;AL87CZ;;AKr8CQ;EAOI,6BAAA;ALk8CZ;;AKz8CQ;EAOI,6BAAA;ALs8CZ;;AK78CQ;EAOI,qBAAA;AL08CZ;;AKj9CQ;EAOI,2BAAA;AL88CZ;;AKr9CQ;EAOI,0BAAA;ALk9CZ;;AKz9CQ;EAOI,wBAAA;ALs9CZ;;AK79CQ;EAOI,0BAAA;AL09CZ;;AKj+CQ;EAOI,wBAAA;AL89CZ;;AKr+CQ;EAOI,0BAAA;EAAA,2BAAA;ALm+CZ;;AK1+CQ;EAOI,gCAAA;EAAA,iCAAA;ALw+CZ;;AK/+CQ;EAOI,+BAAA;EAAA,gCAAA;AL6+CZ;;AKp/CQ;EAOI,6BAAA;EAAA,8BAAA;ALk/CZ;;AKz/CQ;EAOI,+BAAA;EAAA,gCAAA;ALu/CZ;;AK9/CQ;EAOI,6BAAA;EAAA,8BAAA;AL4/CZ;;AKngDQ;EAOI,yBAAA;EAAA,4BAAA;ALigDZ;;AKxgDQ;EAOI,+BAAA;EAAA,kCAAA;ALsgDZ;;AK7gDQ;EAOI,8BAAA;EAAA,iCAAA;AL2gDZ;;AKlhDQ;EAOI,4BAAA;EAAA,+BAAA;ALghDZ;;AKvhDQ;EAOI,8BAAA;EAAA,iCAAA;ALqhDZ;;AK5hDQ;EAOI,4BAAA;EAAA,+BAAA;AL0hDZ;;AKjiDQ;EAOI,yBAAA;AL8hDZ;;AKriDQ;EAOI,+BAAA;ALkiDZ;;AKziDQ;EAOI,8BAAA;ALsiDZ;;AK7iDQ;EAOI,4BAAA;AL0iDZ;;AKjjDQ;EAOI,8BAAA;AL8iDZ;;AKrjDQ;EAOI,4BAAA;ALkjDZ;;AKzjDQ;EAOI,0BAAA;ALsjDZ;;AK7jDQ;EAOI,gCAAA;AL0jDZ;;AKjkDQ;EAOI,+BAAA;AL8jDZ;;AKrkDQ;EAOI,6BAAA;ALkkDZ;;AKzkDQ;EAOI,+BAAA;ALskDZ;;AK7kDQ;EAOI,6BAAA;AL0kDZ;;AKjlDQ;EAOI,4BAAA;AL8kDZ;;AKrlDQ;EAOI,kCAAA;ALklDZ;;AKzlDQ;EAOI,iCAAA;ALslDZ;;AK7lDQ;EAOI,+BAAA;AL0lDZ;;AKjmDQ;EAOI,iCAAA;AL8lDZ;;AKrmDQ;EAOI,+BAAA;ALkmDZ;;AKzmDQ;EAOI,2BAAA;ALsmDZ;;AK7mDQ;EAOI,iCAAA;AL0mDZ;;AKjnDQ;EAOI,gCAAA;AL8mDZ;;AKrnDQ;EAOI,8BAAA;ALknDZ;;AKznDQ;EAOI,gCAAA;ALsnDZ;;AK7nDQ;EAOI,8BAAA;AL0nDZ;;ACpoDI;EIGI;IAOI,0BAAA;EL+nDV;EKtoDM;IAOI,gCAAA;ELkoDV;EKzoDM;IAOI,yBAAA;ELqoDV;EK5oDM;IAOI,wBAAA;ELwoDV;EK/oDM;IAOI,+BAAA;EL2oDV;EKlpDM;IAOI,yBAAA;EL8oDV;EKrpDM;IAOI,6BAAA;ELipDV;EKxpDM;IAOI,8BAAA;ELopDV;EK3pDM;IAOI,wBAAA;ELupDV;EK9pDM;IAOI,+BAAA;EL0pDV;EKjqDM;IAOI,wBAAA;EL6pDV;EKpqDM;IAOI,yBAAA;ELgqDV;EKvqDM;IAOI,8BAAA;ELmqDV;EK1qDM;IAOI,iCAAA;ELsqDV;EK7qDM;IAOI,sCAAA;ELyqDV;EKhrDM;IAOI,yCAAA;EL4qDV;EKnrDM;IAOI,uBAAA;EL+qDV;EKtrDM;IAOI,uBAAA;ELkrDV;EKzrDM;IAOI,yBAAA;ELqrDV;EK5rDM;IAOI,yBAAA;ELwrDV;EK/rDM;IAOI,0BAAA;EL2rDV;EKlsDM;IAOI,4BAAA;EL8rDV;EKrsDM;IAOI,kCAAA;ELisDV;EKxsDM;IAOI,sCAAA;ELosDV;EK3sDM;IAOI,oCAAA;ELusDV;EK9sDM;IAOI,kCAAA;EL0sDV;EKjtDM;IAOI,yCAAA;EL6sDV;EKptDM;IAOI,wCAAA;ELgtDV;EKvtDM;IAOI,wCAAA;ELmtDV;EK1tDM;IAOI,kCAAA;ELstDV;EK7tDM;IAOI,gCAAA;ELytDV;EKhuDM;IAOI,8BAAA;EL4tDV;EKnuDM;IAOI,gCAAA;EL+tDV;EKtuDM;IAOI,+BAAA;ELkuDV;EKzuDM;IAOI,oCAAA;ELquDV;EK5uDM;IAOI,kCAAA;ELwuDV;EK/uDM;IAOI,gCAAA;EL2uDV;EKlvDM;IAOI,uCAAA;EL8uDV;EKrvDM;IAOI,sCAAA;ELivDV;EKxvDM;IAOI,iCAAA;ELovDV;EK3vDM;IAOI,2BAAA;ELuvDV;EK9vDM;IAOI,iCAAA;EL0vDV;EKjwDM;IAOI,+BAAA;EL6vDV;EKpwDM;IAOI,6BAAA;ELgwDV;EKvwDM;IAOI,+BAAA;ELmwDV;EK1wDM;IAOI,8BAAA;ELswDV;EK7wDM;IAOI,oBAAA;ELywDV;EKhxDM;IAOI,mBAAA;EL4wDV;EKnxDM;IAOI,mBAAA;EL+wDV;EKtxDM;IAOI,mBAAA;ELkxDV;EKzxDM;IAOI,mBAAA;ELqxDV;EK5xDM;IAOI,mBAAA;ELwxDV;EK/xDM;IAOI,mBAAA;EL2xDV;EKlyDM;IAOI,mBAAA;EL8xDV;EKryDM;IAOI,oBAAA;ELiyDV;EKxyDM;IAOI,0BAAA;ELoyDV;EK3yDM;IAOI,yBAAA;ELuyDV;EK9yDM;IAOI,uBAAA;EL0yDV;EKjzDM;IAOI,yBAAA;EL6yDV;EKpzDM;IAOI,uBAAA;ELgzDV;EKvzDM;IAOI,uBAAA;ELmzDV;EK1zDM;IAOI,yBAAA;IAAA,0BAAA;ELuzDV;EK9zDM;IAOI,+BAAA;IAAA,gCAAA;EL2zDV;EKl0DM;IAOI,8BAAA;IAAA,+BAAA;EL+zDV;EKt0DM;IAOI,4BAAA;IAAA,6BAAA;ELm0DV;EK10DM;IAOI,8BAAA;IAAA,+BAAA;ELu0DV;EK90DM;IAOI,4BAAA;IAAA,6BAAA;EL20DV;EKl1DM;IAOI,4BAAA;IAAA,6BAAA;EL+0DV;EKt1DM;IAOI,wBAAA;IAAA,2BAAA;ELm1DV;EK11DM;IAOI,8BAAA;IAAA,iCAAA;ELu1DV;EK91DM;IAOI,6BAAA;IAAA,gCAAA;EL21DV;EKl2DM;IAOI,2BAAA;IAAA,8BAAA;EL+1DV;EKt2DM;IAOI,6BAAA;IAAA,gCAAA;ELm2DV;EK12DM;IAOI,2BAAA;IAAA,8BAAA;ELu2DV;EK92DM;IAOI,2BAAA;IAAA,8BAAA;EL22DV;EKl3DM;IAOI,wBAAA;EL82DV;EKr3DM;IAOI,8BAAA;ELi3DV;EKx3DM;IAOI,6BAAA;ELo3DV;EK33DM;IAOI,2BAAA;ELu3DV;EK93DM;IAOI,6BAAA;EL03DV;EKj4DM;IAOI,2BAAA;EL63DV;EKp4DM;IAOI,2BAAA;ELg4DV;EKv4DM;IAOI,yBAAA;ELm4DV;EK14DM;IAOI,+BAAA;ELs4DV;EK74DM;IAOI,8BAAA;ELy4DV;EKh5DM;IAOI,4BAAA;EL44DV;EKn5DM;IAOI,8BAAA;EL+4DV;EKt5DM;IAOI,4BAAA;ELk5DV;EKz5DM;IAOI,4BAAA;ELq5DV;EK55DM;IAOI,2BAAA;ELw5DV;EK/5DM;IAOI,iCAAA;EL25DV;EKl6DM;IAOI,gCAAA;EL85DV;EKr6DM;IAOI,8BAAA;ELi6DV;EKx6DM;IAOI,gCAAA;ELo6DV;EK36DM;IAOI,8BAAA;ELu6DV;EK96DM;IAOI,8BAAA;EL06DV;EKj7DM;IAOI,0BAAA;EL66DV;EKp7DM;IAOI,gCAAA;ELg7DV;EKv7DM;IAOI,+BAAA;ELm7DV;EK17DM;IAOI,6BAAA;ELs7DV;EK77DM;IAOI,+BAAA;ELy7DV;EKh8DM;IAOI,6BAAA;EL47DV;EKn8DM;IAOI,6BAAA;EL+7DV;EKt8DM;IAOI,qBAAA;ELk8DV;EKz8DM;IAOI,2BAAA;ELq8DV;EK58DM;IAOI,0BAAA;ELw8DV;EK/8DM;IAOI,wBAAA;EL28DV;EKl9DM;IAOI,0BAAA;EL88DV;EKr9DM;IAOI,wBAAA;ELi9DV;EKx9DM;IAOI,0BAAA;IAAA,2BAAA;ELq9DV;EK59DM;IAOI,gCAAA;IAAA,iCAAA;ELy9DV;EKh+DM;IAOI,+BAAA;IAAA,gCAAA;EL69DV;EKp+DM;IAOI,6BAAA;IAAA,8BAAA;ELi+DV;EKx+DM;IAOI,+BAAA;IAAA,gCAAA;ELq+DV;EK5+DM;IAOI,6BAAA;IAAA,8BAAA;ELy+DV;EKh/DM;IAOI,yBAAA;IAAA,4BAAA;EL6+DV;EKp/DM;IAOI,+BAAA;IAAA,kCAAA;ELi/DV;EKx/DM;IAOI,8BAAA;IAAA,iCAAA;ELq/DV;EK5/DM;IAOI,4BAAA;IAAA,+BAAA;ELy/DV;EKhgEM;IAOI,8BAAA;IAAA,iCAAA;EL6/DV;EKpgEM;IAOI,4BAAA;IAAA,+BAAA;ELigEV;EKxgEM;IAOI,yBAAA;ELogEV;EK3gEM;IAOI,+BAAA;ELugEV;EK9gEM;IAOI,8BAAA;EL0gEV;EKjhEM;IAOI,4BAAA;EL6gEV;EKphEM;IAOI,8BAAA;ELghEV;EKvhEM;IAOI,4BAAA;ELmhEV;EK1hEM;IAOI,0BAAA;ELshEV;EK7hEM;IAOI,gCAAA;ELyhEV;EKhiEM;IAOI,+BAAA;EL4hEV;EKniEM;IAOI,6BAAA;EL+hEV;EKtiEM;IAOI,+BAAA;ELkiEV;EKziEM;IAOI,6BAAA;ELqiEV;EK5iEM;IAOI,4BAAA;ELwiEV;EK/iEM;IAOI,kCAAA;EL2iEV;EKljEM;IAOI,iCAAA;EL8iEV;EKrjEM;IAOI,+BAAA;ELijEV;EKxjEM;IAOI,iCAAA;ELojEV;EK3jEM;IAOI,+BAAA;ELujEV;EK9jEM;IAOI,2BAAA;EL0jEV;EKjkEM;IAOI,iCAAA;EL6jEV;EKpkEM;IAOI,gCAAA;ELgkEV;EKvkEM;IAOI,8BAAA;ELmkEV;EK1kEM;IAOI,gCAAA;ELskEV;EK7kEM;IAOI,8BAAA;ELykEV;AACF;ACplEI;EIGI;IAOI,0BAAA;EL8kEV;EKrlEM;IAOI,gCAAA;ELilEV;EKxlEM;IAOI,yBAAA;ELolEV;EK3lEM;IAOI,wBAAA;ELulEV;EK9lEM;IAOI,+BAAA;EL0lEV;EKjmEM;IAOI,yBAAA;EL6lEV;EKpmEM;IAOI,6BAAA;ELgmEV;EKvmEM;IAOI,8BAAA;ELmmEV;EK1mEM;IAOI,wBAAA;ELsmEV;EK7mEM;IAOI,+BAAA;ELymEV;EKhnEM;IAOI,wBAAA;EL4mEV;EKnnEM;IAOI,yBAAA;EL+mEV;EKtnEM;IAOI,8BAAA;ELknEV;EKznEM;IAOI,iCAAA;ELqnEV;EK5nEM;IAOI,sCAAA;ELwnEV;EK/nEM;IAOI,yCAAA;EL2nEV;EKloEM;IAOI,uBAAA;EL8nEV;EKroEM;IAOI,uBAAA;ELioEV;EKxoEM;IAOI,yBAAA;ELooEV;EK3oEM;IAOI,yBAAA;ELuoEV;EK9oEM;IAOI,0BAAA;EL0oEV;EKjpEM;IAOI,4BAAA;EL6oEV;EKppEM;IAOI,kCAAA;ELgpEV;EKvpEM;IAOI,sCAAA;ELmpEV;EK1pEM;IAOI,oCAAA;ELspEV;EK7pEM;IAOI,kCAAA;ELypEV;EKhqEM;IAOI,yCAAA;EL4pEV;EKnqEM;IAOI,wCAAA;EL+pEV;EKtqEM;IAOI,wCAAA;ELkqEV;EKzqEM;IAOI,kCAAA;ELqqEV;EK5qEM;IAOI,gCAAA;ELwqEV;EK/qEM;IAOI,8BAAA;EL2qEV;EKlrEM;IAOI,gCAAA;EL8qEV;EKrrEM;IAOI,+BAAA;ELirEV;EKxrEM;IAOI,oCAAA;ELorEV;EK3rEM;IAOI,kCAAA;ELurEV;EK9rEM;IAOI,gCAAA;EL0rEV;EKjsEM;IAOI,uCAAA;EL6rEV;EKpsEM;IAOI,sCAAA;ELgsEV;EKvsEM;IAOI,iCAAA;ELmsEV;EK1sEM;IAOI,2BAAA;ELssEV;EK7sEM;IAOI,iCAAA;ELysEV;EKhtEM;IAOI,+BAAA;EL4sEV;EKntEM;IAOI,6BAAA;EL+sEV;EKttEM;IAOI,+BAAA;ELktEV;EKztEM;IAOI,8BAAA;ELqtEV;EK5tEM;IAOI,oBAAA;ELwtEV;EK/tEM;IAOI,mBAAA;EL2tEV;EKluEM;IAOI,mBAAA;EL8tEV;EKruEM;IAOI,mBAAA;ELiuEV;EKxuEM;IAOI,mBAAA;ELouEV;EK3uEM;IAOI,mBAAA;ELuuEV;EK9uEM;IAOI,mBAAA;EL0uEV;EKjvEM;IAOI,mBAAA;EL6uEV;EKpvEM;IAOI,oBAAA;ELgvEV;EKvvEM;IAOI,0BAAA;ELmvEV;EK1vEM;IAOI,yBAAA;ELsvEV;EK7vEM;IAOI,uBAAA;ELyvEV;EKhwEM;IAOI,yBAAA;EL4vEV;EKnwEM;IAOI,uBAAA;EL+vEV;EKtwEM;IAOI,uBAAA;ELkwEV;EKzwEM;IAOI,yBAAA;IAAA,0BAAA;ELswEV;EK7wEM;IAOI,+BAAA;IAAA,gCAAA;EL0wEV;EKjxEM;IAOI,8BAAA;IAAA,+BAAA;EL8wEV;EKrxEM;IAOI,4BAAA;IAAA,6BAAA;ELkxEV;EKzxEM;IAOI,8BAAA;IAAA,+BAAA;ELsxEV;EK7xEM;IAOI,4BAAA;IAAA,6BAAA;EL0xEV;EKjyEM;IAOI,4BAAA;IAAA,6BAAA;EL8xEV;EKryEM;IAOI,wBAAA;IAAA,2BAAA;ELkyEV;EKzyEM;IAOI,8BAAA;IAAA,iCAAA;ELsyEV;EK7yEM;IAOI,6BAAA;IAAA,gCAAA;EL0yEV;EKjzEM;IAOI,2BAAA;IAAA,8BAAA;EL8yEV;EKrzEM;IAOI,6BAAA;IAAA,gCAAA;ELkzEV;EKzzEM;IAOI,2BAAA;IAAA,8BAAA;ELszEV;EK7zEM;IAOI,2BAAA;IAAA,8BAAA;EL0zEV;EKj0EM;IAOI,wBAAA;EL6zEV;EKp0EM;IAOI,8BAAA;ELg0EV;EKv0EM;IAOI,6BAAA;ELm0EV;EK10EM;IAOI,2BAAA;ELs0EV;EK70EM;IAOI,6BAAA;ELy0EV;EKh1EM;IAOI,2BAAA;EL40EV;EKn1EM;IAOI,2BAAA;EL+0EV;EKt1EM;IAOI,yBAAA;ELk1EV;EKz1EM;IAOI,+BAAA;ELq1EV;EK51EM;IAOI,8BAAA;ELw1EV;EK/1EM;IAOI,4BAAA;EL21EV;EKl2EM;IAOI,8BAAA;EL81EV;EKr2EM;IAOI,4BAAA;ELi2EV;EKx2EM;IAOI,4BAAA;ELo2EV;EK32EM;IAOI,2BAAA;ELu2EV;EK92EM;IAOI,iCAAA;EL02EV;EKj3EM;IAOI,gCAAA;EL62EV;EKp3EM;IAOI,8BAAA;ELg3EV;EKv3EM;IAOI,gCAAA;ELm3EV;EK13EM;IAOI,8BAAA;ELs3EV;EK73EM;IAOI,8BAAA;ELy3EV;EKh4EM;IAOI,0BAAA;EL43EV;EKn4EM;IAOI,gCAAA;EL+3EV;EKt4EM;IAOI,+BAAA;ELk4EV;EKz4EM;IAOI,6BAAA;ELq4EV;EK54EM;IAOI,+BAAA;ELw4EV;EK/4EM;IAOI,6BAAA;EL24EV;EKl5EM;IAOI,6BAAA;EL84EV;EKr5EM;IAOI,qBAAA;ELi5EV;EKx5EM;IAOI,2BAAA;ELo5EV;EK35EM;IAOI,0BAAA;ELu5EV;EK95EM;IAOI,wBAAA;EL05EV;EKj6EM;IAOI,0BAAA;EL65EV;EKp6EM;IAOI,wBAAA;ELg6EV;EKv6EM;IAOI,0BAAA;IAAA,2BAAA;ELo6EV;EK36EM;IAOI,gCAAA;IAAA,iCAAA;ELw6EV;EK/6EM;IAOI,+BAAA;IAAA,gCAAA;EL46EV;EKn7EM;IAOI,6BAAA;IAAA,8BAAA;ELg7EV;EKv7EM;IAOI,+BAAA;IAAA,gCAAA;ELo7EV;EK37EM;IAOI,6BAAA;IAAA,8BAAA;ELw7EV;EK/7EM;IAOI,yBAAA;IAAA,4BAAA;EL47EV;EKn8EM;IAOI,+BAAA;IAAA,kCAAA;ELg8EV;EKv8EM;IAOI,8BAAA;IAAA,iCAAA;ELo8EV;EK38EM;IAOI,4BAAA;IAAA,+BAAA;ELw8EV;EK/8EM;IAOI,8BAAA;IAAA,iCAAA;EL48EV;EKn9EM;IAOI,4BAAA;IAAA,+BAAA;ELg9EV;EKv9EM;IAOI,yBAAA;ELm9EV;EK19EM;IAOI,+BAAA;ELs9EV;EK79EM;IAOI,8BAAA;ELy9EV;EKh+EM;IAOI,4BAAA;EL49EV;EKn+EM;IAOI,8BAAA;EL+9EV;EKt+EM;IAOI,4BAAA;ELk+EV;EKz+EM;IAOI,0BAAA;ELq+EV;EK5+EM;IAOI,gCAAA;ELw+EV;EK/+EM;IAOI,+BAAA;EL2+EV;EKl/EM;IAOI,6BAAA;EL8+EV;EKr/EM;IAOI,+BAAA;ELi/EV;EKx/EM;IAOI,6BAAA;ELo/EV;EK3/EM;IAOI,4BAAA;ELu/EV;EK9/EM;IAOI,kCAAA;EL0/EV;EKjgFM;IAOI,iCAAA;EL6/EV;EKpgFM;IAOI,+BAAA;ELggFV;EKvgFM;IAOI,iCAAA;ELmgFV;EK1gFM;IAOI,+BAAA;ELsgFV;EK7gFM;IAOI,2BAAA;ELygFV;EKhhFM;IAOI,iCAAA;EL4gFV;EKnhFM;IAOI,gCAAA;EL+gFV;EKthFM;IAOI,8BAAA;ELkhFV;EKzhFM;IAOI,gCAAA;ELqhFV;EK5hFM;IAOI,8BAAA;ELwhFV;AACF;ACniFI;EIGI;IAOI,0BAAA;EL6hFV;EKpiFM;IAOI,gCAAA;ELgiFV;EKviFM;IAOI,yBAAA;ELmiFV;EK1iFM;IAOI,wBAAA;ELsiFV;EK7iFM;IAOI,+BAAA;ELyiFV;EKhjFM;IAOI,yBAAA;EL4iFV;EKnjFM;IAOI,6BAAA;EL+iFV;EKtjFM;IAOI,8BAAA;ELkjFV;EKzjFM;IAOI,wBAAA;ELqjFV;EK5jFM;IAOI,+BAAA;ELwjFV;EK/jFM;IAOI,wBAAA;EL2jFV;EKlkFM;IAOI,yBAAA;EL8jFV;EKrkFM;IAOI,8BAAA;ELikFV;EKxkFM;IAOI,iCAAA;ELokFV;EK3kFM;IAOI,sCAAA;ELukFV;EK9kFM;IAOI,yCAAA;EL0kFV;EKjlFM;IAOI,uBAAA;EL6kFV;EKplFM;IAOI,uBAAA;ELglFV;EKvlFM;IAOI,yBAAA;ELmlFV;EK1lFM;IAOI,yBAAA;ELslFV;EK7lFM;IAOI,0BAAA;ELylFV;EKhmFM;IAOI,4BAAA;EL4lFV;EKnmFM;IAOI,kCAAA;EL+lFV;EKtmFM;IAOI,sCAAA;ELkmFV;EKzmFM;IAOI,oCAAA;ELqmFV;EK5mFM;IAOI,kCAAA;ELwmFV;EK/mFM;IAOI,yCAAA;EL2mFV;EKlnFM;IAOI,wCAAA;EL8mFV;EKrnFM;IAOI,wCAAA;ELinFV;EKxnFM;IAOI,kCAAA;ELonFV;EK3nFM;IAOI,gCAAA;ELunFV;EK9nFM;IAOI,8BAAA;EL0nFV;EKjoFM;IAOI,gCAAA;EL6nFV;EKpoFM;IAOI,+BAAA;ELgoFV;EKvoFM;IAOI,oCAAA;ELmoFV;EK1oFM;IAOI,kCAAA;ELsoFV;EK7oFM;IAOI,gCAAA;ELyoFV;EKhpFM;IAOI,uCAAA;EL4oFV;EKnpFM;IAOI,sCAAA;EL+oFV;EKtpFM;IAOI,iCAAA;ELkpFV;EKzpFM;IAOI,2BAAA;ELqpFV;EK5pFM;IAOI,iCAAA;ELwpFV;EK/pFM;IAOI,+BAAA;EL2pFV;EKlqFM;IAOI,6BAAA;EL8pFV;EKrqFM;IAOI,+BAAA;ELiqFV;EKxqFM;IAOI,8BAAA;ELoqFV;EK3qFM;IAOI,oBAAA;ELuqFV;EK9qFM;IAOI,mBAAA;EL0qFV;EKjrFM;IAOI,mBAAA;EL6qFV;EKprFM;IAOI,mBAAA;ELgrFV;EKvrFM;IAOI,mBAAA;ELmrFV;EK1rFM;IAOI,mBAAA;ELsrFV;EK7rFM;IAOI,mBAAA;ELyrFV;EKhsFM;IAOI,mBAAA;EL4rFV;EKnsFM;IAOI,oBAAA;EL+rFV;EKtsFM;IAOI,0BAAA;ELksFV;EKzsFM;IAOI,yBAAA;ELqsFV;EK5sFM;IAOI,uBAAA;ELwsFV;EK/sFM;IAOI,yBAAA;EL2sFV;EKltFM;IAOI,uBAAA;EL8sFV;EKrtFM;IAOI,uBAAA;ELitFV;EKxtFM;IAOI,yBAAA;IAAA,0BAAA;ELqtFV;EK5tFM;IAOI,+BAAA;IAAA,gCAAA;ELytFV;EKhuFM;IAOI,8BAAA;IAAA,+BAAA;EL6tFV;EKpuFM;IAOI,4BAAA;IAAA,6BAAA;ELiuFV;EKxuFM;IAOI,8BAAA;IAAA,+BAAA;ELquFV;EK5uFM;IAOI,4BAAA;IAAA,6BAAA;ELyuFV;EKhvFM;IAOI,4BAAA;IAAA,6BAAA;EL6uFV;EKpvFM;IAOI,wBAAA;IAAA,2BAAA;ELivFV;EKxvFM;IAOI,8BAAA;IAAA,iCAAA;ELqvFV;EK5vFM;IAOI,6BAAA;IAAA,gCAAA;ELyvFV;EKhwFM;IAOI,2BAAA;IAAA,8BAAA;EL6vFV;EKpwFM;IAOI,6BAAA;IAAA,gCAAA;ELiwFV;EKxwFM;IAOI,2BAAA;IAAA,8BAAA;ELqwFV;EK5wFM;IAOI,2BAAA;IAAA,8BAAA;ELywFV;EKhxFM;IAOI,wBAAA;EL4wFV;EKnxFM;IAOI,8BAAA;EL+wFV;EKtxFM;IAOI,6BAAA;ELkxFV;EKzxFM;IAOI,2BAAA;ELqxFV;EK5xFM;IAOI,6BAAA;ELwxFV;EK/xFM;IAOI,2BAAA;EL2xFV;EKlyFM;IAOI,2BAAA;EL8xFV;EKryFM;IAOI,yBAAA;ELiyFV;EKxyFM;IAOI,+BAAA;ELoyFV;EK3yFM;IAOI,8BAAA;ELuyFV;EK9yFM;IAOI,4BAAA;EL0yFV;EKjzFM;IAOI,8BAAA;EL6yFV;EKpzFM;IAOI,4BAAA;ELgzFV;EKvzFM;IAOI,4BAAA;ELmzFV;EK1zFM;IAOI,2BAAA;ELszFV;EK7zFM;IAOI,iCAAA;ELyzFV;EKh0FM;IAOI,gCAAA;EL4zFV;EKn0FM;IAOI,8BAAA;EL+zFV;EKt0FM;IAOI,gCAAA;ELk0FV;EKz0FM;IAOI,8BAAA;ELq0FV;EK50FM;IAOI,8BAAA;ELw0FV;EK/0FM;IAOI,0BAAA;EL20FV;EKl1FM;IAOI,gCAAA;EL80FV;EKr1FM;IAOI,+BAAA;ELi1FV;EKx1FM;IAOI,6BAAA;ELo1FV;EK31FM;IAOI,+BAAA;ELu1FV;EK91FM;IAOI,6BAAA;EL01FV;EKj2FM;IAOI,6BAAA;EL61FV;EKp2FM;IAOI,qBAAA;ELg2FV;EKv2FM;IAOI,2BAAA;ELm2FV;EK12FM;IAOI,0BAAA;ELs2FV;EK72FM;IAOI,wBAAA;ELy2FV;EKh3FM;IAOI,0BAAA;EL42FV;EKn3FM;IAOI,wBAAA;EL+2FV;EKt3FM;IAOI,0BAAA;IAAA,2BAAA;ELm3FV;EK13FM;IAOI,gCAAA;IAAA,iCAAA;ELu3FV;EK93FM;IAOI,+BAAA;IAAA,gCAAA;EL23FV;EKl4FM;IAOI,6BAAA;IAAA,8BAAA;EL+3FV;EKt4FM;IAOI,+BAAA;IAAA,gCAAA;ELm4FV;EK14FM;IAOI,6BAAA;IAAA,8BAAA;ELu4FV;EK94FM;IAOI,yBAAA;IAAA,4BAAA;EL24FV;EKl5FM;IAOI,+BAAA;IAAA,kCAAA;EL+4FV;EKt5FM;IAOI,8BAAA;IAAA,iCAAA;ELm5FV;EK15FM;IAOI,4BAAA;IAAA,+BAAA;ELu5FV;EK95FM;IAOI,8BAAA;IAAA,iCAAA;EL25FV;EKl6FM;IAOI,4BAAA;IAAA,+BAAA;EL+5FV;EKt6FM;IAOI,yBAAA;ELk6FV;EKz6FM;IAOI,+BAAA;ELq6FV;EK56FM;IAOI,8BAAA;ELw6FV;EK/6FM;IAOI,4BAAA;EL26FV;EKl7FM;IAOI,8BAAA;EL86FV;EKr7FM;IAOI,4BAAA;ELi7FV;EKx7FM;IAOI,0BAAA;ELo7FV;EK37FM;IAOI,gCAAA;ELu7FV;EK97FM;IAOI,+BAAA;EL07FV;EKj8FM;IAOI,6BAAA;EL67FV;EKp8FM;IAOI,+BAAA;ELg8FV;EKv8FM;IAOI,6BAAA;ELm8FV;EK18FM;IAOI,4BAAA;ELs8FV;EK78FM;IAOI,kCAAA;ELy8FV;EKh9FM;IAOI,iCAAA;EL48FV;EKn9FM;IAOI,+BAAA;EL+8FV;EKt9FM;IAOI,iCAAA;ELk9FV;EKz9FM;IAOI,+BAAA;ELq9FV;EK59FM;IAOI,2BAAA;ELw9FV;EK/9FM;IAOI,iCAAA;EL29FV;EKl+FM;IAOI,gCAAA;EL89FV;EKr+FM;IAOI,8BAAA;ELi+FV;EKx+FM;IAOI,gCAAA;ELo+FV;EK3+FM;IAOI,8BAAA;ELu+FV;AACF;ACl/FI;EIGI;IAOI,0BAAA;EL4+FV;EKn/FM;IAOI,gCAAA;EL++FV;EKt/FM;IAOI,yBAAA;ELk/FV;EKz/FM;IAOI,wBAAA;ELq/FV;EK5/FM;IAOI,+BAAA;ELw/FV;EK//FM;IAOI,yBAAA;EL2/FV;EKlgGM;IAOI,6BAAA;EL8/FV;EKrgGM;IAOI,8BAAA;ELigGV;EKxgGM;IAOI,wBAAA;ELogGV;EK3gGM;IAOI,+BAAA;ELugGV;EK9gGM;IAOI,wBAAA;EL0gGV;EKjhGM;IAOI,yBAAA;EL6gGV;EKphGM;IAOI,8BAAA;ELghGV;EKvhGM;IAOI,iCAAA;ELmhGV;EK1hGM;IAOI,sCAAA;ELshGV;EK7hGM;IAOI,yCAAA;ELyhGV;EKhiGM;IAOI,uBAAA;EL4hGV;EKniGM;IAOI,uBAAA;EL+hGV;EKtiGM;IAOI,yBAAA;ELkiGV;EKziGM;IAOI,yBAAA;ELqiGV;EK5iGM;IAOI,0BAAA;ELwiGV;EK/iGM;IAOI,4BAAA;EL2iGV;EKljGM;IAOI,kCAAA;EL8iGV;EKrjGM;IAOI,sCAAA;ELijGV;EKxjGM;IAOI,oCAAA;ELojGV;EK3jGM;IAOI,kCAAA;ELujGV;EK9jGM;IAOI,yCAAA;EL0jGV;EKjkGM;IAOI,wCAAA;EL6jGV;EKpkGM;IAOI,wCAAA;ELgkGV;EKvkGM;IAOI,kCAAA;ELmkGV;EK1kGM;IAOI,gCAAA;ELskGV;EK7kGM;IAOI,8BAAA;ELykGV;EKhlGM;IAOI,gCAAA;EL4kGV;EKnlGM;IAOI,+BAAA;EL+kGV;EKtlGM;IAOI,oCAAA;ELklGV;EKzlGM;IAOI,kCAAA;ELqlGV;EK5lGM;IAOI,gCAAA;ELwlGV;EK/lGM;IAOI,uCAAA;EL2lGV;EKlmGM;IAOI,sCAAA;EL8lGV;EKrmGM;IAOI,iCAAA;ELimGV;EKxmGM;IAOI,2BAAA;ELomGV;EK3mGM;IAOI,iCAAA;ELumGV;EK9mGM;IAOI,+BAAA;EL0mGV;EKjnGM;IAOI,6BAAA;EL6mGV;EKpnGM;IAOI,+BAAA;ELgnGV;EKvnGM;IAOI,8BAAA;ELmnGV;EK1nGM;IAOI,oBAAA;ELsnGV;EK7nGM;IAOI,mBAAA;ELynGV;EKhoGM;IAOI,mBAAA;EL4nGV;EKnoGM;IAOI,mBAAA;EL+nGV;EKtoGM;IAOI,mBAAA;ELkoGV;EKzoGM;IAOI,mBAAA;ELqoGV;EK5oGM;IAOI,mBAAA;ELwoGV;EK/oGM;IAOI,mBAAA;EL2oGV;EKlpGM;IAOI,oBAAA;EL8oGV;EKrpGM;IAOI,0BAAA;ELipGV;EKxpGM;IAOI,yBAAA;ELopGV;EK3pGM;IAOI,uBAAA;ELupGV;EK9pGM;IAOI,yBAAA;EL0pGV;EKjqGM;IAOI,uBAAA;EL6pGV;EKpqGM;IAOI,uBAAA;ELgqGV;EKvqGM;IAOI,yBAAA;IAAA,0BAAA;ELoqGV;EK3qGM;IAOI,+BAAA;IAAA,gCAAA;ELwqGV;EK/qGM;IAOI,8BAAA;IAAA,+BAAA;EL4qGV;EKnrGM;IAOI,4BAAA;IAAA,6BAAA;ELgrGV;EKvrGM;IAOI,8BAAA;IAAA,+BAAA;ELorGV;EK3rGM;IAOI,4BAAA;IAAA,6BAAA;ELwrGV;EK/rGM;IAOI,4BAAA;IAAA,6BAAA;EL4rGV;EKnsGM;IAOI,wBAAA;IAAA,2BAAA;ELgsGV;EKvsGM;IAOI,8BAAA;IAAA,iCAAA;ELosGV;EK3sGM;IAOI,6BAAA;IAAA,gCAAA;ELwsGV;EK/sGM;IAOI,2BAAA;IAAA,8BAAA;EL4sGV;EKntGM;IAOI,6BAAA;IAAA,gCAAA;ELgtGV;EKvtGM;IAOI,2BAAA;IAAA,8BAAA;ELotGV;EK3tGM;IAOI,2BAAA;IAAA,8BAAA;ELwtGV;EK/tGM;IAOI,wBAAA;EL2tGV;EKluGM;IAOI,8BAAA;EL8tGV;EKruGM;IAOI,6BAAA;ELiuGV;EKxuGM;IAOI,2BAAA;ELouGV;EK3uGM;IAOI,6BAAA;ELuuGV;EK9uGM;IAOI,2BAAA;EL0uGV;EKjvGM;IAOI,2BAAA;EL6uGV;EKpvGM;IAOI,yBAAA;ELgvGV;EKvvGM;IAOI,+BAAA;ELmvGV;EK1vGM;IAOI,8BAAA;ELsvGV;EK7vGM;IAOI,4BAAA;ELyvGV;EKhwGM;IAOI,8BAAA;EL4vGV;EKnwGM;IAOI,4BAAA;EL+vGV;EKtwGM;IAOI,4BAAA;ELkwGV;EKzwGM;IAOI,2BAAA;ELqwGV;EK5wGM;IAOI,iCAAA;ELwwGV;EK/wGM;IAOI,gCAAA;EL2wGV;EKlxGM;IAOI,8BAAA;EL8wGV;EKrxGM;IAOI,gCAAA;ELixGV;EKxxGM;IAOI,8BAAA;ELoxGV;EK3xGM;IAOI,8BAAA;ELuxGV;EK9xGM;IAOI,0BAAA;EL0xGV;EKjyGM;IAOI,gCAAA;EL6xGV;EKpyGM;IAOI,+BAAA;ELgyGV;EKvyGM;IAOI,6BAAA;ELmyGV;EK1yGM;IAOI,+BAAA;ELsyGV;EK7yGM;IAOI,6BAAA;ELyyGV;EKhzGM;IAOI,6BAAA;EL4yGV;EKnzGM;IAOI,qBAAA;EL+yGV;EKtzGM;IAOI,2BAAA;ELkzGV;EKzzGM;IAOI,0BAAA;ELqzGV;EK5zGM;IAOI,wBAAA;ELwzGV;EK/zGM;IAOI,0BAAA;EL2zGV;EKl0GM;IAOI,wBAAA;EL8zGV;EKr0GM;IAOI,0BAAA;IAAA,2BAAA;ELk0GV;EKz0GM;IAOI,gCAAA;IAAA,iCAAA;ELs0GV;EK70GM;IAOI,+BAAA;IAAA,gCAAA;EL00GV;EKj1GM;IAOI,6BAAA;IAAA,8BAAA;EL80GV;EKr1GM;IAOI,+BAAA;IAAA,gCAAA;ELk1GV;EKz1GM;IAOI,6BAAA;IAAA,8BAAA;ELs1GV;EK71GM;IAOI,yBAAA;IAAA,4BAAA;EL01GV;EKj2GM;IAOI,+BAAA;IAAA,kCAAA;EL81GV;EKr2GM;IAOI,8BAAA;IAAA,iCAAA;ELk2GV;EKz2GM;IAOI,4BAAA;IAAA,+BAAA;ELs2GV;EK72GM;IAOI,8BAAA;IAAA,iCAAA;EL02GV;EKj3GM;IAOI,4BAAA;IAAA,+BAAA;EL82GV;EKr3GM;IAOI,yBAAA;ELi3GV;EKx3GM;IAOI,+BAAA;ELo3GV;EK33GM;IAOI,8BAAA;ELu3GV;EK93GM;IAOI,4BAAA;EL03GV;EKj4GM;IAOI,8BAAA;EL63GV;EKp4GM;IAOI,4BAAA;ELg4GV;EKv4GM;IAOI,0BAAA;ELm4GV;EK14GM;IAOI,gCAAA;ELs4GV;EK74GM;IAOI,+BAAA;ELy4GV;EKh5GM;IAOI,6BAAA;EL44GV;EKn5GM;IAOI,+BAAA;EL+4GV;EKt5GM;IAOI,6BAAA;ELk5GV;EKz5GM;IAOI,4BAAA;ELq5GV;EK55GM;IAOI,kCAAA;ELw5GV;EK/5GM;IAOI,iCAAA;EL25GV;EKl6GM;IAOI,+BAAA;EL85GV;EKr6GM;IAOI,iCAAA;ELi6GV;EKx6GM;IAOI,+BAAA;ELo6GV;EK36GM;IAOI,2BAAA;ELu6GV;EK96GM;IAOI,iCAAA;EL06GV;EKj7GM;IAOI,gCAAA;EL66GV;EKp7GM;IAOI,8BAAA;ELg7GV;EKv7GM;IAOI,gCAAA;ELm7GV;EK17GM;IAOI,8BAAA;ELs7GV;AACF;ACj8GI;EIGI;IAOI,0BAAA;EL27GV;EKl8GM;IAOI,gCAAA;EL87GV;EKr8GM;IAOI,yBAAA;ELi8GV;EKx8GM;IAOI,wBAAA;ELo8GV;EK38GM;IAOI,+BAAA;ELu8GV;EK98GM;IAOI,yBAAA;EL08GV;EKj9GM;IAOI,6BAAA;EL68GV;EKp9GM;IAOI,8BAAA;ELg9GV;EKv9GM;IAOI,wBAAA;ELm9GV;EK19GM;IAOI,+BAAA;ELs9GV;EK79GM;IAOI,wBAAA;ELy9GV;EKh+GM;IAOI,yBAAA;EL49GV;EKn+GM;IAOI,8BAAA;EL+9GV;EKt+GM;IAOI,iCAAA;ELk+GV;EKz+GM;IAOI,sCAAA;ELq+GV;EK5+GM;IAOI,yCAAA;ELw+GV;EK/+GM;IAOI,uBAAA;EL2+GV;EKl/GM;IAOI,uBAAA;EL8+GV;EKr/GM;IAOI,yBAAA;ELi/GV;EKx/GM;IAOI,yBAAA;ELo/GV;EK3/GM;IAOI,0BAAA;ELu/GV;EK9/GM;IAOI,4BAAA;EL0/GV;EKjgHM;IAOI,kCAAA;EL6/GV;EKpgHM;IAOI,sCAAA;ELggHV;EKvgHM;IAOI,oCAAA;ELmgHV;EK1gHM;IAOI,kCAAA;ELsgHV;EK7gHM;IAOI,yCAAA;ELygHV;EKhhHM;IAOI,wCAAA;EL4gHV;EKnhHM;IAOI,wCAAA;EL+gHV;EKthHM;IAOI,kCAAA;ELkhHV;EKzhHM;IAOI,gCAAA;ELqhHV;EK5hHM;IAOI,8BAAA;ELwhHV;EK/hHM;IAOI,gCAAA;EL2hHV;EKliHM;IAOI,+BAAA;EL8hHV;EKriHM;IAOI,oCAAA;ELiiHV;EKxiHM;IAOI,kCAAA;ELoiHV;EK3iHM;IAOI,gCAAA;ELuiHV;EK9iHM;IAOI,uCAAA;EL0iHV;EKjjHM;IAOI,sCAAA;EL6iHV;EKpjHM;IAOI,iCAAA;ELgjHV;EKvjHM;IAOI,2BAAA;ELmjHV;EK1jHM;IAOI,iCAAA;ELsjHV;EK7jHM;IAOI,+BAAA;ELyjHV;EKhkHM;IAOI,6BAAA;EL4jHV;EKnkHM;IAOI,+BAAA;EL+jHV;EKtkHM;IAOI,8BAAA;ELkkHV;EKzkHM;IAOI,oBAAA;ELqkHV;EK5kHM;IAOI,mBAAA;ELwkHV;EK/kHM;IAOI,mBAAA;EL2kHV;EKllHM;IAOI,mBAAA;EL8kHV;EKrlHM;IAOI,mBAAA;ELilHV;EKxlHM;IAOI,mBAAA;ELolHV;EK3lHM;IAOI,mBAAA;ELulHV;EK9lHM;IAOI,mBAAA;EL0lHV;EKjmHM;IAOI,oBAAA;EL6lHV;EKpmHM;IAOI,0BAAA;ELgmHV;EKvmHM;IAOI,yBAAA;ELmmHV;EK1mHM;IAOI,uBAAA;ELsmHV;EK7mHM;IAOI,yBAAA;ELymHV;EKhnHM;IAOI,uBAAA;EL4mHV;EKnnHM;IAOI,uBAAA;EL+mHV;EKtnHM;IAOI,yBAAA;IAAA,0BAAA;ELmnHV;EK1nHM;IAOI,+BAAA;IAAA,gCAAA;ELunHV;EK9nHM;IAOI,8BAAA;IAAA,+BAAA;EL2nHV;EKloHM;IAOI,4BAAA;IAAA,6BAAA;EL+nHV;EKtoHM;IAOI,8BAAA;IAAA,+BAAA;ELmoHV;EK1oHM;IAOI,4BAAA;IAAA,6BAAA;ELuoHV;EK9oHM;IAOI,4BAAA;IAAA,6BAAA;EL2oHV;EKlpHM;IAOI,wBAAA;IAAA,2BAAA;EL+oHV;EKtpHM;IAOI,8BAAA;IAAA,iCAAA;ELmpHV;EK1pHM;IAOI,6BAAA;IAAA,gCAAA;ELupHV;EK9pHM;IAOI,2BAAA;IAAA,8BAAA;EL2pHV;EKlqHM;IAOI,6BAAA;IAAA,gCAAA;EL+pHV;EKtqHM;IAOI,2BAAA;IAAA,8BAAA;ELmqHV;EK1qHM;IAOI,2BAAA;IAAA,8BAAA;ELuqHV;EK9qHM;IAOI,wBAAA;EL0qHV;EKjrHM;IAOI,8BAAA;EL6qHV;EKprHM;IAOI,6BAAA;ELgrHV;EKvrHM;IAOI,2BAAA;ELmrHV;EK1rHM;IAOI,6BAAA;ELsrHV;EK7rHM;IAOI,2BAAA;ELyrHV;EKhsHM;IAOI,2BAAA;EL4rHV;EKnsHM;IAOI,yBAAA;EL+rHV;EKtsHM;IAOI,+BAAA;ELksHV;EKzsHM;IAOI,8BAAA;ELqsHV;EK5sHM;IAOI,4BAAA;ELwsHV;EK/sHM;IAOI,8BAAA;EL2sHV;EKltHM;IAOI,4BAAA;EL8sHV;EKrtHM;IAOI,4BAAA;ELitHV;EKxtHM;IAOI,2BAAA;ELotHV;EK3tHM;IAOI,iCAAA;ELutHV;EK9tHM;IAOI,gCAAA;EL0tHV;EKjuHM;IAOI,8BAAA;EL6tHV;EKpuHM;IAOI,gCAAA;ELguHV;EKvuHM;IAOI,8BAAA;ELmuHV;EK1uHM;IAOI,8BAAA;ELsuHV;EK7uHM;IAOI,0BAAA;ELyuHV;EKhvHM;IAOI,gCAAA;EL4uHV;EKnvHM;IAOI,+BAAA;EL+uHV;EKtvHM;IAOI,6BAAA;ELkvHV;EKzvHM;IAOI,+BAAA;ELqvHV;EK5vHM;IAOI,6BAAA;ELwvHV;EK/vHM;IAOI,6BAAA;EL2vHV;EKlwHM;IAOI,qBAAA;EL8vHV;EKrwHM;IAOI,2BAAA;ELiwHV;EKxwHM;IAOI,0BAAA;ELowHV;EK3wHM;IAOI,wBAAA;ELuwHV;EK9wHM;IAOI,0BAAA;EL0wHV;EKjxHM;IAOI,wBAAA;EL6wHV;EKpxHM;IAOI,0BAAA;IAAA,2BAAA;ELixHV;EKxxHM;IAOI,gCAAA;IAAA,iCAAA;ELqxHV;EK5xHM;IAOI,+BAAA;IAAA,gCAAA;ELyxHV;EKhyHM;IAOI,6BAAA;IAAA,8BAAA;EL6xHV;EKpyHM;IAOI,+BAAA;IAAA,gCAAA;ELiyHV;EKxyHM;IAOI,6BAAA;IAAA,8BAAA;ELqyHV;EK5yHM;IAOI,yBAAA;IAAA,4BAAA;ELyyHV;EKhzHM;IAOI,+BAAA;IAAA,kCAAA;EL6yHV;EKpzHM;IAOI,8BAAA;IAAA,iCAAA;ELizHV;EKxzHM;IAOI,4BAAA;IAAA,+BAAA;ELqzHV;EK5zHM;IAOI,8BAAA;IAAA,iCAAA;ELyzHV;EKh0HM;IAOI,4BAAA;IAAA,+BAAA;EL6zHV;EKp0HM;IAOI,yBAAA;ELg0HV;EKv0HM;IAOI,+BAAA;ELm0HV;EK10HM;IAOI,8BAAA;ELs0HV;EK70HM;IAOI,4BAAA;ELy0HV;EKh1HM;IAOI,8BAAA;EL40HV;EKn1HM;IAOI,4BAAA;EL+0HV;EKt1HM;IAOI,0BAAA;ELk1HV;EKz1HM;IAOI,gCAAA;ELq1HV;EK51HM;IAOI,+BAAA;ELw1HV;EK/1HM;IAOI,6BAAA;EL21HV;EKl2HM;IAOI,+BAAA;EL81HV;EKr2HM;IAOI,6BAAA;ELi2HV;EKx2HM;IAOI,4BAAA;ELo2HV;EK32HM;IAOI,kCAAA;ELu2HV;EK92HM;IAOI,iCAAA;EL02HV;EKj3HM;IAOI,+BAAA;EL62HV;EKp3HM;IAOI,iCAAA;ELg3HV;EKv3HM;IAOI,+BAAA;ELm3HV;EK13HM;IAOI,2BAAA;ELs3HV;EK73HM;IAOI,iCAAA;ELy3HV;EKh4HM;IAOI,gCAAA;EL43HV;EKn4HM;IAOI,8BAAA;EL+3HV;EKt4HM;IAOI,gCAAA;ELk4HV;EKz4HM;IAOI,8BAAA;ELq4HV;AACF;AMz6HA;ED4BQ;IAOI,0BAAA;EL04HV;EKj5HM;IAOI,gCAAA;EL64HV;EKp5HM;IAOI,yBAAA;ELg5HV;EKv5HM;IAOI,wBAAA;ELm5HV;EK15HM;IAOI,+BAAA;ELs5HV;EK75HM;IAOI,yBAAA;ELy5HV;EKh6HM;IAOI,6BAAA;EL45HV;EKn6HM;IAOI,8BAAA;EL+5HV;EKt6HM;IAOI,wBAAA;ELk6HV;EKz6HM;IAOI,+BAAA;ELq6HV;EK56HM;IAOI,wBAAA;ELw6HV;AACF","file":"bootstrap-grid.rtl.css","sourcesContent":["@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-container-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n","// Container mixins\n\n@mixin make-container($gutter: $container-padding-x) {\n --#{$prefix}gutter-x: #{$gutter};\n --#{$prefix}gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-right: auto;\n margin-left: auto;\n}\n","/*!\n * Bootstrap Grid v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n:root {\n --bs-breakpoint-xs: 0;\n --bs-breakpoint-sm: 576px;\n --bs-breakpoint-md: 768px;\n --bs-breakpoint-lg: 992px;\n --bs-breakpoint-xl: 1200px;\n --bs-breakpoint-xxl: 1400px;\n}\n\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(-1 * var(--bs-gutter-y));\n margin-right: calc(-0.5 * var(--bs-gutter-x));\n margin-left: calc(-0.5 * var(--bs-gutter-x));\n}\n.row > * {\n box-sizing: border-box;\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0;\n }\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n .g-sm-0,\n .gx-sm-0 {\n --bs-gutter-x: 0;\n }\n .g-sm-0,\n .gy-sm-0 {\n --bs-gutter-y: 0;\n }\n .g-sm-1,\n .gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-sm-1,\n .gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-sm-2,\n .gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-sm-2,\n .gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-sm-3,\n .gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n .g-sm-3,\n .gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n .g-sm-4,\n .gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-sm-4,\n .gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-sm-5,\n .gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n .g-sm-5,\n .gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0;\n }\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n .g-md-0,\n .gx-md-0 {\n --bs-gutter-x: 0;\n }\n .g-md-0,\n .gy-md-0 {\n --bs-gutter-y: 0;\n }\n .g-md-1,\n .gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-md-1,\n .gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-md-2,\n .gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-md-2,\n .gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-md-3,\n .gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n .g-md-3,\n .gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n .g-md-4,\n .gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-md-4,\n .gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-md-5,\n .gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n .g-md-5,\n .gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0;\n }\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n .g-lg-0,\n .gx-lg-0 {\n --bs-gutter-x: 0;\n }\n .g-lg-0,\n .gy-lg-0 {\n --bs-gutter-y: 0;\n }\n .g-lg-1,\n .gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-lg-1,\n .gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-lg-2,\n .gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-lg-2,\n .gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-lg-3,\n .gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n .g-lg-3,\n .gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n .g-lg-4,\n .gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-lg-4,\n .gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-lg-5,\n .gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n .g-lg-5,\n .gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0;\n }\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n .g-xl-0,\n .gx-xl-0 {\n --bs-gutter-x: 0;\n }\n .g-xl-0,\n .gy-xl-0 {\n --bs-gutter-y: 0;\n }\n .g-xl-1,\n .gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xl-1,\n .gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xl-2,\n .gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xl-2,\n .gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xl-3,\n .gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xl-3,\n .gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xl-4,\n .gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xl-4,\n .gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xl-5,\n .gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xl-5,\n .gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0;\n }\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xxl-0 {\n margin-left: 0;\n }\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xxl-3 {\n margin-left: 25%;\n }\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xxl-6 {\n margin-left: 50%;\n }\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xxl-9 {\n margin-left: 75%;\n }\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n .g-xxl-0,\n .gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n .g-xxl-0,\n .gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n .g-xxl-1,\n .gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xxl-1,\n .gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xxl-2,\n .gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xxl-2,\n .gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xxl-3,\n .gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xxl-3,\n .gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xxl-4,\n .gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xxl-4,\n .gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xxl-5,\n .gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xxl-5,\n .gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-inline-grid {\n display: inline-grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-grid {\n display: grid !important;\n }\n .d-sm-inline-grid {\n display: inline-grid !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n .d-sm-none {\n display: none !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n .order-sm-first {\n order: -1 !important;\n }\n .order-sm-0 {\n order: 0 !important;\n }\n .order-sm-1 {\n order: 1 !important;\n }\n .order-sm-2 {\n order: 2 !important;\n }\n .order-sm-3 {\n order: 3 !important;\n }\n .order-sm-4 {\n order: 4 !important;\n }\n .order-sm-5 {\n order: 5 !important;\n }\n .order-sm-last {\n order: 6 !important;\n }\n .m-sm-0 {\n margin: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n .mt-sm-auto {\n margin-top: auto !important;\n }\n .me-sm-0 {\n margin-right: 0 !important;\n }\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n .me-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n .ms-sm-auto {\n margin-left: auto !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 768px) {\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-grid {\n display: grid !important;\n }\n .d-md-inline-grid {\n display: inline-grid !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n .d-md-none {\n display: none !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n .order-md-first {\n order: -1 !important;\n }\n .order-md-0 {\n order: 0 !important;\n }\n .order-md-1 {\n order: 1 !important;\n }\n .order-md-2 {\n order: 2 !important;\n }\n .order-md-3 {\n order: 3 !important;\n }\n .order-md-4 {\n order: 4 !important;\n }\n .order-md-5 {\n order: 5 !important;\n }\n .order-md-last {\n order: 6 !important;\n }\n .m-md-0 {\n margin: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-md-0 {\n margin-top: 0 !important;\n }\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n .mt-md-auto {\n margin-top: auto !important;\n }\n .me-md-0 {\n margin-right: 0 !important;\n }\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n .me-md-3 {\n margin-right: 1rem !important;\n }\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n .me-md-5 {\n margin-right: 3rem !important;\n }\n .me-md-auto {\n margin-right: auto !important;\n }\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n .ms-md-0 {\n margin-left: 0 !important;\n }\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n .ms-md-auto {\n margin-left: auto !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-md-0 {\n padding-top: 0 !important;\n }\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n .pe-md-0 {\n padding-right: 0 !important;\n }\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n .ps-md-0 {\n padding-left: 0 !important;\n }\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 992px) {\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-grid {\n display: grid !important;\n }\n .d-lg-inline-grid {\n display: inline-grid !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n .d-lg-none {\n display: none !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n .order-lg-first {\n order: -1 !important;\n }\n .order-lg-0 {\n order: 0 !important;\n }\n .order-lg-1 {\n order: 1 !important;\n }\n .order-lg-2 {\n order: 2 !important;\n }\n .order-lg-3 {\n order: 3 !important;\n }\n .order-lg-4 {\n order: 4 !important;\n }\n .order-lg-5 {\n order: 5 !important;\n }\n .order-lg-last {\n order: 6 !important;\n }\n .m-lg-0 {\n margin: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n .mt-lg-auto {\n margin-top: auto !important;\n }\n .me-lg-0 {\n margin-right: 0 !important;\n }\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n .me-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n .ms-lg-auto {\n margin-left: auto !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1200px) {\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-grid {\n display: grid !important;\n }\n .d-xl-inline-grid {\n display: inline-grid !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n .d-xl-none {\n display: none !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n .order-xl-first {\n order: -1 !important;\n }\n .order-xl-0 {\n order: 0 !important;\n }\n .order-xl-1 {\n order: 1 !important;\n }\n .order-xl-2 {\n order: 2 !important;\n }\n .order-xl-3 {\n order: 3 !important;\n }\n .order-xl-4 {\n order: 4 !important;\n }\n .order-xl-5 {\n order: 5 !important;\n }\n .order-xl-last {\n order: 6 !important;\n }\n .m-xl-0 {\n margin: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n .mt-xl-auto {\n margin-top: auto !important;\n }\n .me-xl-0 {\n margin-right: 0 !important;\n }\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n .me-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n .ms-xl-auto {\n margin-left: auto !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n}\n@media (min-width: 1400px) {\n .d-xxl-inline {\n display: inline !important;\n }\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n .d-xxl-block {\n display: block !important;\n }\n .d-xxl-grid {\n display: grid !important;\n }\n .d-xxl-inline-grid {\n display: inline-grid !important;\n }\n .d-xxl-table {\n display: table !important;\n }\n .d-xxl-table-row {\n display: table-row !important;\n }\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n .d-xxl-flex {\n display: flex !important;\n }\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n .d-xxl-none {\n display: none !important;\n }\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xxl-row {\n flex-direction: row !important;\n }\n .flex-xxl-column {\n flex-direction: column !important;\n }\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n .align-items-xxl-center {\n align-items: center !important;\n }\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n .align-content-xxl-center {\n align-content: center !important;\n }\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n .align-self-xxl-center {\n align-self: center !important;\n }\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n .order-xxl-first {\n order: -1 !important;\n }\n .order-xxl-0 {\n order: 0 !important;\n }\n .order-xxl-1 {\n order: 1 !important;\n }\n .order-xxl-2 {\n order: 2 !important;\n }\n .order-xxl-3 {\n order: 3 !important;\n }\n .order-xxl-4 {\n order: 4 !important;\n }\n .order-xxl-5 {\n order: 5 !important;\n }\n .order-xxl-last {\n order: 6 !important;\n }\n .m-xxl-0 {\n margin: 0 !important;\n }\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n .m-xxl-3 {\n margin: 1rem !important;\n }\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n .m-xxl-5 {\n margin: 3rem !important;\n }\n .m-xxl-auto {\n margin: auto !important;\n }\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n .me-xxl-auto {\n margin-right: auto !important;\n }\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n .p-xxl-0 {\n padding: 0 !important;\n }\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n .p-xxl-3 {\n padding: 1rem !important;\n }\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n .p-xxl-5 {\n padding: 3rem !important;\n }\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-grid {\n display: grid !important;\n }\n .d-print-inline-grid {\n display: inline-grid !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap-grid.css.map */\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl xxl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @if not $n {\n @error \"breakpoint `#{$name}` not found in `#{$breakpoints}`\";\n }\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width.\n// The maximum value is reduced by 0.02px to work around the limitations of\n// `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $max: map-get($breakpoints, $name);\n @return if($max and $max > 0, $max - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $next: breakpoint-next($name, $breakpoints);\n $max: breakpoint-max($next, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($next, $breakpoints) {\n @content;\n }\n }\n}\n","// Variables\n//\n// Variables should follow the `$component-state-property-size` formula for\n// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.\n\n// Color system\n\n// scss-docs-start gray-color-variables\n$white: #fff !default;\n$gray-100: #f8f9fa !default;\n$gray-200: #e9ecef !default;\n$gray-300: #dee2e6 !default;\n$gray-400: #ced4da !default;\n$gray-500: #adb5bd !default;\n$gray-600: #6c757d !default;\n$gray-700: #495057 !default;\n$gray-800: #343a40 !default;\n$gray-900: #212529 !default;\n$black: #000 !default;\n// scss-docs-end gray-color-variables\n\n// fusv-disable\n// scss-docs-start gray-colors-map\n$grays: (\n \"100\": $gray-100,\n \"200\": $gray-200,\n \"300\": $gray-300,\n \"400\": $gray-400,\n \"500\": $gray-500,\n \"600\": $gray-600,\n \"700\": $gray-700,\n \"800\": $gray-800,\n \"900\": $gray-900\n) !default;\n// scss-docs-end gray-colors-map\n// fusv-enable\n\n// scss-docs-start color-variables\n$blue: #0d6efd !default;\n$indigo: #6610f2 !default;\n$purple: #6f42c1 !default;\n$pink: #d63384 !default;\n$red: #dc3545 !default;\n$orange: #fd7e14 !default;\n$yellow: #ffc107 !default;\n$green: #198754 !default;\n$teal: #20c997 !default;\n$cyan: #0dcaf0 !default;\n// scss-docs-end color-variables\n\n// scss-docs-start colors-map\n$colors: (\n \"blue\": $blue,\n \"indigo\": $indigo,\n \"purple\": $purple,\n \"pink\": $pink,\n \"red\": $red,\n \"orange\": $orange,\n \"yellow\": $yellow,\n \"green\": $green,\n \"teal\": $teal,\n \"cyan\": $cyan,\n \"black\": $black,\n \"white\": $white,\n \"gray\": $gray-600,\n \"gray-dark\": $gray-800\n) !default;\n// scss-docs-end colors-map\n\n// The contrast ratio to reach against white, to determine if color changes from \"light\" to \"dark\". Acceptable values for WCAG 2.2 are 3, 4.5 and 7.\n// See https://www.w3.org/TR/WCAG/#contrast-minimum\n$min-contrast-ratio: 4.5 !default;\n\n// Customize the light and dark text colors for use in our color contrast function.\n$color-contrast-dark: $black !default;\n$color-contrast-light: $white !default;\n\n// fusv-disable\n$blue-100: tint-color($blue, 80%) !default;\n$blue-200: tint-color($blue, 60%) !default;\n$blue-300: tint-color($blue, 40%) !default;\n$blue-400: tint-color($blue, 20%) !default;\n$blue-500: $blue !default;\n$blue-600: shade-color($blue, 20%) !default;\n$blue-700: shade-color($blue, 40%) !default;\n$blue-800: shade-color($blue, 60%) !default;\n$blue-900: shade-color($blue, 80%) !default;\n\n$indigo-100: tint-color($indigo, 80%) !default;\n$indigo-200: tint-color($indigo, 60%) !default;\n$indigo-300: tint-color($indigo, 40%) !default;\n$indigo-400: tint-color($indigo, 20%) !default;\n$indigo-500: $indigo !default;\n$indigo-600: shade-color($indigo, 20%) !default;\n$indigo-700: shade-color($indigo, 40%) !default;\n$indigo-800: shade-color($indigo, 60%) !default;\n$indigo-900: shade-color($indigo, 80%) !default;\n\n$purple-100: tint-color($purple, 80%) !default;\n$purple-200: tint-color($purple, 60%) !default;\n$purple-300: tint-color($purple, 40%) !default;\n$purple-400: tint-color($purple, 20%) !default;\n$purple-500: $purple !default;\n$purple-600: shade-color($purple, 20%) !default;\n$purple-700: shade-color($purple, 40%) !default;\n$purple-800: shade-color($purple, 60%) !default;\n$purple-900: shade-color($purple, 80%) !default;\n\n$pink-100: tint-color($pink, 80%) !default;\n$pink-200: tint-color($pink, 60%) !default;\n$pink-300: tint-color($pink, 40%) !default;\n$pink-400: tint-color($pink, 20%) !default;\n$pink-500: $pink !default;\n$pink-600: shade-color($pink, 20%) !default;\n$pink-700: shade-color($pink, 40%) !default;\n$pink-800: shade-color($pink, 60%) !default;\n$pink-900: shade-color($pink, 80%) !default;\n\n$red-100: tint-color($red, 80%) !default;\n$red-200: tint-color($red, 60%) !default;\n$red-300: tint-color($red, 40%) !default;\n$red-400: tint-color($red, 20%) !default;\n$red-500: $red !default;\n$red-600: shade-color($red, 20%) !default;\n$red-700: shade-color($red, 40%) !default;\n$red-800: shade-color($red, 60%) !default;\n$red-900: shade-color($red, 80%) !default;\n\n$orange-100: tint-color($orange, 80%) !default;\n$orange-200: tint-color($orange, 60%) !default;\n$orange-300: tint-color($orange, 40%) !default;\n$orange-400: tint-color($orange, 20%) !default;\n$orange-500: $orange !default;\n$orange-600: shade-color($orange, 20%) !default;\n$orange-700: shade-color($orange, 40%) !default;\n$orange-800: shade-color($orange, 60%) !default;\n$orange-900: shade-color($orange, 80%) !default;\n\n$yellow-100: tint-color($yellow, 80%) !default;\n$yellow-200: tint-color($yellow, 60%) !default;\n$yellow-300: tint-color($yellow, 40%) !default;\n$yellow-400: tint-color($yellow, 20%) !default;\n$yellow-500: $yellow !default;\n$yellow-600: shade-color($yellow, 20%) !default;\n$yellow-700: shade-color($yellow, 40%) !default;\n$yellow-800: shade-color($yellow, 60%) !default;\n$yellow-900: shade-color($yellow, 80%) !default;\n\n$green-100: tint-color($green, 80%) !default;\n$green-200: tint-color($green, 60%) !default;\n$green-300: tint-color($green, 40%) !default;\n$green-400: tint-color($green, 20%) !default;\n$green-500: $green !default;\n$green-600: shade-color($green, 20%) !default;\n$green-700: shade-color($green, 40%) !default;\n$green-800: shade-color($green, 60%) !default;\n$green-900: shade-color($green, 80%) !default;\n\n$teal-100: tint-color($teal, 80%) !default;\n$teal-200: tint-color($teal, 60%) !default;\n$teal-300: tint-color($teal, 40%) !default;\n$teal-400: tint-color($teal, 20%) !default;\n$teal-500: $teal !default;\n$teal-600: shade-color($teal, 20%) !default;\n$teal-700: shade-color($teal, 40%) !default;\n$teal-800: shade-color($teal, 60%) !default;\n$teal-900: shade-color($teal, 80%) !default;\n\n$cyan-100: tint-color($cyan, 80%) !default;\n$cyan-200: tint-color($cyan, 60%) !default;\n$cyan-300: tint-color($cyan, 40%) !default;\n$cyan-400: tint-color($cyan, 20%) !default;\n$cyan-500: $cyan !default;\n$cyan-600: shade-color($cyan, 20%) !default;\n$cyan-700: shade-color($cyan, 40%) !default;\n$cyan-800: shade-color($cyan, 60%) !default;\n$cyan-900: shade-color($cyan, 80%) !default;\n\n$blues: (\n \"blue-100\": $blue-100,\n \"blue-200\": $blue-200,\n \"blue-300\": $blue-300,\n \"blue-400\": $blue-400,\n \"blue-500\": $blue-500,\n \"blue-600\": $blue-600,\n \"blue-700\": $blue-700,\n \"blue-800\": $blue-800,\n \"blue-900\": $blue-900\n) !default;\n\n$indigos: (\n \"indigo-100\": $indigo-100,\n \"indigo-200\": $indigo-200,\n \"indigo-300\": $indigo-300,\n \"indigo-400\": $indigo-400,\n \"indigo-500\": $indigo-500,\n \"indigo-600\": $indigo-600,\n \"indigo-700\": $indigo-700,\n \"indigo-800\": $indigo-800,\n \"indigo-900\": $indigo-900\n) !default;\n\n$purples: (\n \"purple-100\": $purple-100,\n \"purple-200\": $purple-200,\n \"purple-300\": $purple-300,\n \"purple-400\": $purple-400,\n \"purple-500\": $purple-500,\n \"purple-600\": $purple-600,\n \"purple-700\": $purple-700,\n \"purple-800\": $purple-800,\n \"purple-900\": $purple-900\n) !default;\n\n$pinks: (\n \"pink-100\": $pink-100,\n \"pink-200\": $pink-200,\n \"pink-300\": $pink-300,\n \"pink-400\": $pink-400,\n \"pink-500\": $pink-500,\n \"pink-600\": $pink-600,\n \"pink-700\": $pink-700,\n \"pink-800\": $pink-800,\n \"pink-900\": $pink-900\n) !default;\n\n$reds: (\n \"red-100\": $red-100,\n \"red-200\": $red-200,\n \"red-300\": $red-300,\n \"red-400\": $red-400,\n \"red-500\": $red-500,\n \"red-600\": $red-600,\n \"red-700\": $red-700,\n \"red-800\": $red-800,\n \"red-900\": $red-900\n) !default;\n\n$oranges: (\n \"orange-100\": $orange-100,\n \"orange-200\": $orange-200,\n \"orange-300\": $orange-300,\n \"orange-400\": $orange-400,\n \"orange-500\": $orange-500,\n \"orange-600\": $orange-600,\n \"orange-700\": $orange-700,\n \"orange-800\": $orange-800,\n \"orange-900\": $orange-900\n) !default;\n\n$yellows: (\n \"yellow-100\": $yellow-100,\n \"yellow-200\": $yellow-200,\n \"yellow-300\": $yellow-300,\n \"yellow-400\": $yellow-400,\n \"yellow-500\": $yellow-500,\n \"yellow-600\": $yellow-600,\n \"yellow-700\": $yellow-700,\n \"yellow-800\": $yellow-800,\n \"yellow-900\": $yellow-900\n) !default;\n\n$greens: (\n \"green-100\": $green-100,\n \"green-200\": $green-200,\n \"green-300\": $green-300,\n \"green-400\": $green-400,\n \"green-500\": $green-500,\n \"green-600\": $green-600,\n \"green-700\": $green-700,\n \"green-800\": $green-800,\n \"green-900\": $green-900\n) !default;\n\n$teals: (\n \"teal-100\": $teal-100,\n \"teal-200\": $teal-200,\n \"teal-300\": $teal-300,\n \"teal-400\": $teal-400,\n \"teal-500\": $teal-500,\n \"teal-600\": $teal-600,\n \"teal-700\": $teal-700,\n \"teal-800\": $teal-800,\n \"teal-900\": $teal-900\n) !default;\n\n$cyans: (\n \"cyan-100\": $cyan-100,\n \"cyan-200\": $cyan-200,\n \"cyan-300\": $cyan-300,\n \"cyan-400\": $cyan-400,\n \"cyan-500\": $cyan-500,\n \"cyan-600\": $cyan-600,\n \"cyan-700\": $cyan-700,\n \"cyan-800\": $cyan-800,\n \"cyan-900\": $cyan-900\n) !default;\n// fusv-enable\n\n// scss-docs-start theme-color-variables\n$primary: $blue !default;\n$secondary: $gray-600 !default;\n$success: $green !default;\n$info: $cyan !default;\n$warning: $yellow !default;\n$danger: $red !default;\n$light: $gray-100 !default;\n$dark: $gray-900 !default;\n// scss-docs-end theme-color-variables\n\n// scss-docs-start theme-colors-map\n$theme-colors: (\n \"primary\": $primary,\n \"secondary\": $secondary,\n \"success\": $success,\n \"info\": $info,\n \"warning\": $warning,\n \"danger\": $danger,\n \"light\": $light,\n \"dark\": $dark\n) !default;\n// scss-docs-end theme-colors-map\n\n// scss-docs-start theme-text-variables\n$primary-text-emphasis: shade-color($primary, 60%) !default;\n$secondary-text-emphasis: shade-color($secondary, 60%) !default;\n$success-text-emphasis: shade-color($success, 60%) !default;\n$info-text-emphasis: shade-color($info, 60%) !default;\n$warning-text-emphasis: shade-color($warning, 60%) !default;\n$danger-text-emphasis: shade-color($danger, 60%) !default;\n$light-text-emphasis: $gray-700 !default;\n$dark-text-emphasis: $gray-700 !default;\n// scss-docs-end theme-text-variables\n\n// scss-docs-start theme-bg-subtle-variables\n$primary-bg-subtle: tint-color($primary, 80%) !default;\n$secondary-bg-subtle: tint-color($secondary, 80%) !default;\n$success-bg-subtle: tint-color($success, 80%) !default;\n$info-bg-subtle: tint-color($info, 80%) !default;\n$warning-bg-subtle: tint-color($warning, 80%) !default;\n$danger-bg-subtle: tint-color($danger, 80%) !default;\n$light-bg-subtle: mix($gray-100, $white) !default;\n$dark-bg-subtle: $gray-400 !default;\n// scss-docs-end theme-bg-subtle-variables\n\n// scss-docs-start theme-border-subtle-variables\n$primary-border-subtle: tint-color($primary, 60%) !default;\n$secondary-border-subtle: tint-color($secondary, 60%) !default;\n$success-border-subtle: tint-color($success, 60%) !default;\n$info-border-subtle: tint-color($info, 60%) !default;\n$warning-border-subtle: tint-color($warning, 60%) !default;\n$danger-border-subtle: tint-color($danger, 60%) !default;\n$light-border-subtle: $gray-200 !default;\n$dark-border-subtle: $gray-500 !default;\n// scss-docs-end theme-border-subtle-variables\n\n// Characters which are escaped by the escape-svg function\n$escaped-characters: (\n (\"<\", \"%3c\"),\n (\">\", \"%3e\"),\n (\"#\", \"%23\"),\n (\"(\", \"%28\"),\n (\")\", \"%29\"),\n) !default;\n\n// Options\n//\n// Quickly modify global styling by enabling or disabling optional features.\n\n$enable-caret: true !default;\n$enable-rounded: true !default;\n$enable-shadows: false !default;\n$enable-gradients: false !default;\n$enable-transitions: true !default;\n$enable-reduced-motion: true !default;\n$enable-smooth-scroll: true !default;\n$enable-grid-classes: true !default;\n$enable-container-classes: true !default;\n$enable-cssgrid: false !default;\n$enable-button-pointers: true !default;\n$enable-rfs: true !default;\n$enable-validation-icons: true !default;\n$enable-negative-margins: false !default;\n$enable-deprecation-messages: true !default;\n$enable-important-utilities: true !default;\n\n$enable-dark-mode: true !default;\n$color-mode-type: data !default; // `data` or `media-query`\n\n// Prefix for :root CSS variables\n\n$variable-prefix: bs- !default; // Deprecated in v5.2.0 for the shorter `$prefix`\n$prefix: $variable-prefix !default;\n\n// Gradient\n//\n// The gradient which is added to components if `$enable-gradients` is `true`\n// This gradient is also added to elements with `.bg-gradient`\n// scss-docs-start variable-gradient\n$gradient: linear-gradient(180deg, rgba($white, .15), rgba($white, 0)) !default;\n// scss-docs-end variable-gradient\n\n// Spacing\n//\n// Control the default styling of most Bootstrap elements by modifying these\n// variables. Mostly focused on spacing.\n// You can add more entries to the $spacers map, should you need more variation.\n\n// scss-docs-start spacer-variables-maps\n$spacer: 1rem !default;\n$spacers: (\n 0: 0,\n 1: $spacer * .25,\n 2: $spacer * .5,\n 3: $spacer,\n 4: $spacer * 1.5,\n 5: $spacer * 3,\n) !default;\n// scss-docs-end spacer-variables-maps\n\n// Position\n//\n// Define the edge positioning anchors of the position utilities.\n\n// scss-docs-start position-map\n$position-values: (\n 0: 0,\n 50: 50%,\n 100: 100%\n) !default;\n// scss-docs-end position-map\n\n// Body\n//\n// Settings for the `` element.\n\n$body-text-align: null !default;\n$body-color: $gray-900 !default;\n$body-bg: $white !default;\n\n$body-secondary-color: rgba($body-color, .75) !default;\n$body-secondary-bg: $gray-200 !default;\n\n$body-tertiary-color: rgba($body-color, .5) !default;\n$body-tertiary-bg: $gray-100 !default;\n\n$body-emphasis-color: $black !default;\n\n// Links\n//\n// Style anchor elements.\n\n$link-color: $primary !default;\n$link-decoration: underline !default;\n$link-shade-percentage: 20% !default;\n$link-hover-color: shift-color($link-color, $link-shade-percentage) !default;\n$link-hover-decoration: null !default;\n\n$stretched-link-pseudo-element: after !default;\n$stretched-link-z-index: 1 !default;\n\n// Icon links\n// scss-docs-start icon-link-variables\n$icon-link-gap: .375rem !default;\n$icon-link-underline-offset: .25em !default;\n$icon-link-icon-size: 1em !default;\n$icon-link-icon-transition: .2s ease-in-out transform !default;\n$icon-link-icon-transform: translate3d(.25em, 0, 0) !default;\n// scss-docs-end icon-link-variables\n\n// Paragraphs\n//\n// Style p element.\n\n$paragraph-margin-bottom: 1rem !default;\n\n\n// Grid breakpoints\n//\n// Define the minimum dimensions at which your layout will change,\n// adapting to different screen sizes, for use in media queries.\n\n// scss-docs-start grid-breakpoints\n$grid-breakpoints: (\n xs: 0,\n sm: 576px,\n md: 768px,\n lg: 992px,\n xl: 1200px,\n xxl: 1400px\n) !default;\n// scss-docs-end grid-breakpoints\n\n@include _assert-ascending($grid-breakpoints, \"$grid-breakpoints\");\n@include _assert-starts-at-zero($grid-breakpoints, \"$grid-breakpoints\");\n\n\n// Grid containers\n//\n// Define the maximum width of `.container` for different screen sizes.\n\n// scss-docs-start container-max-widths\n$container-max-widths: (\n sm: 540px,\n md: 720px,\n lg: 960px,\n xl: 1140px,\n xxl: 1320px\n) !default;\n// scss-docs-end container-max-widths\n\n@include _assert-ascending($container-max-widths, \"$container-max-widths\");\n\n\n// Grid columns\n//\n// Set the number of columns and specify the width of the gutters.\n\n$grid-columns: 12 !default;\n$grid-gutter-width: 1.5rem !default;\n$grid-row-columns: 6 !default;\n\n// Container padding\n\n$container-padding-x: $grid-gutter-width !default;\n\n\n// Components\n//\n// Define common padding and border radius sizes and more.\n\n// scss-docs-start border-variables\n$border-width: 1px !default;\n$border-widths: (\n 1: 1px,\n 2: 2px,\n 3: 3px,\n 4: 4px,\n 5: 5px\n) !default;\n$border-style: solid !default;\n$border-color: $gray-300 !default;\n$border-color-translucent: rgba($black, .175) !default;\n// scss-docs-end border-variables\n\n// scss-docs-start border-radius-variables\n$border-radius: .375rem !default;\n$border-radius-sm: .25rem !default;\n$border-radius-lg: .5rem !default;\n$border-radius-xl: 1rem !default;\n$border-radius-xxl: 2rem !default;\n$border-radius-pill: 50rem !default;\n// scss-docs-end border-radius-variables\n// fusv-disable\n$border-radius-2xl: $border-radius-xxl !default; // Deprecated in v5.3.0\n// fusv-enable\n\n// scss-docs-start box-shadow-variables\n$box-shadow: 0 .5rem 1rem rgba($black, .15) !default;\n$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default;\n$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default;\n$box-shadow-inset: inset 0 1px 2px rgba($black, .075) !default;\n// scss-docs-end box-shadow-variables\n\n$component-active-color: $white !default;\n$component-active-bg: $primary !default;\n\n// scss-docs-start focus-ring-variables\n$focus-ring-width: .25rem !default;\n$focus-ring-opacity: .25 !default;\n$focus-ring-color: rgba($primary, $focus-ring-opacity) !default;\n$focus-ring-blur: 0 !default;\n$focus-ring-box-shadow: 0 0 $focus-ring-blur $focus-ring-width $focus-ring-color !default;\n// scss-docs-end focus-ring-variables\n\n// scss-docs-start caret-variables\n$caret-width: .3em !default;\n$caret-vertical-align: $caret-width * .85 !default;\n$caret-spacing: $caret-width * .85 !default;\n// scss-docs-end caret-variables\n\n$transition-base: all .2s ease-in-out !default;\n$transition-fade: opacity .15s linear !default;\n// scss-docs-start collapse-transition\n$transition-collapse: height .35s ease !default;\n$transition-collapse-width: width .35s ease !default;\n// scss-docs-end collapse-transition\n\n// stylelint-disable function-disallowed-list\n// scss-docs-start aspect-ratios\n$aspect-ratios: (\n \"1x1\": 100%,\n \"4x3\": calc(3 / 4 * 100%),\n \"16x9\": calc(9 / 16 * 100%),\n \"21x9\": calc(9 / 21 * 100%)\n) !default;\n// scss-docs-end aspect-ratios\n// stylelint-enable function-disallowed-list\n\n// Typography\n//\n// Font, line-height, and color for body text, headings, and more.\n\n// scss-docs-start font-variables\n// stylelint-disable value-keyword-case\n$font-family-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", \"Liberation Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\n$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !default;\n// stylelint-enable value-keyword-case\n$font-family-base: var(--#{$prefix}font-sans-serif) !default;\n$font-family-code: var(--#{$prefix}font-monospace) !default;\n\n// $font-size-root affects the value of `rem`, which is used for as well font sizes, paddings, and margins\n// $font-size-base affects the font size of the body text\n$font-size-root: null !default;\n$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`\n$font-size-sm: $font-size-base * .875 !default;\n$font-size-lg: $font-size-base * 1.25 !default;\n\n$font-weight-lighter: lighter !default;\n$font-weight-light: 300 !default;\n$font-weight-normal: 400 !default;\n$font-weight-medium: 500 !default;\n$font-weight-semibold: 600 !default;\n$font-weight-bold: 700 !default;\n$font-weight-bolder: bolder !default;\n\n$font-weight-base: $font-weight-normal !default;\n\n$line-height-base: 1.5 !default;\n$line-height-sm: 1.25 !default;\n$line-height-lg: 2 !default;\n\n$h1-font-size: $font-size-base * 2.5 !default;\n$h2-font-size: $font-size-base * 2 !default;\n$h3-font-size: $font-size-base * 1.75 !default;\n$h4-font-size: $font-size-base * 1.5 !default;\n$h5-font-size: $font-size-base * 1.25 !default;\n$h6-font-size: $font-size-base !default;\n// scss-docs-end font-variables\n\n// scss-docs-start font-sizes\n$font-sizes: (\n 1: $h1-font-size,\n 2: $h2-font-size,\n 3: $h3-font-size,\n 4: $h4-font-size,\n 5: $h5-font-size,\n 6: $h6-font-size\n) !default;\n// scss-docs-end font-sizes\n\n// scss-docs-start headings-variables\n$headings-margin-bottom: $spacer * .5 !default;\n$headings-font-family: null !default;\n$headings-font-style: null !default;\n$headings-font-weight: 500 !default;\n$headings-line-height: 1.2 !default;\n$headings-color: inherit !default;\n// scss-docs-end headings-variables\n\n// scss-docs-start display-headings\n$display-font-sizes: (\n 1: 5rem,\n 2: 4.5rem,\n 3: 4rem,\n 4: 3.5rem,\n 5: 3rem,\n 6: 2.5rem\n) !default;\n\n$display-font-family: null !default;\n$display-font-style: null !default;\n$display-font-weight: 300 !default;\n$display-line-height: $headings-line-height !default;\n// scss-docs-end display-headings\n\n// scss-docs-start type-variables\n$lead-font-size: $font-size-base * 1.25 !default;\n$lead-font-weight: 300 !default;\n\n$small-font-size: .875em !default;\n\n$sub-sup-font-size: .75em !default;\n\n// fusv-disable\n$text-muted: var(--#{$prefix}secondary-color) !default; // Deprecated in 5.3.0\n// fusv-enable\n\n$initialism-font-size: $small-font-size !default;\n\n$blockquote-margin-y: $spacer !default;\n$blockquote-font-size: $font-size-base * 1.25 !default;\n$blockquote-footer-color: $gray-600 !default;\n$blockquote-footer-font-size: $small-font-size !default;\n\n$hr-margin-y: $spacer !default;\n$hr-color: inherit !default;\n\n// fusv-disable\n$hr-bg-color: null !default; // Deprecated in v5.2.0\n$hr-height: null !default; // Deprecated in v5.2.0\n// fusv-enable\n\n$hr-border-color: null !default; // Allows for inherited colors\n$hr-border-width: var(--#{$prefix}border-width) !default;\n$hr-opacity: .25 !default;\n\n// scss-docs-start vr-variables\n$vr-border-width: var(--#{$prefix}border-width) !default;\n// scss-docs-end vr-variables\n\n$legend-margin-bottom: .5rem !default;\n$legend-font-size: 1.5rem !default;\n$legend-font-weight: null !default;\n\n$dt-font-weight: $font-weight-bold !default;\n\n$list-inline-padding: .5rem !default;\n\n$mark-padding: .1875em !default;\n$mark-color: $body-color !default;\n$mark-bg: $yellow-100 !default;\n// scss-docs-end type-variables\n\n\n// Tables\n//\n// Customizes the `.table` component with basic values, each used across all table variations.\n\n// scss-docs-start table-variables\n$table-cell-padding-y: .5rem !default;\n$table-cell-padding-x: .5rem !default;\n$table-cell-padding-y-sm: .25rem !default;\n$table-cell-padding-x-sm: .25rem !default;\n\n$table-cell-vertical-align: top !default;\n\n$table-color: var(--#{$prefix}emphasis-color) !default;\n$table-bg: var(--#{$prefix}body-bg) !default;\n$table-accent-bg: transparent !default;\n\n$table-th-font-weight: null !default;\n\n$table-striped-color: $table-color !default;\n$table-striped-bg-factor: .05 !default;\n$table-striped-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-striped-bg-factor) !default;\n\n$table-active-color: $table-color !default;\n$table-active-bg-factor: .1 !default;\n$table-active-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-active-bg-factor) !default;\n\n$table-hover-color: $table-color !default;\n$table-hover-bg-factor: .075 !default;\n$table-hover-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-hover-bg-factor) !default;\n\n$table-border-factor: .2 !default;\n$table-border-width: var(--#{$prefix}border-width) !default;\n$table-border-color: var(--#{$prefix}border-color) !default;\n\n$table-striped-order: odd !default;\n$table-striped-columns-order: even !default;\n\n$table-group-separator-color: currentcolor !default;\n\n$table-caption-color: var(--#{$prefix}secondary-color) !default;\n\n$table-bg-scale: -80% !default;\n// scss-docs-end table-variables\n\n// scss-docs-start table-loop\n$table-variants: (\n \"primary\": shift-color($primary, $table-bg-scale),\n \"secondary\": shift-color($secondary, $table-bg-scale),\n \"success\": shift-color($success, $table-bg-scale),\n \"info\": shift-color($info, $table-bg-scale),\n \"warning\": shift-color($warning, $table-bg-scale),\n \"danger\": shift-color($danger, $table-bg-scale),\n \"light\": $light,\n \"dark\": $dark,\n) !default;\n// scss-docs-end table-loop\n\n\n// Buttons + Forms\n//\n// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.\n\n// scss-docs-start input-btn-variables\n$input-btn-padding-y: .375rem !default;\n$input-btn-padding-x: .75rem !default;\n$input-btn-font-family: null !default;\n$input-btn-font-size: $font-size-base !default;\n$input-btn-line-height: $line-height-base !default;\n\n$input-btn-focus-width: $focus-ring-width !default;\n$input-btn-focus-color-opacity: $focus-ring-opacity !default;\n$input-btn-focus-color: $focus-ring-color !default;\n$input-btn-focus-blur: $focus-ring-blur !default;\n$input-btn-focus-box-shadow: $focus-ring-box-shadow !default;\n\n$input-btn-padding-y-sm: .25rem !default;\n$input-btn-padding-x-sm: .5rem !default;\n$input-btn-font-size-sm: $font-size-sm !default;\n\n$input-btn-padding-y-lg: .5rem !default;\n$input-btn-padding-x-lg: 1rem !default;\n$input-btn-font-size-lg: $font-size-lg !default;\n\n$input-btn-border-width: var(--#{$prefix}border-width) !default;\n// scss-docs-end input-btn-variables\n\n\n// Buttons\n//\n// For each of Bootstrap's buttons, define text, background, and border color.\n\n// scss-docs-start btn-variables\n$btn-color: var(--#{$prefix}body-color) !default;\n$btn-padding-y: $input-btn-padding-y !default;\n$btn-padding-x: $input-btn-padding-x !default;\n$btn-font-family: $input-btn-font-family !default;\n$btn-font-size: $input-btn-font-size !default;\n$btn-line-height: $input-btn-line-height !default;\n$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping\n\n$btn-padding-y-sm: $input-btn-padding-y-sm !default;\n$btn-padding-x-sm: $input-btn-padding-x-sm !default;\n$btn-font-size-sm: $input-btn-font-size-sm !default;\n\n$btn-padding-y-lg: $input-btn-padding-y-lg !default;\n$btn-padding-x-lg: $input-btn-padding-x-lg !default;\n$btn-font-size-lg: $input-btn-font-size-lg !default;\n\n$btn-border-width: $input-btn-border-width !default;\n\n$btn-font-weight: $font-weight-normal !default;\n$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;\n$btn-focus-width: $input-btn-focus-width !default;\n$btn-focus-box-shadow: $input-btn-focus-box-shadow !default;\n$btn-disabled-opacity: .65 !default;\n$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default;\n\n$btn-link-color: var(--#{$prefix}link-color) !default;\n$btn-link-hover-color: var(--#{$prefix}link-hover-color) !default;\n$btn-link-disabled-color: $gray-600 !default;\n$btn-link-focus-shadow-rgb: to-rgb(mix(color-contrast($link-color), $link-color, 15%)) !default;\n\n// Allows for customizing button radius independently from global border radius\n$btn-border-radius: var(--#{$prefix}border-radius) !default;\n$btn-border-radius-sm: var(--#{$prefix}border-radius-sm) !default;\n$btn-border-radius-lg: var(--#{$prefix}border-radius-lg) !default;\n\n$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$btn-hover-bg-shade-amount: 15% !default;\n$btn-hover-bg-tint-amount: 15% !default;\n$btn-hover-border-shade-amount: 20% !default;\n$btn-hover-border-tint-amount: 10% !default;\n$btn-active-bg-shade-amount: 20% !default;\n$btn-active-bg-tint-amount: 20% !default;\n$btn-active-border-shade-amount: 25% !default;\n$btn-active-border-tint-amount: 10% !default;\n// scss-docs-end btn-variables\n\n\n// Forms\n\n// scss-docs-start form-text-variables\n$form-text-margin-top: .25rem !default;\n$form-text-font-size: $small-font-size !default;\n$form-text-font-style: null !default;\n$form-text-font-weight: null !default;\n$form-text-color: var(--#{$prefix}secondary-color) !default;\n// scss-docs-end form-text-variables\n\n// scss-docs-start form-label-variables\n$form-label-margin-bottom: .5rem !default;\n$form-label-font-size: null !default;\n$form-label-font-style: null !default;\n$form-label-font-weight: null !default;\n$form-label-color: null !default;\n// scss-docs-end form-label-variables\n\n// scss-docs-start form-input-variables\n$input-padding-y: $input-btn-padding-y !default;\n$input-padding-x: $input-btn-padding-x !default;\n$input-font-family: $input-btn-font-family !default;\n$input-font-size: $input-btn-font-size !default;\n$input-font-weight: $font-weight-base !default;\n$input-line-height: $input-btn-line-height !default;\n\n$input-padding-y-sm: $input-btn-padding-y-sm !default;\n$input-padding-x-sm: $input-btn-padding-x-sm !default;\n$input-font-size-sm: $input-btn-font-size-sm !default;\n\n$input-padding-y-lg: $input-btn-padding-y-lg !default;\n$input-padding-x-lg: $input-btn-padding-x-lg !default;\n$input-font-size-lg: $input-btn-font-size-lg !default;\n\n$input-bg: var(--#{$prefix}body-bg) !default;\n$input-disabled-color: null !default;\n$input-disabled-bg: var(--#{$prefix}secondary-bg) !default;\n$input-disabled-border-color: null !default;\n\n$input-color: var(--#{$prefix}body-color) !default;\n$input-border-color: var(--#{$prefix}border-color) !default;\n$input-border-width: $input-btn-border-width !default;\n$input-box-shadow: var(--#{$prefix}box-shadow-inset) !default;\n\n$input-border-radius: var(--#{$prefix}border-radius) !default;\n$input-border-radius-sm: var(--#{$prefix}border-radius-sm) !default;\n$input-border-radius-lg: var(--#{$prefix}border-radius-lg) !default;\n\n$input-focus-bg: $input-bg !default;\n$input-focus-border-color: tint-color($component-active-bg, 50%) !default;\n$input-focus-color: $input-color !default;\n$input-focus-width: $input-btn-focus-width !default;\n$input-focus-box-shadow: $input-btn-focus-box-shadow !default;\n\n$input-placeholder-color: var(--#{$prefix}secondary-color) !default;\n$input-plaintext-color: var(--#{$prefix}body-color) !default;\n\n$input-height-border: calc(#{$input-border-width} * 2) !default; // stylelint-disable-line function-disallowed-list\n\n$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default;\n$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default;\n$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y * .5) !default;\n\n$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default;\n$input-height-sm: add($input-line-height * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default;\n$input-height-lg: add($input-line-height * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default;\n\n$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$form-color-width: 3rem !default;\n// scss-docs-end form-input-variables\n\n// scss-docs-start form-check-variables\n$form-check-input-width: 1em !default;\n$form-check-min-height: $font-size-base * $line-height-base !default;\n$form-check-padding-start: $form-check-input-width + .5em !default;\n$form-check-margin-bottom: .125rem !default;\n$form-check-label-color: null !default;\n$form-check-label-cursor: null !default;\n$form-check-transition: null !default;\n\n$form-check-input-active-filter: brightness(90%) !default;\n\n$form-check-input-bg: $input-bg !default;\n$form-check-input-border: var(--#{$prefix}border-width) solid var(--#{$prefix}border-color) !default;\n$form-check-input-border-radius: .25em !default;\n$form-check-radio-border-radius: 50% !default;\n$form-check-input-focus-border: $input-focus-border-color !default;\n$form-check-input-focus-box-shadow: $focus-ring-box-shadow !default;\n\n$form-check-input-checked-color: $component-active-color !default;\n$form-check-input-checked-bg-color: $component-active-bg !default;\n$form-check-input-checked-border-color: $form-check-input-checked-bg-color !default;\n$form-check-input-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-check-radio-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-check-input-indeterminate-color: $component-active-color !default;\n$form-check-input-indeterminate-bg-color: $component-active-bg !default;\n$form-check-input-indeterminate-border-color: $form-check-input-indeterminate-bg-color !default;\n$form-check-input-indeterminate-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-check-input-disabled-opacity: .5 !default;\n$form-check-label-disabled-opacity: $form-check-input-disabled-opacity !default;\n$form-check-btn-check-disabled-opacity: $btn-disabled-opacity !default;\n\n$form-check-inline-margin-end: 1rem !default;\n// scss-docs-end form-check-variables\n\n// scss-docs-start form-switch-variables\n$form-switch-color: rgba($black, .25) !default;\n$form-switch-width: 2em !default;\n$form-switch-padding-start: $form-switch-width + .5em !default;\n$form-switch-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-switch-border-radius: $form-switch-width !default;\n$form-switch-transition: background-position .15s ease-in-out !default;\n\n$form-switch-focus-color: $input-focus-border-color !default;\n$form-switch-focus-bg-image: url(\"data:image/svg+xml,\") !default;\n\n$form-switch-checked-color: $component-active-color !default;\n$form-switch-checked-bg-image: url(\"data:image/svg+xml,\") !default;\n$form-switch-checked-bg-position: right center !default;\n// scss-docs-end form-switch-variables\n\n// scss-docs-start input-group-variables\n$input-group-addon-padding-y: $input-padding-y !default;\n$input-group-addon-padding-x: $input-padding-x !default;\n$input-group-addon-font-weight: $input-font-weight !default;\n$input-group-addon-color: $input-color !default;\n$input-group-addon-bg: var(--#{$prefix}tertiary-bg) !default;\n$input-group-addon-border-color: $input-border-color !default;\n// scss-docs-end input-group-variables\n\n// scss-docs-start form-select-variables\n$form-select-padding-y: $input-padding-y !default;\n$form-select-padding-x: $input-padding-x !default;\n$form-select-font-family: $input-font-family !default;\n$form-select-font-size: $input-font-size !default;\n$form-select-indicator-padding: $form-select-padding-x * 3 !default; // Extra padding for background-image\n$form-select-font-weight: $input-font-weight !default;\n$form-select-line-height: $input-line-height !default;\n$form-select-color: $input-color !default;\n$form-select-bg: $input-bg !default;\n$form-select-disabled-color: null !default;\n$form-select-disabled-bg: $input-disabled-bg !default;\n$form-select-disabled-border-color: $input-disabled-border-color !default;\n$form-select-bg-position: right $form-select-padding-x center !default;\n$form-select-bg-size: 16px 12px !default; // In pixels because image dimensions\n$form-select-indicator-color: $gray-800 !default;\n$form-select-indicator: url(\"data:image/svg+xml,\") !default;\n\n$form-select-feedback-icon-padding-end: $form-select-padding-x * 2.5 + $form-select-indicator-padding !default;\n$form-select-feedback-icon-position: center right $form-select-indicator-padding !default;\n$form-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default;\n\n$form-select-border-width: $input-border-width !default;\n$form-select-border-color: $input-border-color !default;\n$form-select-border-radius: $input-border-radius !default;\n$form-select-box-shadow: var(--#{$prefix}box-shadow-inset) !default;\n\n$form-select-focus-border-color: $input-focus-border-color !default;\n$form-select-focus-width: $input-focus-width !default;\n$form-select-focus-box-shadow: 0 0 0 $form-select-focus-width $input-btn-focus-color !default;\n\n$form-select-padding-y-sm: $input-padding-y-sm !default;\n$form-select-padding-x-sm: $input-padding-x-sm !default;\n$form-select-font-size-sm: $input-font-size-sm !default;\n$form-select-border-radius-sm: $input-border-radius-sm !default;\n\n$form-select-padding-y-lg: $input-padding-y-lg !default;\n$form-select-padding-x-lg: $input-padding-x-lg !default;\n$form-select-font-size-lg: $input-font-size-lg !default;\n$form-select-border-radius-lg: $input-border-radius-lg !default;\n\n$form-select-transition: $input-transition !default;\n// scss-docs-end form-select-variables\n\n// scss-docs-start form-range-variables\n$form-range-track-width: 100% !default;\n$form-range-track-height: .5rem !default;\n$form-range-track-cursor: pointer !default;\n$form-range-track-bg: var(--#{$prefix}secondary-bg) !default;\n$form-range-track-border-radius: 1rem !default;\n$form-range-track-box-shadow: var(--#{$prefix}box-shadow-inset) !default;\n\n$form-range-thumb-width: 1rem !default;\n$form-range-thumb-height: $form-range-thumb-width !default;\n$form-range-thumb-bg: $component-active-bg !default;\n$form-range-thumb-border: 0 !default;\n$form-range-thumb-border-radius: 1rem !default;\n$form-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default;\n$form-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default;\n$form-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in Edge\n$form-range-thumb-active-bg: tint-color($component-active-bg, 70%) !default;\n$form-range-thumb-disabled-bg: var(--#{$prefix}secondary-color) !default;\n$form-range-thumb-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n// scss-docs-end form-range-variables\n\n// scss-docs-start form-file-variables\n$form-file-button-color: $input-color !default;\n$form-file-button-bg: var(--#{$prefix}tertiary-bg) !default;\n$form-file-button-hover-bg: var(--#{$prefix}secondary-bg) !default;\n// scss-docs-end form-file-variables\n\n// scss-docs-start form-floating-variables\n$form-floating-height: add(3.5rem, $input-height-border) !default;\n$form-floating-line-height: 1.25 !default;\n$form-floating-padding-x: $input-padding-x !default;\n$form-floating-padding-y: 1rem !default;\n$form-floating-input-padding-t: 1.625rem !default;\n$form-floating-input-padding-b: .625rem !default;\n$form-floating-label-height: 1.5em !default;\n$form-floating-label-opacity: .65 !default;\n$form-floating-label-transform: scale(.85) translateY(-.5rem) translateX(.15rem) !default;\n$form-floating-label-disabled-color: $gray-600 !default;\n$form-floating-transition: opacity .1s ease-in-out, transform .1s ease-in-out !default;\n// scss-docs-end form-floating-variables\n\n// Form validation\n\n// scss-docs-start form-feedback-variables\n$form-feedback-margin-top: $form-text-margin-top !default;\n$form-feedback-font-size: $form-text-font-size !default;\n$form-feedback-font-style: $form-text-font-style !default;\n$form-feedback-valid-color: $success !default;\n$form-feedback-invalid-color: $danger !default;\n\n$form-feedback-icon-valid-color: $form-feedback-valid-color !default;\n$form-feedback-icon-valid: url(\"data:image/svg+xml,\") !default;\n$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default;\n$form-feedback-icon-invalid: url(\"data:image/svg+xml,\") !default;\n// scss-docs-end form-feedback-variables\n\n// scss-docs-start form-validation-colors\n$form-valid-color: $form-feedback-valid-color !default;\n$form-valid-border-color: $form-feedback-valid-color !default;\n$form-invalid-color: $form-feedback-invalid-color !default;\n$form-invalid-border-color: $form-feedback-invalid-color !default;\n// scss-docs-end form-validation-colors\n\n// scss-docs-start form-validation-states\n$form-validation-states: (\n \"valid\": (\n \"color\": var(--#{$prefix}form-valid-color),\n \"icon\": $form-feedback-icon-valid,\n \"tooltip-color\": #fff,\n \"tooltip-bg-color\": var(--#{$prefix}success),\n \"focus-box-shadow\": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}success-rgb), $input-btn-focus-color-opacity),\n \"border-color\": var(--#{$prefix}form-valid-border-color),\n ),\n \"invalid\": (\n \"color\": var(--#{$prefix}form-invalid-color),\n \"icon\": $form-feedback-icon-invalid,\n \"tooltip-color\": #fff,\n \"tooltip-bg-color\": var(--#{$prefix}danger),\n \"focus-box-shadow\": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}danger-rgb), $input-btn-focus-color-opacity),\n \"border-color\": var(--#{$prefix}form-invalid-border-color),\n )\n) !default;\n// scss-docs-end form-validation-states\n\n// Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n\n// scss-docs-start zindex-stack\n$zindex-dropdown: 1000 !default;\n$zindex-sticky: 1020 !default;\n$zindex-fixed: 1030 !default;\n$zindex-offcanvas-backdrop: 1040 !default;\n$zindex-offcanvas: 1045 !default;\n$zindex-modal-backdrop: 1050 !default;\n$zindex-modal: 1055 !default;\n$zindex-popover: 1070 !default;\n$zindex-tooltip: 1080 !default;\n$zindex-toast: 1090 !default;\n// scss-docs-end zindex-stack\n\n// scss-docs-start zindex-levels-map\n$zindex-levels: (\n n1: -1,\n 0: 0,\n 1: 1,\n 2: 2,\n 3: 3\n) !default;\n// scss-docs-end zindex-levels-map\n\n\n// Navs\n\n// scss-docs-start nav-variables\n$nav-link-padding-y: .5rem !default;\n$nav-link-padding-x: 1rem !default;\n$nav-link-font-size: null !default;\n$nav-link-font-weight: null !default;\n$nav-link-color: var(--#{$prefix}link-color) !default;\n$nav-link-hover-color: var(--#{$prefix}link-hover-color) !default;\n$nav-link-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out !default;\n$nav-link-disabled-color: var(--#{$prefix}secondary-color) !default;\n$nav-link-focus-box-shadow: $focus-ring-box-shadow !default;\n\n$nav-tabs-border-color: var(--#{$prefix}border-color) !default;\n$nav-tabs-border-width: var(--#{$prefix}border-width) !default;\n$nav-tabs-border-radius: var(--#{$prefix}border-radius) !default;\n$nav-tabs-link-hover-border-color: var(--#{$prefix}secondary-bg) var(--#{$prefix}secondary-bg) $nav-tabs-border-color !default;\n$nav-tabs-link-active-color: var(--#{$prefix}emphasis-color) !default;\n$nav-tabs-link-active-bg: var(--#{$prefix}body-bg) !default;\n$nav-tabs-link-active-border-color: var(--#{$prefix}border-color) var(--#{$prefix}border-color) $nav-tabs-link-active-bg !default;\n\n$nav-pills-border-radius: var(--#{$prefix}border-radius) !default;\n$nav-pills-link-active-color: $component-active-color !default;\n$nav-pills-link-active-bg: $component-active-bg !default;\n\n$nav-underline-gap: 1rem !default;\n$nav-underline-border-width: .125rem !default;\n$nav-underline-link-active-color: var(--#{$prefix}emphasis-color) !default;\n// scss-docs-end nav-variables\n\n\n// Navbar\n\n// scss-docs-start navbar-variables\n$navbar-padding-y: $spacer * .5 !default;\n$navbar-padding-x: null !default;\n\n$navbar-nav-link-padding-x: .5rem !default;\n\n$navbar-brand-font-size: $font-size-lg !default;\n// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link\n$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default;\n$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default;\n$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) * .5 !default;\n$navbar-brand-margin-end: 1rem !default;\n\n$navbar-toggler-padding-y: .25rem !default;\n$navbar-toggler-padding-x: .75rem !default;\n$navbar-toggler-font-size: $font-size-lg !default;\n$navbar-toggler-border-radius: $btn-border-radius !default;\n$navbar-toggler-focus-width: $btn-focus-width !default;\n$navbar-toggler-transition: box-shadow .15s ease-in-out !default;\n\n$navbar-light-color: rgba(var(--#{$prefix}emphasis-color-rgb), .65) !default;\n$navbar-light-hover-color: rgba(var(--#{$prefix}emphasis-color-rgb), .8) !default;\n$navbar-light-active-color: rgba(var(--#{$prefix}emphasis-color-rgb), 1) !default;\n$navbar-light-disabled-color: rgba(var(--#{$prefix}emphasis-color-rgb), .3) !default;\n$navbar-light-icon-color: rgba($body-color, .75) !default;\n$navbar-light-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-light-toggler-border-color: rgba(var(--#{$prefix}emphasis-color-rgb), .15) !default;\n$navbar-light-brand-color: $navbar-light-active-color !default;\n$navbar-light-brand-hover-color: $navbar-light-active-color !default;\n// scss-docs-end navbar-variables\n\n// scss-docs-start navbar-dark-variables\n$navbar-dark-color: rgba($white, .55) !default;\n$navbar-dark-hover-color: rgba($white, .75) !default;\n$navbar-dark-active-color: $white !default;\n$navbar-dark-disabled-color: rgba($white, .25) !default;\n$navbar-dark-icon-color: $navbar-dark-color !default;\n$navbar-dark-toggler-icon-bg: url(\"data:image/svg+xml,\") !default;\n$navbar-dark-toggler-border-color: rgba($white, .1) !default;\n$navbar-dark-brand-color: $navbar-dark-active-color !default;\n$navbar-dark-brand-hover-color: $navbar-dark-active-color !default;\n// scss-docs-end navbar-dark-variables\n\n\n// Dropdowns\n//\n// Dropdown menu container and contents.\n\n// scss-docs-start dropdown-variables\n$dropdown-min-width: 10rem !default;\n$dropdown-padding-x: 0 !default;\n$dropdown-padding-y: .5rem !default;\n$dropdown-spacer: .125rem !default;\n$dropdown-font-size: $font-size-base !default;\n$dropdown-color: var(--#{$prefix}body-color) !default;\n$dropdown-bg: var(--#{$prefix}body-bg) !default;\n$dropdown-border-color: var(--#{$prefix}border-color-translucent) !default;\n$dropdown-border-radius: var(--#{$prefix}border-radius) !default;\n$dropdown-border-width: var(--#{$prefix}border-width) !default;\n$dropdown-inner-border-radius: calc(#{$dropdown-border-radius} - #{$dropdown-border-width}) !default; // stylelint-disable-line function-disallowed-list\n$dropdown-divider-bg: $dropdown-border-color !default;\n$dropdown-divider-margin-y: $spacer * .5 !default;\n$dropdown-box-shadow: var(--#{$prefix}box-shadow) !default;\n\n$dropdown-link-color: var(--#{$prefix}body-color) !default;\n$dropdown-link-hover-color: $dropdown-link-color !default;\n$dropdown-link-hover-bg: var(--#{$prefix}tertiary-bg) !default;\n\n$dropdown-link-active-color: $component-active-color !default;\n$dropdown-link-active-bg: $component-active-bg !default;\n\n$dropdown-link-disabled-color: var(--#{$prefix}tertiary-color) !default;\n\n$dropdown-item-padding-y: $spacer * .25 !default;\n$dropdown-item-padding-x: $spacer !default;\n\n$dropdown-header-color: $gray-600 !default;\n$dropdown-header-padding-x: $dropdown-item-padding-x !default;\n$dropdown-header-padding-y: $dropdown-padding-y !default;\n// fusv-disable\n$dropdown-header-padding: $dropdown-header-padding-y $dropdown-header-padding-x !default; // Deprecated in v5.2.0\n// fusv-enable\n// scss-docs-end dropdown-variables\n\n// scss-docs-start dropdown-dark-variables\n$dropdown-dark-color: $gray-300 !default;\n$dropdown-dark-bg: $gray-800 !default;\n$dropdown-dark-border-color: $dropdown-border-color !default;\n$dropdown-dark-divider-bg: $dropdown-divider-bg !default;\n$dropdown-dark-box-shadow: null !default;\n$dropdown-dark-link-color: $dropdown-dark-color !default;\n$dropdown-dark-link-hover-color: $white !default;\n$dropdown-dark-link-hover-bg: rgba($white, .15) !default;\n$dropdown-dark-link-active-color: $dropdown-link-active-color !default;\n$dropdown-dark-link-active-bg: $dropdown-link-active-bg !default;\n$dropdown-dark-link-disabled-color: $gray-500 !default;\n$dropdown-dark-header-color: $gray-500 !default;\n// scss-docs-end dropdown-dark-variables\n\n\n// Pagination\n\n// scss-docs-start pagination-variables\n$pagination-padding-y: .375rem !default;\n$pagination-padding-x: .75rem !default;\n$pagination-padding-y-sm: .25rem !default;\n$pagination-padding-x-sm: .5rem !default;\n$pagination-padding-y-lg: .75rem !default;\n$pagination-padding-x-lg: 1.5rem !default;\n\n$pagination-font-size: $font-size-base !default;\n\n$pagination-color: var(--#{$prefix}link-color) !default;\n$pagination-bg: var(--#{$prefix}body-bg) !default;\n$pagination-border-radius: var(--#{$prefix}border-radius) !default;\n$pagination-border-width: var(--#{$prefix}border-width) !default;\n$pagination-margin-start: calc(-1 * #{$pagination-border-width}) !default; // stylelint-disable-line function-disallowed-list\n$pagination-border-color: var(--#{$prefix}border-color) !default;\n\n$pagination-focus-color: var(--#{$prefix}link-hover-color) !default;\n$pagination-focus-bg: var(--#{$prefix}secondary-bg) !default;\n$pagination-focus-box-shadow: $focus-ring-box-shadow !default;\n$pagination-focus-outline: 0 !default;\n\n$pagination-hover-color: var(--#{$prefix}link-hover-color) !default;\n$pagination-hover-bg: var(--#{$prefix}tertiary-bg) !default;\n$pagination-hover-border-color: var(--#{$prefix}border-color) !default; // Todo in v6: remove this?\n\n$pagination-active-color: $component-active-color !default;\n$pagination-active-bg: $component-active-bg !default;\n$pagination-active-border-color: $component-active-bg !default;\n\n$pagination-disabled-color: var(--#{$prefix}secondary-color) !default;\n$pagination-disabled-bg: var(--#{$prefix}secondary-bg) !default;\n$pagination-disabled-border-color: var(--#{$prefix}border-color) !default;\n\n$pagination-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;\n\n$pagination-border-radius-sm: var(--#{$prefix}border-radius-sm) !default;\n$pagination-border-radius-lg: var(--#{$prefix}border-radius-lg) !default;\n// scss-docs-end pagination-variables\n\n\n// Placeholders\n\n// scss-docs-start placeholders\n$placeholder-opacity-max: .5 !default;\n$placeholder-opacity-min: .2 !default;\n// scss-docs-end placeholders\n\n// Cards\n\n// scss-docs-start card-variables\n$card-spacer-y: $spacer !default;\n$card-spacer-x: $spacer !default;\n$card-title-spacer-y: $spacer * .5 !default;\n$card-title-color: null !default;\n$card-subtitle-color: null !default;\n$card-border-width: var(--#{$prefix}border-width) !default;\n$card-border-color: var(--#{$prefix}border-color-translucent) !default;\n$card-border-radius: var(--#{$prefix}border-radius) !default;\n$card-box-shadow: null !default;\n$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default;\n$card-cap-padding-y: $card-spacer-y * .5 !default;\n$card-cap-padding-x: $card-spacer-x !default;\n$card-cap-bg: rgba(var(--#{$prefix}body-color-rgb), .03) !default;\n$card-cap-color: null !default;\n$card-height: null !default;\n$card-color: null !default;\n$card-bg: var(--#{$prefix}body-bg) !default;\n$card-img-overlay-padding: $spacer !default;\n$card-group-margin: $grid-gutter-width * .5 !default;\n// scss-docs-end card-variables\n\n// Accordion\n\n// scss-docs-start accordion-variables\n$accordion-padding-y: 1rem !default;\n$accordion-padding-x: 1.25rem !default;\n$accordion-color: var(--#{$prefix}body-color) !default;\n$accordion-bg: var(--#{$prefix}body-bg) !default;\n$accordion-border-width: var(--#{$prefix}border-width) !default;\n$accordion-border-color: var(--#{$prefix}border-color) !default;\n$accordion-border-radius: var(--#{$prefix}border-radius) !default;\n$accordion-inner-border-radius: subtract($accordion-border-radius, $accordion-border-width) !default;\n\n$accordion-body-padding-y: $accordion-padding-y !default;\n$accordion-body-padding-x: $accordion-padding-x !default;\n\n$accordion-button-padding-y: $accordion-padding-y !default;\n$accordion-button-padding-x: $accordion-padding-x !default;\n$accordion-button-color: var(--#{$prefix}body-color) !default;\n$accordion-button-bg: var(--#{$prefix}accordion-bg) !default;\n$accordion-transition: $btn-transition, border-radius .15s ease !default;\n$accordion-button-active-bg: var(--#{$prefix}primary-bg-subtle) !default;\n$accordion-button-active-color: var(--#{$prefix}primary-text-emphasis) !default;\n\n// fusv-disable\n$accordion-button-focus-border-color: $input-focus-border-color !default; // Deprecated in v5.3.3\n// fusv-enable\n$accordion-button-focus-box-shadow: $btn-focus-box-shadow !default;\n\n$accordion-icon-width: 1.25rem !default;\n$accordion-icon-color: $body-color !default;\n$accordion-icon-active-color: $primary-text-emphasis !default;\n$accordion-icon-transition: transform .2s ease-in-out !default;\n$accordion-icon-transform: rotate(-180deg) !default;\n\n$accordion-button-icon: url(\"data:image/svg+xml,\") !default;\n$accordion-button-active-icon: url(\"data:image/svg+xml,\") !default;\n// scss-docs-end accordion-variables\n\n// Tooltips\n\n// scss-docs-start tooltip-variables\n$tooltip-font-size: $font-size-sm !default;\n$tooltip-max-width: 200px !default;\n$tooltip-color: var(--#{$prefix}body-bg) !default;\n$tooltip-bg: var(--#{$prefix}emphasis-color) !default;\n$tooltip-border-radius: var(--#{$prefix}border-radius) !default;\n$tooltip-opacity: .9 !default;\n$tooltip-padding-y: $spacer * .25 !default;\n$tooltip-padding-x: $spacer * .5 !default;\n$tooltip-margin: null !default; // TODO: remove this in v6\n\n$tooltip-arrow-width: .8rem !default;\n$tooltip-arrow-height: .4rem !default;\n// fusv-disable\n$tooltip-arrow-color: null !default; // Deprecated in Bootstrap 5.2.0 for CSS variables\n// fusv-enable\n// scss-docs-end tooltip-variables\n\n// Form tooltips must come after regular tooltips\n// scss-docs-start tooltip-feedback-variables\n$form-feedback-tooltip-padding-y: $tooltip-padding-y !default;\n$form-feedback-tooltip-padding-x: $tooltip-padding-x !default;\n$form-feedback-tooltip-font-size: $tooltip-font-size !default;\n$form-feedback-tooltip-line-height: null !default;\n$form-feedback-tooltip-opacity: $tooltip-opacity !default;\n$form-feedback-tooltip-border-radius: $tooltip-border-radius !default;\n// scss-docs-end tooltip-feedback-variables\n\n\n// Popovers\n\n// scss-docs-start popover-variables\n$popover-font-size: $font-size-sm !default;\n$popover-bg: var(--#{$prefix}body-bg) !default;\n$popover-max-width: 276px !default;\n$popover-border-width: var(--#{$prefix}border-width) !default;\n$popover-border-color: var(--#{$prefix}border-color-translucent) !default;\n$popover-border-radius: var(--#{$prefix}border-radius-lg) !default;\n$popover-inner-border-radius: calc(#{$popover-border-radius} - #{$popover-border-width}) !default; // stylelint-disable-line function-disallowed-list\n$popover-box-shadow: var(--#{$prefix}box-shadow) !default;\n\n$popover-header-font-size: $font-size-base !default;\n$popover-header-bg: var(--#{$prefix}secondary-bg) !default;\n$popover-header-color: $headings-color !default;\n$popover-header-padding-y: .5rem !default;\n$popover-header-padding-x: $spacer !default;\n\n$popover-body-color: var(--#{$prefix}body-color) !default;\n$popover-body-padding-y: $spacer !default;\n$popover-body-padding-x: $spacer !default;\n\n$popover-arrow-width: 1rem !default;\n$popover-arrow-height: .5rem !default;\n// scss-docs-end popover-variables\n\n// fusv-disable\n// Deprecated in Bootstrap 5.2.0 for CSS variables\n$popover-arrow-color: $popover-bg !default;\n$popover-arrow-outer-color: var(--#{$prefix}border-color-translucent) !default;\n// fusv-enable\n\n\n// Toasts\n\n// scss-docs-start toast-variables\n$toast-max-width: 350px !default;\n$toast-padding-x: .75rem !default;\n$toast-padding-y: .5rem !default;\n$toast-font-size: .875rem !default;\n$toast-color: null !default;\n$toast-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default;\n$toast-border-width: var(--#{$prefix}border-width) !default;\n$toast-border-color: var(--#{$prefix}border-color-translucent) !default;\n$toast-border-radius: var(--#{$prefix}border-radius) !default;\n$toast-box-shadow: var(--#{$prefix}box-shadow) !default;\n$toast-spacing: $container-padding-x !default;\n\n$toast-header-color: var(--#{$prefix}secondary-color) !default;\n$toast-header-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default;\n$toast-header-border-color: $toast-border-color !default;\n// scss-docs-end toast-variables\n\n\n// Badges\n\n// scss-docs-start badge-variables\n$badge-font-size: .75em !default;\n$badge-font-weight: $font-weight-bold !default;\n$badge-color: $white !default;\n$badge-padding-y: .35em !default;\n$badge-padding-x: .65em !default;\n$badge-border-radius: var(--#{$prefix}border-radius) !default;\n// scss-docs-end badge-variables\n\n\n// Modals\n\n// scss-docs-start modal-variables\n$modal-inner-padding: $spacer !default;\n\n$modal-footer-margin-between: .5rem !default;\n\n$modal-dialog-margin: .5rem !default;\n$modal-dialog-margin-y-sm-up: 1.75rem !default;\n\n$modal-title-line-height: $line-height-base !default;\n\n$modal-content-color: var(--#{$prefix}body-color) !default;\n$modal-content-bg: var(--#{$prefix}body-bg) !default;\n$modal-content-border-color: var(--#{$prefix}border-color-translucent) !default;\n$modal-content-border-width: var(--#{$prefix}border-width) !default;\n$modal-content-border-radius: var(--#{$prefix}border-radius-lg) !default;\n$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default;\n$modal-content-box-shadow-xs: var(--#{$prefix}box-shadow-sm) !default;\n$modal-content-box-shadow-sm-up: var(--#{$prefix}box-shadow) !default;\n\n$modal-backdrop-bg: $black !default;\n$modal-backdrop-opacity: .5 !default;\n\n$modal-header-border-color: var(--#{$prefix}border-color) !default;\n$modal-header-border-width: $modal-content-border-width !default;\n$modal-header-padding-y: $modal-inner-padding !default;\n$modal-header-padding-x: $modal-inner-padding !default;\n$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility\n\n$modal-footer-bg: null !default;\n$modal-footer-border-color: $modal-header-border-color !default;\n$modal-footer-border-width: $modal-header-border-width !default;\n\n$modal-sm: 300px !default;\n$modal-md: 500px !default;\n$modal-lg: 800px !default;\n$modal-xl: 1140px !default;\n\n$modal-fade-transform: translate(0, -50px) !default;\n$modal-show-transform: none !default;\n$modal-transition: transform .3s ease-out !default;\n$modal-scale-transform: scale(1.02) !default;\n// scss-docs-end modal-variables\n\n\n// Alerts\n//\n// Define alert colors, border radius, and padding.\n\n// scss-docs-start alert-variables\n$alert-padding-y: $spacer !default;\n$alert-padding-x: $spacer !default;\n$alert-margin-bottom: 1rem !default;\n$alert-border-radius: var(--#{$prefix}border-radius) !default;\n$alert-link-font-weight: $font-weight-bold !default;\n$alert-border-width: var(--#{$prefix}border-width) !default;\n$alert-dismissible-padding-r: $alert-padding-x * 3 !default; // 3x covers width of x plus default padding on either side\n// scss-docs-end alert-variables\n\n// fusv-disable\n$alert-bg-scale: -80% !default; // Deprecated in v5.2.0, to be removed in v6\n$alert-border-scale: -70% !default; // Deprecated in v5.2.0, to be removed in v6\n$alert-color-scale: 40% !default; // Deprecated in v5.2.0, to be removed in v6\n// fusv-enable\n\n// Progress bars\n\n// scss-docs-start progress-variables\n$progress-height: 1rem !default;\n$progress-font-size: $font-size-base * .75 !default;\n$progress-bg: var(--#{$prefix}secondary-bg) !default;\n$progress-border-radius: var(--#{$prefix}border-radius) !default;\n$progress-box-shadow: var(--#{$prefix}box-shadow-inset) !default;\n$progress-bar-color: $white !default;\n$progress-bar-bg: $primary !default;\n$progress-bar-animation-timing: 1s linear infinite !default;\n$progress-bar-transition: width .6s ease !default;\n// scss-docs-end progress-variables\n\n\n// List group\n\n// scss-docs-start list-group-variables\n$list-group-color: var(--#{$prefix}body-color) !default;\n$list-group-bg: var(--#{$prefix}body-bg) !default;\n$list-group-border-color: var(--#{$prefix}border-color) !default;\n$list-group-border-width: var(--#{$prefix}border-width) !default;\n$list-group-border-radius: var(--#{$prefix}border-radius) !default;\n\n$list-group-item-padding-y: $spacer * .5 !default;\n$list-group-item-padding-x: $spacer !default;\n// fusv-disable\n$list-group-item-bg-scale: -80% !default; // Deprecated in v5.3.0\n$list-group-item-color-scale: 40% !default; // Deprecated in v5.3.0\n// fusv-enable\n\n$list-group-hover-bg: var(--#{$prefix}tertiary-bg) !default;\n$list-group-active-color: $component-active-color !default;\n$list-group-active-bg: $component-active-bg !default;\n$list-group-active-border-color: $list-group-active-bg !default;\n\n$list-group-disabled-color: var(--#{$prefix}secondary-color) !default;\n$list-group-disabled-bg: $list-group-bg !default;\n\n$list-group-action-color: var(--#{$prefix}secondary-color) !default;\n$list-group-action-hover-color: var(--#{$prefix}emphasis-color) !default;\n\n$list-group-action-active-color: var(--#{$prefix}body-color) !default;\n$list-group-action-active-bg: var(--#{$prefix}secondary-bg) !default;\n// scss-docs-end list-group-variables\n\n\n// Image thumbnails\n\n// scss-docs-start thumbnail-variables\n$thumbnail-padding: .25rem !default;\n$thumbnail-bg: var(--#{$prefix}body-bg) !default;\n$thumbnail-border-width: var(--#{$prefix}border-width) !default;\n$thumbnail-border-color: var(--#{$prefix}border-color) !default;\n$thumbnail-border-radius: var(--#{$prefix}border-radius) !default;\n$thumbnail-box-shadow: var(--#{$prefix}box-shadow-sm) !default;\n// scss-docs-end thumbnail-variables\n\n\n// Figures\n\n// scss-docs-start figure-variables\n$figure-caption-font-size: $small-font-size !default;\n$figure-caption-color: var(--#{$prefix}secondary-color) !default;\n// scss-docs-end figure-variables\n\n\n// Breadcrumbs\n\n// scss-docs-start breadcrumb-variables\n$breadcrumb-font-size: null !default;\n$breadcrumb-padding-y: 0 !default;\n$breadcrumb-padding-x: 0 !default;\n$breadcrumb-item-padding-x: .5rem !default;\n$breadcrumb-margin-bottom: 1rem !default;\n$breadcrumb-bg: null !default;\n$breadcrumb-divider-color: var(--#{$prefix}secondary-color) !default;\n$breadcrumb-active-color: var(--#{$prefix}secondary-color) !default;\n$breadcrumb-divider: quote(\"/\") !default;\n$breadcrumb-divider-flipped: $breadcrumb-divider !default;\n$breadcrumb-border-radius: null !default;\n// scss-docs-end breadcrumb-variables\n\n// Carousel\n\n// scss-docs-start carousel-variables\n$carousel-control-color: $white !default;\n$carousel-control-width: 15% !default;\n$carousel-control-opacity: .5 !default;\n$carousel-control-hover-opacity: .9 !default;\n$carousel-control-transition: opacity .15s ease !default;\n$carousel-control-icon-filter: null !default;\n\n$carousel-indicator-width: 30px !default;\n$carousel-indicator-height: 3px !default;\n$carousel-indicator-hit-area-height: 10px !default;\n$carousel-indicator-spacer: 3px !default;\n$carousel-indicator-opacity: .5 !default;\n$carousel-indicator-active-bg: $white !default;\n$carousel-indicator-active-opacity: 1 !default;\n$carousel-indicator-transition: opacity .6s ease !default;\n\n$carousel-caption-width: 70% !default;\n$carousel-caption-color: $white !default;\n$carousel-caption-padding-y: 1.25rem !default;\n$carousel-caption-spacer: 1.25rem !default;\n\n$carousel-control-icon-width: 2rem !default;\n\n$carousel-control-prev-icon-bg: url(\"data:image/svg+xml,\") !default;\n$carousel-control-next-icon-bg: url(\"data:image/svg+xml,\") !default;\n\n$carousel-transition-duration: .6s !default;\n$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`)\n// scss-docs-end carousel-variables\n\n// scss-docs-start carousel-dark-variables\n$carousel-dark-indicator-active-bg: $black !default; // Deprecated in v5.3.4\n$carousel-dark-caption-color: $black !default; // Deprecated in v5.3.4\n$carousel-dark-control-icon-filter: invert(1) grayscale(100) !default; // Deprecated in v5.3.4\n// scss-docs-end carousel-dark-variables\n\n\n// Spinners\n\n// scss-docs-start spinner-variables\n$spinner-width: 2rem !default;\n$spinner-height: $spinner-width !default;\n$spinner-vertical-align: -.125em !default;\n$spinner-border-width: .25em !default;\n$spinner-animation-speed: .75s !default;\n\n$spinner-width-sm: 1rem !default;\n$spinner-height-sm: $spinner-width-sm !default;\n$spinner-border-width-sm: .2em !default;\n// scss-docs-end spinner-variables\n\n\n// Close\n\n// scss-docs-start close-variables\n$btn-close-width: 1em !default;\n$btn-close-height: $btn-close-width !default;\n$btn-close-padding-x: .25em !default;\n$btn-close-padding-y: $btn-close-padding-x !default;\n$btn-close-color: $black !default;\n$btn-close-bg: url(\"data:image/svg+xml,\") !default;\n$btn-close-focus-shadow: $focus-ring-box-shadow !default;\n$btn-close-opacity: .5 !default;\n$btn-close-hover-opacity: .75 !default;\n$btn-close-focus-opacity: 1 !default;\n$btn-close-disabled-opacity: .25 !default;\n$btn-close-filter: null !default;\n$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default; // Deprecated in v5.3.4\n// scss-docs-end close-variables\n\n\n// Offcanvas\n\n// scss-docs-start offcanvas-variables\n$offcanvas-padding-y: $modal-inner-padding !default;\n$offcanvas-padding-x: $modal-inner-padding !default;\n$offcanvas-horizontal-width: 400px !default;\n$offcanvas-vertical-height: 30vh !default;\n$offcanvas-transition-duration: .3s !default;\n$offcanvas-border-color: $modal-content-border-color !default;\n$offcanvas-border-width: $modal-content-border-width !default;\n$offcanvas-title-line-height: $modal-title-line-height !default;\n$offcanvas-bg-color: var(--#{$prefix}body-bg) !default;\n$offcanvas-color: var(--#{$prefix}body-color) !default;\n$offcanvas-box-shadow: $modal-content-box-shadow-xs !default;\n$offcanvas-backdrop-bg: $modal-backdrop-bg !default;\n$offcanvas-backdrop-opacity: $modal-backdrop-opacity !default;\n// scss-docs-end offcanvas-variables\n\n// Code\n\n$code-font-size: $small-font-size !default;\n$code-color: $pink !default;\n\n$kbd-padding-y: .1875rem !default;\n$kbd-padding-x: .375rem !default;\n$kbd-font-size: $code-font-size !default;\n$kbd-color: var(--#{$prefix}body-bg) !default;\n$kbd-bg: var(--#{$prefix}body-color) !default;\n$nested-kbd-font-weight: null !default; // Deprecated in v5.2.0, removing in v6\n\n$pre-color: null !default;\n\n@import \"variables-dark\"; // TODO: can be removed safely in v6, only here to avoid breaking changes in v5.3\n","// Row\n//\n// Rows contain your columns.\n\n:root {\n @each $name, $value in $grid-breakpoints {\n --#{$prefix}breakpoint-#{$name}: #{$value};\n }\n}\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n\n > * {\n @include make-col-ready();\n }\n }\n}\n\n@if $enable-cssgrid {\n .grid {\n display: grid;\n grid-template-rows: repeat(var(--#{$prefix}rows, 1), 1fr);\n grid-template-columns: repeat(var(--#{$prefix}columns, #{$grid-columns}), 1fr);\n gap: var(--#{$prefix}gap, #{$grid-gutter-width});\n\n @include make-cssgrid();\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-row($gutter: $grid-gutter-width) {\n --#{$prefix}gutter-x: #{$gutter};\n --#{$prefix}gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n // TODO: Revisit calc order after https://github.com/react-bootstrap/react-bootstrap/issues/6039 is fixed\n margin-top: calc(-1 * var(--#{$prefix}gutter-y)); // stylelint-disable-line function-disallowed-list\n margin-right: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list\n margin-left: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list\n}\n\n@mixin make-col-ready() {\n // Add box sizing if only the grid is loaded\n box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null);\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we set the width\n // later on to override this initial width.\n flex-shrink: 0;\n width: 100%;\n max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid\n padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-top: var(--#{$prefix}gutter-y);\n}\n\n@mixin make-col($size: false, $columns: $grid-columns) {\n @if $size {\n flex: 0 0 auto;\n width: percentage(divide($size, $columns));\n\n } @else {\n flex: 1 1 0;\n max-width: 100%;\n }\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: divide($size, $columns);\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// number of columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 auto;\n width: percentage(divide(1, $count));\n }\n}\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex: 1 0 0;\n }\n\n .row-cols#{$infix}-auto > * {\n @include make-col-auto();\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n\n // Gutters\n //\n // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns.\n @each $key, $value in $gutters {\n .g#{$infix}-#{$key},\n .gx#{$infix}-#{$key} {\n --#{$prefix}gutter-x: #{$value};\n }\n\n .g#{$infix}-#{$key},\n .gy#{$infix}-#{$key} {\n --#{$prefix}gutter-y: #{$value};\n }\n }\n }\n }\n}\n\n@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .g-col#{$infix}-#{$i} {\n grid-column: auto / span $i;\n }\n }\n\n // Start with `1` because `0` is an invalid value.\n // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible.\n @for $i from 1 through ($columns - 1) {\n .g-start#{$infix}-#{$i} {\n grid-column-start: $i;\n }\n }\n }\n }\n }\n}\n","// Utility generator\n// Used to generate utilities & print utilities\n@mixin generate-utility($utility, $infix: \"\", $is-rfs-media-query: false) {\n $values: map-get($utility, values);\n\n // If the values are a list or string, convert it into a map\n @if type-of($values) == \"string\" or type-of(nth($values, 1)) != \"list\" {\n $values: zip($values, $values);\n }\n\n @each $key, $value in $values {\n $properties: map-get($utility, property);\n\n // Multiple properties are possible, for example with vertical or horizontal margins or paddings\n @if type-of($properties) == \"string\" {\n $properties: append((), $properties);\n }\n\n // Use custom class if present\n $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));\n $property-class: if($property-class == null, \"\", $property-class);\n\n // Use custom CSS variable name if present, otherwise default to `class`\n $css-variable-name: if(map-has-key($utility, css-variable-name), map-get($utility, css-variable-name), map-get($utility, class));\n\n // State params to generate pseudo-classes\n $state: if(map-has-key($utility, state), map-get($utility, state), ());\n\n $infix: if($property-class == \"\" and str-slice($infix, 1, 1) == \"-\", str-slice($infix, 2), $infix);\n\n // Don't prefix if value key is null (e.g. with shadow class)\n $property-class-modifier: if($key, if($property-class == \"\" and $infix == \"\", \"\", \"-\") + $key, \"\");\n\n @if map-get($utility, rfs) {\n // Inside the media query\n @if $is-rfs-media-query {\n $val: rfs-value($value);\n\n // Do not render anything if fluid and non fluid values are the same\n $value: if($val == rfs-fluid-value($value), null, $val);\n }\n @else {\n $value: rfs-fluid-value($value);\n }\n }\n\n $is-css-var: map-get($utility, css-var);\n $is-local-vars: map-get($utility, local-vars);\n $is-rtl: map-get($utility, rtl);\n\n @if $value != null {\n @if $is-rtl == false {\n /* rtl:begin:remove */\n }\n\n @if $is-css-var {\n .#{$property-class + $infix + $property-class-modifier} {\n --#{$prefix}#{$css-variable-name}: #{$value};\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n --#{$prefix}#{$css-variable-name}: #{$value};\n }\n }\n } @else {\n .#{$property-class + $infix + $property-class-modifier} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $variable in $is-local-vars {\n --#{$prefix}#{$local-var}: #{$variable};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $variable in $is-local-vars {\n --#{$prefix}#{$local-var}: #{$variable};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n }\n }\n\n @if $is-rtl == false {\n /* rtl:end:remove */\n }\n }\n }\n}\n","// Loop over each breakpoint\n@each $breakpoint in map-keys($grid-breakpoints) {\n\n // Generate media query if needed\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix);\n }\n }\n }\n}\n\n// RFS rescaling\n@media (min-width: $rfs-mq-value) {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) {\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix, true);\n }\n }\n }\n }\n}\n\n\n// Print utilities\n@media print {\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Then check if the utility needs print styles\n @if type-of($utility) == \"map\" and map-get($utility, print) == true {\n @include generate-utility($utility, \"-print\");\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css new file mode 100644 index 0000000..9b24320 --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap Grid v5.3.8 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-left:calc(var(--bs-gutter-x) * .5);padding-right:calc(var(--bs-gutter-x) * .5);margin-left:auto;margin-right:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-left:calc(-.5 * var(--bs-gutter-x));margin-right:calc(-.5 * var(--bs-gutter-x))}.row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-left:calc(var(--bs-gutter-x) * .5);padding-right:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-right:8.33333333%}.offset-2{margin-right:16.66666667%}.offset-3{margin-right:25%}.offset-4{margin-right:33.33333333%}.offset-5{margin-right:41.66666667%}.offset-6{margin-right:50%}.offset-7{margin-right:58.33333333%}.offset-8{margin-right:66.66666667%}.offset-9{margin-right:75%}.offset-10{margin-right:83.33333333%}.offset-11{margin-right:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-right:0}.offset-sm-1{margin-right:8.33333333%}.offset-sm-2{margin-right:16.66666667%}.offset-sm-3{margin-right:25%}.offset-sm-4{margin-right:33.33333333%}.offset-sm-5{margin-right:41.66666667%}.offset-sm-6{margin-right:50%}.offset-sm-7{margin-right:58.33333333%}.offset-sm-8{margin-right:66.66666667%}.offset-sm-9{margin-right:75%}.offset-sm-10{margin-right:83.33333333%}.offset-sm-11{margin-right:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-right:0}.offset-md-1{margin-right:8.33333333%}.offset-md-2{margin-right:16.66666667%}.offset-md-3{margin-right:25%}.offset-md-4{margin-right:33.33333333%}.offset-md-5{margin-right:41.66666667%}.offset-md-6{margin-right:50%}.offset-md-7{margin-right:58.33333333%}.offset-md-8{margin-right:66.66666667%}.offset-md-9{margin-right:75%}.offset-md-10{margin-right:83.33333333%}.offset-md-11{margin-right:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-right:0}.offset-lg-1{margin-right:8.33333333%}.offset-lg-2{margin-right:16.66666667%}.offset-lg-3{margin-right:25%}.offset-lg-4{margin-right:33.33333333%}.offset-lg-5{margin-right:41.66666667%}.offset-lg-6{margin-right:50%}.offset-lg-7{margin-right:58.33333333%}.offset-lg-8{margin-right:66.66666667%}.offset-lg-9{margin-right:75%}.offset-lg-10{margin-right:83.33333333%}.offset-lg-11{margin-right:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-right:0}.offset-xl-1{margin-right:8.33333333%}.offset-xl-2{margin-right:16.66666667%}.offset-xl-3{margin-right:25%}.offset-xl-4{margin-right:33.33333333%}.offset-xl-5{margin-right:41.66666667%}.offset-xl-6{margin-right:50%}.offset-xl-7{margin-right:58.33333333%}.offset-xl-8{margin-right:66.66666667%}.offset-xl-9{margin-right:75%}.offset-xl-10{margin-right:83.33333333%}.offset-xl-11{margin-right:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-right:0}.offset-xxl-1{margin-right:8.33333333%}.offset-xxl-2{margin-right:16.66666667%}.offset-xxl-3{margin-right:25%}.offset-xxl-4{margin-right:33.33333333%}.offset-xxl-5{margin-right:41.66666667%}.offset-xxl-6{margin-right:50%}.offset-xxl-7{margin-right:58.33333333%}.offset-xxl-8{margin-right:66.66666667%}.offset-xxl-9{margin-right:75%}.offset-xxl-10{margin-right:83.33333333%}.offset-xxl-11{margin-right:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-left:0!important;margin-right:0!important}.mx-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-3{margin-left:1rem!important;margin-right:1rem!important}.mx-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-5{margin-left:3rem!important;margin-right:3rem!important}.mx-auto{margin-left:auto!important;margin-right:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-left:0!important}.me-1{margin-left:.25rem!important}.me-2{margin-left:.5rem!important}.me-3{margin-left:1rem!important}.me-4{margin-left:1.5rem!important}.me-5{margin-left:3rem!important}.me-auto{margin-left:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-right:0!important}.ms-1{margin-right:.25rem!important}.ms-2{margin-right:.5rem!important}.ms-3{margin-right:1rem!important}.ms-4{margin-right:1.5rem!important}.ms-5{margin-right:3rem!important}.ms-auto{margin-right:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-left:0!important;padding-right:0!important}.px-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-3{padding-left:1rem!important;padding-right:1rem!important}.px-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-5{padding-left:3rem!important;padding-right:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-left:0!important}.pe-1{padding-left:.25rem!important}.pe-2{padding-left:.5rem!important}.pe-3{padding-left:1rem!important}.pe-4{padding-left:1.5rem!important}.pe-5{padding-left:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-right:0!important}.ps-1{padding-right:.25rem!important}.ps-2{padding-right:.5rem!important}.ps-3{padding-right:1rem!important}.ps-4{padding-right:1.5rem!important}.ps-5{padding-right:3rem!important}@media (min-width:576px){.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-left:0!important;margin-right:0!important}.mx-sm-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-sm-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-sm-3{margin-left:1rem!important;margin-right:1rem!important}.mx-sm-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-sm-5{margin-left:3rem!important;margin-right:3rem!important}.mx-sm-auto{margin-left:auto!important;margin-right:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-left:0!important}.me-sm-1{margin-left:.25rem!important}.me-sm-2{margin-left:.5rem!important}.me-sm-3{margin-left:1rem!important}.me-sm-4{margin-left:1.5rem!important}.me-sm-5{margin-left:3rem!important}.me-sm-auto{margin-left:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-right:0!important}.ms-sm-1{margin-right:.25rem!important}.ms-sm-2{margin-right:.5rem!important}.ms-sm-3{margin-right:1rem!important}.ms-sm-4{margin-right:1.5rem!important}.ms-sm-5{margin-right:3rem!important}.ms-sm-auto{margin-right:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-left:0!important;padding-right:0!important}.px-sm-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-sm-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-sm-3{padding-left:1rem!important;padding-right:1rem!important}.px-sm-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-sm-5{padding-left:3rem!important;padding-right:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-left:0!important}.pe-sm-1{padding-left:.25rem!important}.pe-sm-2{padding-left:.5rem!important}.pe-sm-3{padding-left:1rem!important}.pe-sm-4{padding-left:1.5rem!important}.pe-sm-5{padding-left:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-right:0!important}.ps-sm-1{padding-right:.25rem!important}.ps-sm-2{padding-right:.5rem!important}.ps-sm-3{padding-right:1rem!important}.ps-sm-4{padding-right:1.5rem!important}.ps-sm-5{padding-right:3rem!important}}@media (min-width:768px){.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-left:0!important;margin-right:0!important}.mx-md-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-md-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-md-3{margin-left:1rem!important;margin-right:1rem!important}.mx-md-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-md-5{margin-left:3rem!important;margin-right:3rem!important}.mx-md-auto{margin-left:auto!important;margin-right:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-left:0!important}.me-md-1{margin-left:.25rem!important}.me-md-2{margin-left:.5rem!important}.me-md-3{margin-left:1rem!important}.me-md-4{margin-left:1.5rem!important}.me-md-5{margin-left:3rem!important}.me-md-auto{margin-left:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-right:0!important}.ms-md-1{margin-right:.25rem!important}.ms-md-2{margin-right:.5rem!important}.ms-md-3{margin-right:1rem!important}.ms-md-4{margin-right:1.5rem!important}.ms-md-5{margin-right:3rem!important}.ms-md-auto{margin-right:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-left:0!important;padding-right:0!important}.px-md-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-md-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-md-3{padding-left:1rem!important;padding-right:1rem!important}.px-md-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-md-5{padding-left:3rem!important;padding-right:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-left:0!important}.pe-md-1{padding-left:.25rem!important}.pe-md-2{padding-left:.5rem!important}.pe-md-3{padding-left:1rem!important}.pe-md-4{padding-left:1.5rem!important}.pe-md-5{padding-left:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-right:0!important}.ps-md-1{padding-right:.25rem!important}.ps-md-2{padding-right:.5rem!important}.ps-md-3{padding-right:1rem!important}.ps-md-4{padding-right:1.5rem!important}.ps-md-5{padding-right:3rem!important}}@media (min-width:992px){.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-left:0!important;margin-right:0!important}.mx-lg-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-lg-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-lg-3{margin-left:1rem!important;margin-right:1rem!important}.mx-lg-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-lg-5{margin-left:3rem!important;margin-right:3rem!important}.mx-lg-auto{margin-left:auto!important;margin-right:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-left:0!important}.me-lg-1{margin-left:.25rem!important}.me-lg-2{margin-left:.5rem!important}.me-lg-3{margin-left:1rem!important}.me-lg-4{margin-left:1.5rem!important}.me-lg-5{margin-left:3rem!important}.me-lg-auto{margin-left:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-right:0!important}.ms-lg-1{margin-right:.25rem!important}.ms-lg-2{margin-right:.5rem!important}.ms-lg-3{margin-right:1rem!important}.ms-lg-4{margin-right:1.5rem!important}.ms-lg-5{margin-right:3rem!important}.ms-lg-auto{margin-right:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-left:0!important;padding-right:0!important}.px-lg-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-lg-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-lg-3{padding-left:1rem!important;padding-right:1rem!important}.px-lg-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-lg-5{padding-left:3rem!important;padding-right:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-left:0!important}.pe-lg-1{padding-left:.25rem!important}.pe-lg-2{padding-left:.5rem!important}.pe-lg-3{padding-left:1rem!important}.pe-lg-4{padding-left:1.5rem!important}.pe-lg-5{padding-left:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-right:0!important}.ps-lg-1{padding-right:.25rem!important}.ps-lg-2{padding-right:.5rem!important}.ps-lg-3{padding-right:1rem!important}.ps-lg-4{padding-right:1.5rem!important}.ps-lg-5{padding-right:3rem!important}}@media (min-width:1200px){.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-left:0!important;margin-right:0!important}.mx-xl-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-xl-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-xl-3{margin-left:1rem!important;margin-right:1rem!important}.mx-xl-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-xl-5{margin-left:3rem!important;margin-right:3rem!important}.mx-xl-auto{margin-left:auto!important;margin-right:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-left:0!important}.me-xl-1{margin-left:.25rem!important}.me-xl-2{margin-left:.5rem!important}.me-xl-3{margin-left:1rem!important}.me-xl-4{margin-left:1.5rem!important}.me-xl-5{margin-left:3rem!important}.me-xl-auto{margin-left:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-right:0!important}.ms-xl-1{margin-right:.25rem!important}.ms-xl-2{margin-right:.5rem!important}.ms-xl-3{margin-right:1rem!important}.ms-xl-4{margin-right:1.5rem!important}.ms-xl-5{margin-right:3rem!important}.ms-xl-auto{margin-right:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-left:0!important;padding-right:0!important}.px-xl-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-xl-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-xl-3{padding-left:1rem!important;padding-right:1rem!important}.px-xl-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-xl-5{padding-left:3rem!important;padding-right:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-left:0!important}.pe-xl-1{padding-left:.25rem!important}.pe-xl-2{padding-left:.5rem!important}.pe-xl-3{padding-left:1rem!important}.pe-xl-4{padding-left:1.5rem!important}.pe-xl-5{padding-left:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-right:0!important}.ps-xl-1{padding-right:.25rem!important}.ps-xl-2{padding-right:.5rem!important}.ps-xl-3{padding-right:1rem!important}.ps-xl-4{padding-right:1.5rem!important}.ps-xl-5{padding-right:3rem!important}}@media (min-width:1400px){.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-left:0!important;margin-right:0!important}.mx-xxl-1{margin-left:.25rem!important;margin-right:.25rem!important}.mx-xxl-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-xxl-3{margin-left:1rem!important;margin-right:1rem!important}.mx-xxl-4{margin-left:1.5rem!important;margin-right:1.5rem!important}.mx-xxl-5{margin-left:3rem!important;margin-right:3rem!important}.mx-xxl-auto{margin-left:auto!important;margin-right:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-left:0!important}.me-xxl-1{margin-left:.25rem!important}.me-xxl-2{margin-left:.5rem!important}.me-xxl-3{margin-left:1rem!important}.me-xxl-4{margin-left:1.5rem!important}.me-xxl-5{margin-left:3rem!important}.me-xxl-auto{margin-left:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-right:0!important}.ms-xxl-1{margin-right:.25rem!important}.ms-xxl-2{margin-right:.5rem!important}.ms-xxl-3{margin-right:1rem!important}.ms-xxl-4{margin-right:1.5rem!important}.ms-xxl-5{margin-right:3rem!important}.ms-xxl-auto{margin-right:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-left:0!important;padding-right:0!important}.px-xxl-1{padding-left:.25rem!important;padding-right:.25rem!important}.px-xxl-2{padding-left:.5rem!important;padding-right:.5rem!important}.px-xxl-3{padding-left:1rem!important;padding-right:1rem!important}.px-xxl-4{padding-left:1.5rem!important;padding-right:1.5rem!important}.px-xxl-5{padding-left:3rem!important;padding-right:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-left:0!important}.pe-xxl-1{padding-left:.25rem!important}.pe-xxl-2{padding-left:.5rem!important}.pe-xxl-3{padding-left:1rem!important}.pe-xxl-4{padding-left:1.5rem!important}.pe-xxl-5{padding-left:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-right:0!important}.ps-xxl-1{padding-right:.25rem!important}.ps-xxl-2{padding-right:.5rem!important}.ps-xxl-3{padding-right:1rem!important}.ps-xxl-4{padding-right:1.5rem!important}.ps-xxl-5{padding-right:3rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap-grid.rtl.min.css.map */ \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map new file mode 100644 index 0000000..1c01586 --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_containers.scss","dist/css/bootstrap-grid.rtl.css","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"AACE;;;;ACKA,WCAF,iBAGA,cACA,cACA,cAHA,cADA,eCJE,cAAA,OACA,cAAA,EACA,MAAA,KACA,aAAA,8BACA,cAAA,8BACA,YAAA,KACA,aAAA,KCsDE,yBH5CE,WAAA,cACE,UAAA,OG2CJ,yBH5CE,WAAA,cAAA,cACE,UAAA,OG2CJ,yBH5CE,WAAA,cAAA,cAAA,cACE,UAAA,OG2CJ,0BH5CE,WAAA,cAAA,cAAA,cAAA,cACE,UAAA,QG2CJ,0BH5CE,WAAA,cAAA,cAAA,cAAA,cAAA,eACE,UAAA,QIhBR,MAEI,mBAAA,EAAA,mBAAA,MAAA,mBAAA,MAAA,mBAAA,MAAA,mBAAA,OAAA,oBAAA,OAKF,KCNA,cAAA,OACA,cAAA,EACA,QAAA,KACA,UAAA,KAEA,WAAA,8BACA,YAAA,+BACA,aAAA,+BDEE,OCGF,WAAA,WAIA,YAAA,EACA,MAAA,KACA,UAAA,KACA,aAAA,8BACA,cAAA,8BACA,WAAA,mBA+CI,KACE,KAAA,EAAA,EAAA,EAGF,iBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,cACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,cACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,UAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,OAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,QAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,UAxDV,aAAA,YAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,IAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,IAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,aAwDU,UAxDV,aAAA,IAwDU,WAxDV,aAAA,aAwDU,WAxDV,aAAA,aAmEM,KJ6GR,MI3GU,cAAA,EAGF,KJ6GR,MI3GU,cAAA,EAPF,KJuHR,MIrHU,cAAA,QAGF,KJuHR,MIrHU,cAAA,QAPF,KJiIR,MI/HU,cAAA,OAGF,KJiIR,MI/HU,cAAA,OAPF,KJ2IR,MIzIU,cAAA,KAGF,KJ2IR,MIzIU,cAAA,KAPF,KJqJR,MInJU,cAAA,OAGF,KJqJR,MInJU,cAAA,OAPF,KJ+JR,MI7JU,cAAA,KAGF,KJ+JR,MI7JU,cAAA,KF1DN,yBEUE,QACE,KAAA,EAAA,EAAA,EAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,aAAA,EAwDU,aAxDV,aAAA,YAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAmEM,QJiSN,SI/RQ,cAAA,EAGF,QJgSN,SI9RQ,cAAA,EAPF,QJySN,SIvSQ,cAAA,QAGF,QJwSN,SItSQ,cAAA,QAPF,QJiTN,SI/SQ,cAAA,OAGF,QJgTN,SI9SQ,cAAA,OAPF,QJyTN,SIvTQ,cAAA,KAGF,QJwTN,SItTQ,cAAA,KAPF,QJiUN,SI/TQ,cAAA,OAGF,QJgUN,SI9TQ,cAAA,OAPF,QJyUN,SIvUQ,cAAA,KAGF,QJwUN,SItUQ,cAAA,MF1DN,yBEUE,QACE,KAAA,EAAA,EAAA,EAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,aAAA,EAwDU,aAxDV,aAAA,YAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAmEM,QJ0cN,SIxcQ,cAAA,EAGF,QJycN,SIvcQ,cAAA,EAPF,QJkdN,SIhdQ,cAAA,QAGF,QJidN,SI/cQ,cAAA,QAPF,QJ0dN,SIxdQ,cAAA,OAGF,QJydN,SIvdQ,cAAA,OAPF,QJkeN,SIheQ,cAAA,KAGF,QJieN,SI/dQ,cAAA,KAPF,QJ0eN,SIxeQ,cAAA,OAGF,QJyeN,SIveQ,cAAA,OAPF,QJkfN,SIhfQ,cAAA,KAGF,QJifN,SI/eQ,cAAA,MF1DN,yBEUE,QACE,KAAA,EAAA,EAAA,EAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,aAAA,EAwDU,aAxDV,aAAA,YAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAmEM,QJmnBN,SIjnBQ,cAAA,EAGF,QJknBN,SIhnBQ,cAAA,EAPF,QJ2nBN,SIznBQ,cAAA,QAGF,QJ0nBN,SIxnBQ,cAAA,QAPF,QJmoBN,SIjoBQ,cAAA,OAGF,QJkoBN,SIhoBQ,cAAA,OAPF,QJ2oBN,SIzoBQ,cAAA,KAGF,QJ0oBN,SIxoBQ,cAAA,KAPF,QJmpBN,SIjpBQ,cAAA,OAGF,QJkpBN,SIhpBQ,cAAA,OAPF,QJ2pBN,SIzpBQ,cAAA,KAGF,QJ0pBN,SIxpBQ,cAAA,MF1DN,0BEUE,QACE,KAAA,EAAA,EAAA,EAGF,oBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,iBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,aAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,UAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,aAxDV,aAAA,EAwDU,aAxDV,aAAA,YAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,aAwDU,aAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAmEM,QJ4xBN,SI1xBQ,cAAA,EAGF,QJ2xBN,SIzxBQ,cAAA,EAPF,QJoyBN,SIlyBQ,cAAA,QAGF,QJmyBN,SIjyBQ,cAAA,QAPF,QJ4yBN,SI1yBQ,cAAA,OAGF,QJ2yBN,SIzyBQ,cAAA,OAPF,QJozBN,SIlzBQ,cAAA,KAGF,QJmzBN,SIjzBQ,cAAA,KAPF,QJ4zBN,SI1zBQ,cAAA,OAGF,QJ2zBN,SIzzBQ,cAAA,OAPF,QJo0BN,SIl0BQ,cAAA,KAGF,QJm0BN,SIj0BQ,cAAA,MF1DN,0BEUE,SACE,KAAA,EAAA,EAAA,EAGF,qBApCJ,KAAA,EAAA,EAAA,KACA,MAAA,KAcA,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,KAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,aAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,IAFF,kBACE,KAAA,EAAA,EAAA,KACA,MAAA,aA+BE,cAhDJ,KAAA,EAAA,EAAA,KACA,MAAA,KAqDQ,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,YA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,WAhEN,KAAA,EAAA,EAAA,KACA,MAAA,IA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,aA+DM,YAhEN,KAAA,EAAA,EAAA,KACA,MAAA,KAuEQ,cAxDV,aAAA,EAwDU,cAxDV,aAAA,YAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,IAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,aAwDU,cAxDV,aAAA,IAwDU,eAxDV,aAAA,aAwDU,eAxDV,aAAA,aAmEM,SJq8BN,UIn8BQ,cAAA,EAGF,SJo8BN,UIl8BQ,cAAA,EAPF,SJ68BN,UI38BQ,cAAA,QAGF,SJ48BN,UI18BQ,cAAA,QAPF,SJq9BN,UIn9BQ,cAAA,OAGF,SJo9BN,UIl9BQ,cAAA,OAPF,SJ69BN,UI39BQ,cAAA,KAGF,SJ49BN,UI19BQ,cAAA,KAPF,SJq+BN,UIn+BQ,cAAA,OAGF,SJo+BN,UIl+BQ,cAAA,OAPF,SJ6+BN,UI3+BQ,cAAA,KAGF,SJ4+BN,UI1+BQ,cAAA,MCvDF,UAOI,QAAA,iBAPJ,gBAOI,QAAA,uBAPJ,SAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,eAOI,QAAA,sBAPJ,SAOI,QAAA,gBAPJ,aAOI,QAAA,oBAPJ,cAOI,QAAA,qBAPJ,QAOI,QAAA,eAPJ,eAOI,QAAA,sBAPJ,QAOI,QAAA,eAPJ,WAOI,KAAA,EAAA,EAAA,eAPJ,UAOI,eAAA,cAPJ,aAOI,eAAA,iBAPJ,kBAOI,eAAA,sBAPJ,qBAOI,eAAA,yBAPJ,aAOI,UAAA,YAPJ,aAOI,UAAA,YAPJ,eAOI,YAAA,YAPJ,eAOI,YAAA,YAPJ,WAOI,UAAA,eAPJ,aAOI,UAAA,iBAPJ,mBAOI,UAAA,uBAPJ,uBAOI,gBAAA,qBAPJ,qBAOI,gBAAA,mBAPJ,wBAOI,gBAAA,iBAPJ,yBAOI,gBAAA,wBAPJ,wBAOI,gBAAA,uBAPJ,wBAOI,gBAAA,uBAPJ,mBAOI,YAAA,qBAPJ,iBAOI,YAAA,mBAPJ,oBAOI,YAAA,iBAPJ,sBAOI,YAAA,mBAPJ,qBAOI,YAAA,kBAPJ,qBAOI,cAAA,qBAPJ,mBAOI,cAAA,mBAPJ,sBAOI,cAAA,iBAPJ,uBAOI,cAAA,wBAPJ,sBAOI,cAAA,uBAPJ,uBAOI,cAAA,kBAPJ,iBAOI,WAAA,eAPJ,kBAOI,WAAA,qBAPJ,gBAOI,WAAA,mBAPJ,mBAOI,WAAA,iBAPJ,qBAOI,WAAA,mBAPJ,oBAOI,WAAA,kBAPJ,aAOI,MAAA,aAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,SAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,KAOI,OAAA,YAPJ,KAOI,OAAA,iBAPJ,KAOI,OAAA,gBAPJ,KAOI,OAAA,eAPJ,KAOI,OAAA,iBAPJ,KAOI,OAAA,eAPJ,QAOI,OAAA,eAPJ,MAOI,YAAA,YAAA,aAAA,YAPJ,MAOI,YAAA,iBAAA,aAAA,iBAPJ,MAOI,YAAA,gBAAA,aAAA,gBAPJ,MAOI,YAAA,eAAA,aAAA,eAPJ,MAOI,YAAA,iBAAA,aAAA,iBAPJ,MAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,MAOI,WAAA,YAAA,cAAA,YAPJ,MAOI,WAAA,iBAAA,cAAA,iBAPJ,MAOI,WAAA,gBAAA,cAAA,gBAPJ,MAOI,WAAA,eAAA,cAAA,eAPJ,MAOI,WAAA,iBAAA,cAAA,iBAPJ,MAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,MAOI,WAAA,YAPJ,MAOI,WAAA,iBAPJ,MAOI,WAAA,gBAPJ,MAOI,WAAA,eAPJ,MAOI,WAAA,iBAPJ,MAOI,WAAA,eAPJ,SAOI,WAAA,eAPJ,MAOI,YAAA,YAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,gBAPJ,MAOI,YAAA,eAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,eAPJ,SAOI,YAAA,eAPJ,MAOI,cAAA,YAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,gBAPJ,MAOI,cAAA,eAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,eAPJ,SAOI,cAAA,eAPJ,MAOI,aAAA,YAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,gBAPJ,MAOI,aAAA,eAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,eAPJ,SAOI,aAAA,eAPJ,KAOI,QAAA,YAPJ,KAOI,QAAA,iBAPJ,KAOI,QAAA,gBAPJ,KAOI,QAAA,eAPJ,KAOI,QAAA,iBAPJ,KAOI,QAAA,eAPJ,MAOI,aAAA,YAAA,cAAA,YAPJ,MAOI,aAAA,iBAAA,cAAA,iBAPJ,MAOI,aAAA,gBAAA,cAAA,gBAPJ,MAOI,aAAA,eAAA,cAAA,eAPJ,MAOI,aAAA,iBAAA,cAAA,iBAPJ,MAOI,aAAA,eAAA,cAAA,eAPJ,MAOI,YAAA,YAAA,eAAA,YAPJ,MAOI,YAAA,iBAAA,eAAA,iBAPJ,MAOI,YAAA,gBAAA,eAAA,gBAPJ,MAOI,YAAA,eAAA,eAAA,eAPJ,MAOI,YAAA,iBAAA,eAAA,iBAPJ,MAOI,YAAA,eAAA,eAAA,eAPJ,MAOI,YAAA,YAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,gBAPJ,MAOI,YAAA,eAPJ,MAOI,YAAA,iBAPJ,MAOI,YAAA,eAPJ,MAOI,aAAA,YAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,gBAPJ,MAOI,aAAA,eAPJ,MAOI,aAAA,iBAPJ,MAOI,aAAA,eAPJ,MAOI,eAAA,YAPJ,MAOI,eAAA,iBAPJ,MAOI,eAAA,gBAPJ,MAOI,eAAA,eAPJ,MAOI,eAAA,iBAPJ,MAOI,eAAA,eAPJ,MAOI,cAAA,YAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,gBAPJ,MAOI,cAAA,eAPJ,MAOI,cAAA,iBAPJ,MAOI,cAAA,eHVR,yBGGI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,YAAA,YAAA,aAAA,YAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,gBAAA,aAAA,gBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,YAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,aAAA,YAAA,cAAA,YAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,gBAAA,cAAA,gBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBHVR,yBGGI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,YAAA,YAAA,aAAA,YAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,gBAAA,aAAA,gBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,YAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,aAAA,YAAA,cAAA,YAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,gBAAA,cAAA,gBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBHVR,yBGGI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,YAAA,YAAA,aAAA,YAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,gBAAA,aAAA,gBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,YAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,aAAA,YAAA,cAAA,YAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,gBAAA,cAAA,gBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBHVR,0BGGI,aAOI,QAAA,iBAPJ,mBAOI,QAAA,uBAPJ,YAOI,QAAA,gBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,YAOI,QAAA,gBAPJ,gBAOI,QAAA,oBAPJ,iBAOI,QAAA,qBAPJ,WAOI,QAAA,eAPJ,kBAOI,QAAA,sBAPJ,WAOI,QAAA,eAPJ,cAOI,KAAA,EAAA,EAAA,eAPJ,aAOI,eAAA,cAPJ,gBAOI,eAAA,iBAPJ,qBAOI,eAAA,sBAPJ,wBAOI,eAAA,yBAPJ,gBAOI,UAAA,YAPJ,gBAOI,UAAA,YAPJ,kBAOI,YAAA,YAPJ,kBAOI,YAAA,YAPJ,cAOI,UAAA,eAPJ,gBAOI,UAAA,iBAPJ,sBAOI,UAAA,uBAPJ,0BAOI,gBAAA,qBAPJ,wBAOI,gBAAA,mBAPJ,2BAOI,gBAAA,iBAPJ,4BAOI,gBAAA,wBAPJ,2BAOI,gBAAA,uBAPJ,2BAOI,gBAAA,uBAPJ,sBAOI,YAAA,qBAPJ,oBAOI,YAAA,mBAPJ,uBAOI,YAAA,iBAPJ,yBAOI,YAAA,mBAPJ,wBAOI,YAAA,kBAPJ,wBAOI,cAAA,qBAPJ,sBAOI,cAAA,mBAPJ,yBAOI,cAAA,iBAPJ,0BAOI,cAAA,wBAPJ,yBAOI,cAAA,uBAPJ,0BAOI,cAAA,kBAPJ,oBAOI,WAAA,eAPJ,qBAOI,WAAA,qBAPJ,mBAOI,WAAA,mBAPJ,sBAOI,WAAA,iBAPJ,wBAOI,WAAA,mBAPJ,uBAOI,WAAA,kBAPJ,gBAOI,MAAA,aAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,YAOI,MAAA,YAPJ,eAOI,MAAA,YAPJ,QAOI,OAAA,YAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,gBAPJ,QAOI,OAAA,eAPJ,QAOI,OAAA,iBAPJ,QAOI,OAAA,eAPJ,WAOI,OAAA,eAPJ,SAOI,YAAA,YAAA,aAAA,YAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,gBAAA,aAAA,gBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,YAAA,iBAAA,aAAA,iBAPJ,SAOI,YAAA,eAAA,aAAA,eAPJ,YAOI,YAAA,eAAA,aAAA,eAPJ,SAOI,WAAA,YAAA,cAAA,YAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,gBAAA,cAAA,gBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,iBAAA,cAAA,iBAPJ,SAOI,WAAA,eAAA,cAAA,eAPJ,YAOI,WAAA,eAAA,cAAA,eAPJ,SAOI,WAAA,YAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,gBAPJ,SAOI,WAAA,eAPJ,SAOI,WAAA,iBAPJ,SAOI,WAAA,eAPJ,YAOI,WAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,YAOI,YAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,eAPJ,YAOI,cAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,YAOI,aAAA,eAPJ,QAOI,QAAA,YAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,gBAPJ,QAOI,QAAA,eAPJ,QAOI,QAAA,iBAPJ,QAOI,QAAA,eAPJ,SAOI,aAAA,YAAA,cAAA,YAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,gBAAA,cAAA,gBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,aAAA,iBAAA,cAAA,iBAPJ,SAOI,aAAA,eAAA,cAAA,eAPJ,SAOI,YAAA,YAAA,eAAA,YAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,gBAAA,eAAA,gBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,iBAAA,eAAA,iBAPJ,SAOI,YAAA,eAAA,eAAA,eAPJ,SAOI,YAAA,YAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,gBAPJ,SAOI,YAAA,eAPJ,SAOI,YAAA,iBAPJ,SAOI,YAAA,eAPJ,SAOI,aAAA,YAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,gBAPJ,SAOI,aAAA,eAPJ,SAOI,aAAA,iBAPJ,SAOI,aAAA,eAPJ,SAOI,eAAA,YAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,gBAPJ,SAOI,eAAA,eAPJ,SAOI,eAAA,iBAPJ,SAOI,eAAA,eAPJ,SAOI,cAAA,YAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBAPJ,SAOI,cAAA,eAPJ,SAOI,cAAA,iBAPJ,SAOI,cAAA,gBHVR,0BGGI,cAOI,QAAA,iBAPJ,oBAOI,QAAA,uBAPJ,aAOI,QAAA,gBAPJ,YAOI,QAAA,eAPJ,mBAOI,QAAA,sBAPJ,aAOI,QAAA,gBAPJ,iBAOI,QAAA,oBAPJ,kBAOI,QAAA,qBAPJ,YAOI,QAAA,eAPJ,mBAOI,QAAA,sBAPJ,YAOI,QAAA,eAPJ,eAOI,KAAA,EAAA,EAAA,eAPJ,cAOI,eAAA,cAPJ,iBAOI,eAAA,iBAPJ,sBAOI,eAAA,sBAPJ,yBAOI,eAAA,yBAPJ,iBAOI,UAAA,YAPJ,iBAOI,UAAA,YAPJ,mBAOI,YAAA,YAPJ,mBAOI,YAAA,YAPJ,eAOI,UAAA,eAPJ,iBAOI,UAAA,iBAPJ,uBAOI,UAAA,uBAPJ,2BAOI,gBAAA,qBAPJ,yBAOI,gBAAA,mBAPJ,4BAOI,gBAAA,iBAPJ,6BAOI,gBAAA,wBAPJ,4BAOI,gBAAA,uBAPJ,4BAOI,gBAAA,uBAPJ,uBAOI,YAAA,qBAPJ,qBAOI,YAAA,mBAPJ,wBAOI,YAAA,iBAPJ,0BAOI,YAAA,mBAPJ,yBAOI,YAAA,kBAPJ,yBAOI,cAAA,qBAPJ,uBAOI,cAAA,mBAPJ,0BAOI,cAAA,iBAPJ,2BAOI,cAAA,wBAPJ,0BAOI,cAAA,uBAPJ,2BAOI,cAAA,kBAPJ,qBAOI,WAAA,eAPJ,sBAOI,WAAA,qBAPJ,oBAOI,WAAA,mBAPJ,uBAOI,WAAA,iBAPJ,yBAOI,WAAA,mBAPJ,wBAOI,WAAA,kBAPJ,iBAOI,MAAA,aAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,aAOI,MAAA,YAPJ,gBAOI,MAAA,YAPJ,SAOI,OAAA,YAPJ,SAOI,OAAA,iBAPJ,SAOI,OAAA,gBAPJ,SAOI,OAAA,eAPJ,SAOI,OAAA,iBAPJ,SAOI,OAAA,eAPJ,YAOI,OAAA,eAPJ,UAOI,YAAA,YAAA,aAAA,YAPJ,UAOI,YAAA,iBAAA,aAAA,iBAPJ,UAOI,YAAA,gBAAA,aAAA,gBAPJ,UAOI,YAAA,eAAA,aAAA,eAPJ,UAOI,YAAA,iBAAA,aAAA,iBAPJ,UAOI,YAAA,eAAA,aAAA,eAPJ,aAOI,YAAA,eAAA,aAAA,eAPJ,UAOI,WAAA,YAAA,cAAA,YAPJ,UAOI,WAAA,iBAAA,cAAA,iBAPJ,UAOI,WAAA,gBAAA,cAAA,gBAPJ,UAOI,WAAA,eAAA,cAAA,eAPJ,UAOI,WAAA,iBAAA,cAAA,iBAPJ,UAOI,WAAA,eAAA,cAAA,eAPJ,aAOI,WAAA,eAAA,cAAA,eAPJ,UAOI,WAAA,YAPJ,UAOI,WAAA,iBAPJ,UAOI,WAAA,gBAPJ,UAOI,WAAA,eAPJ,UAOI,WAAA,iBAPJ,UAOI,WAAA,eAPJ,aAOI,WAAA,eAPJ,UAOI,YAAA,YAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,gBAPJ,UAOI,YAAA,eAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,eAPJ,aAOI,YAAA,eAPJ,UAOI,cAAA,YAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBAPJ,UAOI,cAAA,eAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,eAPJ,aAOI,cAAA,eAPJ,UAOI,aAAA,YAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,gBAPJ,UAOI,aAAA,eAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,eAPJ,aAOI,aAAA,eAPJ,SAOI,QAAA,YAPJ,SAOI,QAAA,iBAPJ,SAOI,QAAA,gBAPJ,SAOI,QAAA,eAPJ,SAOI,QAAA,iBAPJ,SAOI,QAAA,eAPJ,UAOI,aAAA,YAAA,cAAA,YAPJ,UAOI,aAAA,iBAAA,cAAA,iBAPJ,UAOI,aAAA,gBAAA,cAAA,gBAPJ,UAOI,aAAA,eAAA,cAAA,eAPJ,UAOI,aAAA,iBAAA,cAAA,iBAPJ,UAOI,aAAA,eAAA,cAAA,eAPJ,UAOI,YAAA,YAAA,eAAA,YAPJ,UAOI,YAAA,iBAAA,eAAA,iBAPJ,UAOI,YAAA,gBAAA,eAAA,gBAPJ,UAOI,YAAA,eAAA,eAAA,eAPJ,UAOI,YAAA,iBAAA,eAAA,iBAPJ,UAOI,YAAA,eAAA,eAAA,eAPJ,UAOI,YAAA,YAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,gBAPJ,UAOI,YAAA,eAPJ,UAOI,YAAA,iBAPJ,UAOI,YAAA,eAPJ,UAOI,aAAA,YAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,gBAPJ,UAOI,aAAA,eAPJ,UAOI,aAAA,iBAPJ,UAOI,aAAA,eAPJ,UAOI,eAAA,YAPJ,UAOI,eAAA,iBAPJ,UAOI,eAAA,gBAPJ,UAOI,eAAA,eAPJ,UAOI,eAAA,iBAPJ,UAOI,eAAA,eAPJ,UAOI,cAAA,YAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBAPJ,UAOI,cAAA,eAPJ,UAOI,cAAA,iBAPJ,UAOI,cAAA,gBCnCZ,aD4BQ,gBAOI,QAAA,iBAPJ,sBAOI,QAAA,uBAPJ,eAOI,QAAA,gBAPJ,cAOI,QAAA,eAPJ,qBAOI,QAAA,sBAPJ,eAOI,QAAA,gBAPJ,mBAOI,QAAA,oBAPJ,oBAOI,QAAA,qBAPJ,cAOI,QAAA,eAPJ,qBAOI,QAAA,sBAPJ,cAOI,QAAA","sourcesContent":["@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-container-classes {\n // Single container class with breakpoint max-widths\n .container,\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n","/*!\n * Bootstrap Grid v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n width: 100%;\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n margin-left: auto;\n margin-right: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n:root {\n --bs-breakpoint-xs: 0;\n --bs-breakpoint-sm: 576px;\n --bs-breakpoint-md: 768px;\n --bs-breakpoint-lg: 992px;\n --bs-breakpoint-xl: 1200px;\n --bs-breakpoint-xxl: 1400px;\n}\n\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(-1 * var(--bs-gutter-y));\n margin-left: calc(-0.5 * var(--bs-gutter-x));\n margin-right: calc(-0.5 * var(--bs-gutter-x));\n}\n.row > * {\n box-sizing: border-box;\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-right: 8.33333333%;\n}\n\n.offset-2 {\n margin-right: 16.66666667%;\n}\n\n.offset-3 {\n margin-right: 25%;\n}\n\n.offset-4 {\n margin-right: 33.33333333%;\n}\n\n.offset-5 {\n margin-right: 41.66666667%;\n}\n\n.offset-6 {\n margin-right: 50%;\n}\n\n.offset-7 {\n margin-right: 58.33333333%;\n}\n\n.offset-8 {\n margin-right: 66.66666667%;\n}\n\n.offset-9 {\n margin-right: 75%;\n}\n\n.offset-10 {\n margin-right: 83.33333333%;\n}\n\n.offset-11 {\n margin-right: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0;\n }\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-sm-0 {\n margin-right: 0;\n }\n .offset-sm-1 {\n margin-right: 8.33333333%;\n }\n .offset-sm-2 {\n margin-right: 16.66666667%;\n }\n .offset-sm-3 {\n margin-right: 25%;\n }\n .offset-sm-4 {\n margin-right: 33.33333333%;\n }\n .offset-sm-5 {\n margin-right: 41.66666667%;\n }\n .offset-sm-6 {\n margin-right: 50%;\n }\n .offset-sm-7 {\n margin-right: 58.33333333%;\n }\n .offset-sm-8 {\n margin-right: 66.66666667%;\n }\n .offset-sm-9 {\n margin-right: 75%;\n }\n .offset-sm-10 {\n margin-right: 83.33333333%;\n }\n .offset-sm-11 {\n margin-right: 91.66666667%;\n }\n .g-sm-0,\n .gx-sm-0 {\n --bs-gutter-x: 0;\n }\n .g-sm-0,\n .gy-sm-0 {\n --bs-gutter-y: 0;\n }\n .g-sm-1,\n .gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-sm-1,\n .gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-sm-2,\n .gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-sm-2,\n .gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-sm-3,\n .gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n .g-sm-3,\n .gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n .g-sm-4,\n .gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-sm-4,\n .gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-sm-5,\n .gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n .g-sm-5,\n .gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0;\n }\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-md-0 {\n margin-right: 0;\n }\n .offset-md-1 {\n margin-right: 8.33333333%;\n }\n .offset-md-2 {\n margin-right: 16.66666667%;\n }\n .offset-md-3 {\n margin-right: 25%;\n }\n .offset-md-4 {\n margin-right: 33.33333333%;\n }\n .offset-md-5 {\n margin-right: 41.66666667%;\n }\n .offset-md-6 {\n margin-right: 50%;\n }\n .offset-md-7 {\n margin-right: 58.33333333%;\n }\n .offset-md-8 {\n margin-right: 66.66666667%;\n }\n .offset-md-9 {\n margin-right: 75%;\n }\n .offset-md-10 {\n margin-right: 83.33333333%;\n }\n .offset-md-11 {\n margin-right: 91.66666667%;\n }\n .g-md-0,\n .gx-md-0 {\n --bs-gutter-x: 0;\n }\n .g-md-0,\n .gy-md-0 {\n --bs-gutter-y: 0;\n }\n .g-md-1,\n .gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-md-1,\n .gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-md-2,\n .gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-md-2,\n .gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-md-3,\n .gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n .g-md-3,\n .gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n .g-md-4,\n .gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-md-4,\n .gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-md-5,\n .gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n .g-md-5,\n .gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0;\n }\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-lg-0 {\n margin-right: 0;\n }\n .offset-lg-1 {\n margin-right: 8.33333333%;\n }\n .offset-lg-2 {\n margin-right: 16.66666667%;\n }\n .offset-lg-3 {\n margin-right: 25%;\n }\n .offset-lg-4 {\n margin-right: 33.33333333%;\n }\n .offset-lg-5 {\n margin-right: 41.66666667%;\n }\n .offset-lg-6 {\n margin-right: 50%;\n }\n .offset-lg-7 {\n margin-right: 58.33333333%;\n }\n .offset-lg-8 {\n margin-right: 66.66666667%;\n }\n .offset-lg-9 {\n margin-right: 75%;\n }\n .offset-lg-10 {\n margin-right: 83.33333333%;\n }\n .offset-lg-11 {\n margin-right: 91.66666667%;\n }\n .g-lg-0,\n .gx-lg-0 {\n --bs-gutter-x: 0;\n }\n .g-lg-0,\n .gy-lg-0 {\n --bs-gutter-y: 0;\n }\n .g-lg-1,\n .gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-lg-1,\n .gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-lg-2,\n .gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-lg-2,\n .gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-lg-3,\n .gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n .g-lg-3,\n .gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n .g-lg-4,\n .gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-lg-4,\n .gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-lg-5,\n .gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n .g-lg-5,\n .gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0;\n }\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xl-0 {\n margin-right: 0;\n }\n .offset-xl-1 {\n margin-right: 8.33333333%;\n }\n .offset-xl-2 {\n margin-right: 16.66666667%;\n }\n .offset-xl-3 {\n margin-right: 25%;\n }\n .offset-xl-4 {\n margin-right: 33.33333333%;\n }\n .offset-xl-5 {\n margin-right: 41.66666667%;\n }\n .offset-xl-6 {\n margin-right: 50%;\n }\n .offset-xl-7 {\n margin-right: 58.33333333%;\n }\n .offset-xl-8 {\n margin-right: 66.66666667%;\n }\n .offset-xl-9 {\n margin-right: 75%;\n }\n .offset-xl-10 {\n margin-right: 83.33333333%;\n }\n .offset-xl-11 {\n margin-right: 91.66666667%;\n }\n .g-xl-0,\n .gx-xl-0 {\n --bs-gutter-x: 0;\n }\n .g-xl-0,\n .gy-xl-0 {\n --bs-gutter-y: 0;\n }\n .g-xl-1,\n .gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xl-1,\n .gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xl-2,\n .gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xl-2,\n .gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xl-3,\n .gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xl-3,\n .gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xl-4,\n .gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xl-4,\n .gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xl-5,\n .gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xl-5,\n .gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0;\n }\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xxl-0 {\n margin-right: 0;\n }\n .offset-xxl-1 {\n margin-right: 8.33333333%;\n }\n .offset-xxl-2 {\n margin-right: 16.66666667%;\n }\n .offset-xxl-3 {\n margin-right: 25%;\n }\n .offset-xxl-4 {\n margin-right: 33.33333333%;\n }\n .offset-xxl-5 {\n margin-right: 41.66666667%;\n }\n .offset-xxl-6 {\n margin-right: 50%;\n }\n .offset-xxl-7 {\n margin-right: 58.33333333%;\n }\n .offset-xxl-8 {\n margin-right: 66.66666667%;\n }\n .offset-xxl-9 {\n margin-right: 75%;\n }\n .offset-xxl-10 {\n margin-right: 83.33333333%;\n }\n .offset-xxl-11 {\n margin-right: 91.66666667%;\n }\n .g-xxl-0,\n .gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n .g-xxl-0,\n .gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n .g-xxl-1,\n .gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xxl-1,\n .gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xxl-2,\n .gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xxl-2,\n .gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xxl-3,\n .gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xxl-3,\n .gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xxl-4,\n .gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xxl-4,\n .gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xxl-5,\n .gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xxl-5,\n .gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-inline-grid {\n display: inline-grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n\n.mx-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n}\n\n.mx-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n}\n\n.mx-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n}\n\n.mx-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n}\n\n.mx-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n}\n\n.mx-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-left: 0 !important;\n}\n\n.me-1 {\n margin-left: 0.25rem !important;\n}\n\n.me-2 {\n margin-left: 0.5rem !important;\n}\n\n.me-3 {\n margin-left: 1rem !important;\n}\n\n.me-4 {\n margin-left: 1.5rem !important;\n}\n\n.me-5 {\n margin-left: 3rem !important;\n}\n\n.me-auto {\n margin-left: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-right: 0 !important;\n}\n\n.ms-1 {\n margin-right: 0.25rem !important;\n}\n\n.ms-2 {\n margin-right: 0.5rem !important;\n}\n\n.ms-3 {\n margin-right: 1rem !important;\n}\n\n.ms-4 {\n margin-right: 1.5rem !important;\n}\n\n.ms-5 {\n margin-right: 3rem !important;\n}\n\n.ms-auto {\n margin-right: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n}\n\n.px-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n}\n\n.px-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n}\n\n.px-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n}\n\n.px-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n}\n\n.px-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-left: 0 !important;\n}\n\n.pe-1 {\n padding-left: 0.25rem !important;\n}\n\n.pe-2 {\n padding-left: 0.5rem !important;\n}\n\n.pe-3 {\n padding-left: 1rem !important;\n}\n\n.pe-4 {\n padding-left: 1.5rem !important;\n}\n\n.pe-5 {\n padding-left: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-right: 0 !important;\n}\n\n.ps-1 {\n padding-right: 0.25rem !important;\n}\n\n.ps-2 {\n padding-right: 0.5rem !important;\n}\n\n.ps-3 {\n padding-right: 1rem !important;\n}\n\n.ps-4 {\n padding-right: 1.5rem !important;\n}\n\n.ps-5 {\n padding-right: 3rem !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-grid {\n display: grid !important;\n }\n .d-sm-inline-grid {\n display: inline-grid !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n .d-sm-none {\n display: none !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n .order-sm-first {\n order: -1 !important;\n }\n .order-sm-0 {\n order: 0 !important;\n }\n .order-sm-1 {\n order: 1 !important;\n }\n .order-sm-2 {\n order: 2 !important;\n }\n .order-sm-3 {\n order: 3 !important;\n }\n .order-sm-4 {\n order: 4 !important;\n }\n .order-sm-5 {\n order: 5 !important;\n }\n .order-sm-last {\n order: 6 !important;\n }\n .m-sm-0 {\n margin: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mx-sm-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n .mx-sm-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n .mx-sm-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n .mx-sm-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n .mt-sm-auto {\n margin-top: auto !important;\n }\n .me-sm-0 {\n margin-left: 0 !important;\n }\n .me-sm-1 {\n margin-left: 0.25rem !important;\n }\n .me-sm-2 {\n margin-left: 0.5rem !important;\n }\n .me-sm-3 {\n margin-left: 1rem !important;\n }\n .me-sm-4 {\n margin-left: 1.5rem !important;\n }\n .me-sm-5 {\n margin-left: 3rem !important;\n }\n .me-sm-auto {\n margin-left: auto !important;\n }\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n .ms-sm-0 {\n margin-right: 0 !important;\n }\n .ms-sm-1 {\n margin-right: 0.25rem !important;\n }\n .ms-sm-2 {\n margin-right: 0.5rem !important;\n }\n .ms-sm-3 {\n margin-right: 1rem !important;\n }\n .ms-sm-4 {\n margin-right: 1.5rem !important;\n }\n .ms-sm-5 {\n margin-right: 3rem !important;\n }\n .ms-sm-auto {\n margin-right: auto !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .px-sm-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n .px-sm-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n .px-sm-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n .px-sm-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n .px-sm-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n .px-sm-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n .pe-sm-0 {\n padding-left: 0 !important;\n }\n .pe-sm-1 {\n padding-left: 0.25rem !important;\n }\n .pe-sm-2 {\n padding-left: 0.5rem !important;\n }\n .pe-sm-3 {\n padding-left: 1rem !important;\n }\n .pe-sm-4 {\n padding-left: 1.5rem !important;\n }\n .pe-sm-5 {\n padding-left: 3rem !important;\n }\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n .ps-sm-0 {\n padding-right: 0 !important;\n }\n .ps-sm-1 {\n padding-right: 0.25rem !important;\n }\n .ps-sm-2 {\n padding-right: 0.5rem !important;\n }\n .ps-sm-3 {\n padding-right: 1rem !important;\n }\n .ps-sm-4 {\n padding-right: 1.5rem !important;\n }\n .ps-sm-5 {\n padding-right: 3rem !important;\n }\n}\n@media (min-width: 768px) {\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-grid {\n display: grid !important;\n }\n .d-md-inline-grid {\n display: inline-grid !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n .d-md-none {\n display: none !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n .order-md-first {\n order: -1 !important;\n }\n .order-md-0 {\n order: 0 !important;\n }\n .order-md-1 {\n order: 1 !important;\n }\n .order-md-2 {\n order: 2 !important;\n }\n .order-md-3 {\n order: 3 !important;\n }\n .order-md-4 {\n order: 4 !important;\n }\n .order-md-5 {\n order: 5 !important;\n }\n .order-md-last {\n order: 6 !important;\n }\n .m-md-0 {\n margin: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mx-md-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n .mx-md-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n .mx-md-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n .mx-md-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n .mx-md-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n .mx-md-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n .mx-md-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-md-0 {\n margin-top: 0 !important;\n }\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n .mt-md-auto {\n margin-top: auto !important;\n }\n .me-md-0 {\n margin-left: 0 !important;\n }\n .me-md-1 {\n margin-left: 0.25rem !important;\n }\n .me-md-2 {\n margin-left: 0.5rem !important;\n }\n .me-md-3 {\n margin-left: 1rem !important;\n }\n .me-md-4 {\n margin-left: 1.5rem !important;\n }\n .me-md-5 {\n margin-left: 3rem !important;\n }\n .me-md-auto {\n margin-left: auto !important;\n }\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n .ms-md-0 {\n margin-right: 0 !important;\n }\n .ms-md-1 {\n margin-right: 0.25rem !important;\n }\n .ms-md-2 {\n margin-right: 0.5rem !important;\n }\n .ms-md-3 {\n margin-right: 1rem !important;\n }\n .ms-md-4 {\n margin-right: 1.5rem !important;\n }\n .ms-md-5 {\n margin-right: 3rem !important;\n }\n .ms-md-auto {\n margin-right: auto !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .px-md-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n .px-md-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n .px-md-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n .px-md-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n .px-md-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n .px-md-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-md-0 {\n padding-top: 0 !important;\n }\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n .pe-md-0 {\n padding-left: 0 !important;\n }\n .pe-md-1 {\n padding-left: 0.25rem !important;\n }\n .pe-md-2 {\n padding-left: 0.5rem !important;\n }\n .pe-md-3 {\n padding-left: 1rem !important;\n }\n .pe-md-4 {\n padding-left: 1.5rem !important;\n }\n .pe-md-5 {\n padding-left: 3rem !important;\n }\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n .ps-md-0 {\n padding-right: 0 !important;\n }\n .ps-md-1 {\n padding-right: 0.25rem !important;\n }\n .ps-md-2 {\n padding-right: 0.5rem !important;\n }\n .ps-md-3 {\n padding-right: 1rem !important;\n }\n .ps-md-4 {\n padding-right: 1.5rem !important;\n }\n .ps-md-5 {\n padding-right: 3rem !important;\n }\n}\n@media (min-width: 992px) {\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-grid {\n display: grid !important;\n }\n .d-lg-inline-grid {\n display: inline-grid !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n .d-lg-none {\n display: none !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n .order-lg-first {\n order: -1 !important;\n }\n .order-lg-0 {\n order: 0 !important;\n }\n .order-lg-1 {\n order: 1 !important;\n }\n .order-lg-2 {\n order: 2 !important;\n }\n .order-lg-3 {\n order: 3 !important;\n }\n .order-lg-4 {\n order: 4 !important;\n }\n .order-lg-5 {\n order: 5 !important;\n }\n .order-lg-last {\n order: 6 !important;\n }\n .m-lg-0 {\n margin: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mx-lg-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n .mx-lg-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n .mx-lg-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n .mx-lg-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n .mt-lg-auto {\n margin-top: auto !important;\n }\n .me-lg-0 {\n margin-left: 0 !important;\n }\n .me-lg-1 {\n margin-left: 0.25rem !important;\n }\n .me-lg-2 {\n margin-left: 0.5rem !important;\n }\n .me-lg-3 {\n margin-left: 1rem !important;\n }\n .me-lg-4 {\n margin-left: 1.5rem !important;\n }\n .me-lg-5 {\n margin-left: 3rem !important;\n }\n .me-lg-auto {\n margin-left: auto !important;\n }\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n .ms-lg-0 {\n margin-right: 0 !important;\n }\n .ms-lg-1 {\n margin-right: 0.25rem !important;\n }\n .ms-lg-2 {\n margin-right: 0.5rem !important;\n }\n .ms-lg-3 {\n margin-right: 1rem !important;\n }\n .ms-lg-4 {\n margin-right: 1.5rem !important;\n }\n .ms-lg-5 {\n margin-right: 3rem !important;\n }\n .ms-lg-auto {\n margin-right: auto !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .px-lg-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n .px-lg-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n .px-lg-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n .px-lg-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n .px-lg-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n .px-lg-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n .pe-lg-0 {\n padding-left: 0 !important;\n }\n .pe-lg-1 {\n padding-left: 0.25rem !important;\n }\n .pe-lg-2 {\n padding-left: 0.5rem !important;\n }\n .pe-lg-3 {\n padding-left: 1rem !important;\n }\n .pe-lg-4 {\n padding-left: 1.5rem !important;\n }\n .pe-lg-5 {\n padding-left: 3rem !important;\n }\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n .ps-lg-0 {\n padding-right: 0 !important;\n }\n .ps-lg-1 {\n padding-right: 0.25rem !important;\n }\n .ps-lg-2 {\n padding-right: 0.5rem !important;\n }\n .ps-lg-3 {\n padding-right: 1rem !important;\n }\n .ps-lg-4 {\n padding-right: 1.5rem !important;\n }\n .ps-lg-5 {\n padding-right: 3rem !important;\n }\n}\n@media (min-width: 1200px) {\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-grid {\n display: grid !important;\n }\n .d-xl-inline-grid {\n display: inline-grid !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n .d-xl-none {\n display: none !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n .order-xl-first {\n order: -1 !important;\n }\n .order-xl-0 {\n order: 0 !important;\n }\n .order-xl-1 {\n order: 1 !important;\n }\n .order-xl-2 {\n order: 2 !important;\n }\n .order-xl-3 {\n order: 3 !important;\n }\n .order-xl-4 {\n order: 4 !important;\n }\n .order-xl-5 {\n order: 5 !important;\n }\n .order-xl-last {\n order: 6 !important;\n }\n .m-xl-0 {\n margin: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mx-xl-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n .mx-xl-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n .mx-xl-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n .mx-xl-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n .mt-xl-auto {\n margin-top: auto !important;\n }\n .me-xl-0 {\n margin-left: 0 !important;\n }\n .me-xl-1 {\n margin-left: 0.25rem !important;\n }\n .me-xl-2 {\n margin-left: 0.5rem !important;\n }\n .me-xl-3 {\n margin-left: 1rem !important;\n }\n .me-xl-4 {\n margin-left: 1.5rem !important;\n }\n .me-xl-5 {\n margin-left: 3rem !important;\n }\n .me-xl-auto {\n margin-left: auto !important;\n }\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n .ms-xl-0 {\n margin-right: 0 !important;\n }\n .ms-xl-1 {\n margin-right: 0.25rem !important;\n }\n .ms-xl-2 {\n margin-right: 0.5rem !important;\n }\n .ms-xl-3 {\n margin-right: 1rem !important;\n }\n .ms-xl-4 {\n margin-right: 1.5rem !important;\n }\n .ms-xl-5 {\n margin-right: 3rem !important;\n }\n .ms-xl-auto {\n margin-right: auto !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .px-xl-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n .px-xl-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n .px-xl-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n .px-xl-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n .px-xl-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n .px-xl-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n .pe-xl-0 {\n padding-left: 0 !important;\n }\n .pe-xl-1 {\n padding-left: 0.25rem !important;\n }\n .pe-xl-2 {\n padding-left: 0.5rem !important;\n }\n .pe-xl-3 {\n padding-left: 1rem !important;\n }\n .pe-xl-4 {\n padding-left: 1.5rem !important;\n }\n .pe-xl-5 {\n padding-left: 3rem !important;\n }\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xl-0 {\n padding-right: 0 !important;\n }\n .ps-xl-1 {\n padding-right: 0.25rem !important;\n }\n .ps-xl-2 {\n padding-right: 0.5rem !important;\n }\n .ps-xl-3 {\n padding-right: 1rem !important;\n }\n .ps-xl-4 {\n padding-right: 1.5rem !important;\n }\n .ps-xl-5 {\n padding-right: 3rem !important;\n }\n}\n@media (min-width: 1400px) {\n .d-xxl-inline {\n display: inline !important;\n }\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n .d-xxl-block {\n display: block !important;\n }\n .d-xxl-grid {\n display: grid !important;\n }\n .d-xxl-inline-grid {\n display: inline-grid !important;\n }\n .d-xxl-table {\n display: table !important;\n }\n .d-xxl-table-row {\n display: table-row !important;\n }\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n .d-xxl-flex {\n display: flex !important;\n }\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n .d-xxl-none {\n display: none !important;\n }\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xxl-row {\n flex-direction: row !important;\n }\n .flex-xxl-column {\n flex-direction: column !important;\n }\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n .align-items-xxl-center {\n align-items: center !important;\n }\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n .align-content-xxl-center {\n align-content: center !important;\n }\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n .align-self-xxl-center {\n align-self: center !important;\n }\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n .order-xxl-first {\n order: -1 !important;\n }\n .order-xxl-0 {\n order: 0 !important;\n }\n .order-xxl-1 {\n order: 1 !important;\n }\n .order-xxl-2 {\n order: 2 !important;\n }\n .order-xxl-3 {\n order: 3 !important;\n }\n .order-xxl-4 {\n order: 4 !important;\n }\n .order-xxl-5 {\n order: 5 !important;\n }\n .order-xxl-last {\n order: 6 !important;\n }\n .m-xxl-0 {\n margin: 0 !important;\n }\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n .m-xxl-3 {\n margin: 1rem !important;\n }\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n .m-xxl-5 {\n margin: 3rem !important;\n }\n .m-xxl-auto {\n margin: auto !important;\n }\n .mx-xxl-0 {\n margin-left: 0 !important;\n margin-right: 0 !important;\n }\n .mx-xxl-1 {\n margin-left: 0.25rem !important;\n margin-right: 0.25rem !important;\n }\n .mx-xxl-2 {\n margin-left: 0.5rem !important;\n margin-right: 0.5rem !important;\n }\n .mx-xxl-3 {\n margin-left: 1rem !important;\n margin-right: 1rem !important;\n }\n .mx-xxl-4 {\n margin-left: 1.5rem !important;\n margin-right: 1.5rem !important;\n }\n .mx-xxl-5 {\n margin-left: 3rem !important;\n margin-right: 3rem !important;\n }\n .mx-xxl-auto {\n margin-left: auto !important;\n margin-right: auto !important;\n }\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n .me-xxl-0 {\n margin-left: 0 !important;\n }\n .me-xxl-1 {\n margin-left: 0.25rem !important;\n }\n .me-xxl-2 {\n margin-left: 0.5rem !important;\n }\n .me-xxl-3 {\n margin-left: 1rem !important;\n }\n .me-xxl-4 {\n margin-left: 1.5rem !important;\n }\n .me-xxl-5 {\n margin-left: 3rem !important;\n }\n .me-xxl-auto {\n margin-left: auto !important;\n }\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n .ms-xxl-0 {\n margin-right: 0 !important;\n }\n .ms-xxl-1 {\n margin-right: 0.25rem !important;\n }\n .ms-xxl-2 {\n margin-right: 0.5rem !important;\n }\n .ms-xxl-3 {\n margin-right: 1rem !important;\n }\n .ms-xxl-4 {\n margin-right: 1.5rem !important;\n }\n .ms-xxl-5 {\n margin-right: 3rem !important;\n }\n .ms-xxl-auto {\n margin-right: auto !important;\n }\n .p-xxl-0 {\n padding: 0 !important;\n }\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n .p-xxl-3 {\n padding: 1rem !important;\n }\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n .p-xxl-5 {\n padding: 3rem !important;\n }\n .px-xxl-0 {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n .px-xxl-1 {\n padding-left: 0.25rem !important;\n padding-right: 0.25rem !important;\n }\n .px-xxl-2 {\n padding-left: 0.5rem !important;\n padding-right: 0.5rem !important;\n }\n .px-xxl-3 {\n padding-left: 1rem !important;\n padding-right: 1rem !important;\n }\n .px-xxl-4 {\n padding-left: 1.5rem !important;\n padding-right: 1.5rem !important;\n }\n .px-xxl-5 {\n padding-left: 3rem !important;\n padding-right: 3rem !important;\n }\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n .pe-xxl-0 {\n padding-left: 0 !important;\n }\n .pe-xxl-1 {\n padding-left: 0.25rem !important;\n }\n .pe-xxl-2 {\n padding-left: 0.5rem !important;\n }\n .pe-xxl-3 {\n padding-left: 1rem !important;\n }\n .pe-xxl-4 {\n padding-left: 1.5rem !important;\n }\n .pe-xxl-5 {\n padding-left: 3rem !important;\n }\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xxl-0 {\n padding-right: 0 !important;\n }\n .ps-xxl-1 {\n padding-right: 0.25rem !important;\n }\n .ps-xxl-2 {\n padding-right: 0.5rem !important;\n }\n .ps-xxl-3 {\n padding-right: 1rem !important;\n }\n .ps-xxl-4 {\n padding-right: 1.5rem !important;\n }\n .ps-xxl-5 {\n padding-right: 3rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-grid {\n display: grid !important;\n }\n .d-print-inline-grid {\n display: inline-grid !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n .d-print-none {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap-grid.rtl.css.map */","// Container mixins\n\n@mixin make-container($gutter: $container-padding-x) {\n --#{$prefix}gutter-x: #{$gutter};\n --#{$prefix}gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-right: auto;\n margin-left: auto;\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl xxl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @if not $n {\n @error \"breakpoint `#{$name}` not found in `#{$breakpoints}`\";\n }\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width.\n// The maximum value is reduced by 0.02px to work around the limitations of\n// `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $max: map-get($breakpoints, $name);\n @return if($max and $max > 0, $max - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $next: breakpoint-next($name, $breakpoints);\n $max: breakpoint-max($next, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($next, $breakpoints) {\n @content;\n }\n }\n}\n","// Row\n//\n// Rows contain your columns.\n\n:root {\n @each $name, $value in $grid-breakpoints {\n --#{$prefix}breakpoint-#{$name}: #{$value};\n }\n}\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n\n > * {\n @include make-col-ready();\n }\n }\n}\n\n@if $enable-cssgrid {\n .grid {\n display: grid;\n grid-template-rows: repeat(var(--#{$prefix}rows, 1), 1fr);\n grid-template-columns: repeat(var(--#{$prefix}columns, #{$grid-columns}), 1fr);\n gap: var(--#{$prefix}gap, #{$grid-gutter-width});\n\n @include make-cssgrid();\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-row($gutter: $grid-gutter-width) {\n --#{$prefix}gutter-x: #{$gutter};\n --#{$prefix}gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n // TODO: Revisit calc order after https://github.com/react-bootstrap/react-bootstrap/issues/6039 is fixed\n margin-top: calc(-1 * var(--#{$prefix}gutter-y)); // stylelint-disable-line function-disallowed-list\n margin-right: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list\n margin-left: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list\n}\n\n@mixin make-col-ready() {\n // Add box sizing if only the grid is loaded\n box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null);\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we set the width\n // later on to override this initial width.\n flex-shrink: 0;\n width: 100%;\n max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid\n padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list\n margin-top: var(--#{$prefix}gutter-y);\n}\n\n@mixin make-col($size: false, $columns: $grid-columns) {\n @if $size {\n flex: 0 0 auto;\n width: percentage(divide($size, $columns));\n\n } @else {\n flex: 1 1 0;\n max-width: 100%;\n }\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: divide($size, $columns);\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// number of columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n > * {\n flex: 0 0 auto;\n width: percentage(divide(1, $count));\n }\n}\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex: 1 0 0;\n }\n\n .row-cols#{$infix}-auto > * {\n @include make-col-auto();\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n\n // Gutters\n //\n // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns.\n @each $key, $value in $gutters {\n .g#{$infix}-#{$key},\n .gx#{$infix}-#{$key} {\n --#{$prefix}gutter-x: #{$value};\n }\n\n .g#{$infix}-#{$key},\n .gy#{$infix}-#{$key} {\n --#{$prefix}gutter-y: #{$value};\n }\n }\n }\n }\n}\n\n@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) {\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .g-col#{$infix}-#{$i} {\n grid-column: auto / span $i;\n }\n }\n\n // Start with `1` because `0` is an invalid value.\n // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible.\n @for $i from 1 through ($columns - 1) {\n .g-start#{$infix}-#{$i} {\n grid-column-start: $i;\n }\n }\n }\n }\n }\n}\n","// Utility generator\n// Used to generate utilities & print utilities\n@mixin generate-utility($utility, $infix: \"\", $is-rfs-media-query: false) {\n $values: map-get($utility, values);\n\n // If the values are a list or string, convert it into a map\n @if type-of($values) == \"string\" or type-of(nth($values, 1)) != \"list\" {\n $values: zip($values, $values);\n }\n\n @each $key, $value in $values {\n $properties: map-get($utility, property);\n\n // Multiple properties are possible, for example with vertical or horizontal margins or paddings\n @if type-of($properties) == \"string\" {\n $properties: append((), $properties);\n }\n\n // Use custom class if present\n $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));\n $property-class: if($property-class == null, \"\", $property-class);\n\n // Use custom CSS variable name if present, otherwise default to `class`\n $css-variable-name: if(map-has-key($utility, css-variable-name), map-get($utility, css-variable-name), map-get($utility, class));\n\n // State params to generate pseudo-classes\n $state: if(map-has-key($utility, state), map-get($utility, state), ());\n\n $infix: if($property-class == \"\" and str-slice($infix, 1, 1) == \"-\", str-slice($infix, 2), $infix);\n\n // Don't prefix if value key is null (e.g. with shadow class)\n $property-class-modifier: if($key, if($property-class == \"\" and $infix == \"\", \"\", \"-\") + $key, \"\");\n\n @if map-get($utility, rfs) {\n // Inside the media query\n @if $is-rfs-media-query {\n $val: rfs-value($value);\n\n // Do not render anything if fluid and non fluid values are the same\n $value: if($val == rfs-fluid-value($value), null, $val);\n }\n @else {\n $value: rfs-fluid-value($value);\n }\n }\n\n $is-css-var: map-get($utility, css-var);\n $is-local-vars: map-get($utility, local-vars);\n $is-rtl: map-get($utility, rtl);\n\n @if $value != null {\n @if $is-rtl == false {\n /* rtl:begin:remove */\n }\n\n @if $is-css-var {\n .#{$property-class + $infix + $property-class-modifier} {\n --#{$prefix}#{$css-variable-name}: #{$value};\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n --#{$prefix}#{$css-variable-name}: #{$value};\n }\n }\n } @else {\n .#{$property-class + $infix + $property-class-modifier} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $variable in $is-local-vars {\n --#{$prefix}#{$local-var}: #{$variable};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n\n @each $pseudo in $state {\n .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} {\n @each $property in $properties {\n @if $is-local-vars {\n @each $local-var, $variable in $is-local-vars {\n --#{$prefix}#{$local-var}: #{$variable};\n }\n }\n #{$property}: $value if($enable-important-utilities, !important, null);\n }\n }\n }\n }\n\n @if $is-rtl == false {\n /* rtl:end:remove */\n }\n }\n }\n}\n","// Loop over each breakpoint\n@each $breakpoint in map-keys($grid-breakpoints) {\n\n // Generate media query if needed\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix);\n }\n }\n }\n}\n\n// RFS rescaling\n@media (min-width: $rfs-mq-value) {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) {\n // Loop over each utility property\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Only proceed if responsive media queries are enabled or if it's the base media query\n @if type-of($utility) == \"map\" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == \"\") {\n @include generate-utility($utility, $infix, true);\n }\n }\n }\n }\n}\n\n\n// Print utilities\n@media print {\n @each $key, $utility in $utilities {\n // The utility can be disabled with `false`, thus check if the utility is a map first\n // Then check if the utility needs print styles\n @if type-of($utility) == \"map\" and map-get($utility, print) == true {\n @include generate-utility($utility, \"-print\");\n }\n }\n}\n"]} \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css new file mode 100644 index 0000000..a95f4eb --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css @@ -0,0 +1,601 @@ +/*! + * Bootstrap Reboot v5.3.8 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +:root, +[data-bs-theme=light] { + --bs-blue: #0d6efd; + --bs-indigo: #6610f2; + --bs-purple: #6f42c1; + --bs-pink: #d63384; + --bs-red: #dc3545; + --bs-orange: #fd7e14; + --bs-yellow: #ffc107; + --bs-green: #198754; + --bs-teal: #20c997; + --bs-cyan: #0dcaf0; + --bs-black: #000; + --bs-white: #fff; + --bs-gray: #6c757d; + --bs-gray-dark: #343a40; + --bs-gray-100: #f8f9fa; + --bs-gray-200: #e9ecef; + --bs-gray-300: #dee2e6; + --bs-gray-400: #ced4da; + --bs-gray-500: #adb5bd; + --bs-gray-600: #6c757d; + --bs-gray-700: #495057; + --bs-gray-800: #343a40; + --bs-gray-900: #212529; + --bs-primary: #0d6efd; + --bs-secondary: #6c757d; + --bs-success: #198754; + --bs-info: #0dcaf0; + --bs-warning: #ffc107; + --bs-danger: #dc3545; + --bs-light: #f8f9fa; + --bs-dark: #212529; + --bs-primary-rgb: 13, 110, 253; + --bs-secondary-rgb: 108, 117, 125; + --bs-success-rgb: 25, 135, 84; + --bs-info-rgb: 13, 202, 240; + --bs-warning-rgb: 255, 193, 7; + --bs-danger-rgb: 220, 53, 69; + --bs-light-rgb: 248, 249, 250; + --bs-dark-rgb: 33, 37, 41; + --bs-primary-text-emphasis: #052c65; + --bs-secondary-text-emphasis: #2b2f32; + --bs-success-text-emphasis: #0a3622; + --bs-info-text-emphasis: #055160; + --bs-warning-text-emphasis: #664d03; + --bs-danger-text-emphasis: #58151c; + --bs-light-text-emphasis: #495057; + --bs-dark-text-emphasis: #495057; + --bs-primary-bg-subtle: #cfe2ff; + --bs-secondary-bg-subtle: #e2e3e5; + --bs-success-bg-subtle: #d1e7dd; + --bs-info-bg-subtle: #cff4fc; + --bs-warning-bg-subtle: #fff3cd; + --bs-danger-bg-subtle: #f8d7da; + --bs-light-bg-subtle: #fcfcfd; + --bs-dark-bg-subtle: #ced4da; + --bs-primary-border-subtle: #9ec5fe; + --bs-secondary-border-subtle: #c4c8cb; + --bs-success-border-subtle: #a3cfbb; + --bs-info-border-subtle: #9eeaf9; + --bs-warning-border-subtle: #ffe69c; + --bs-danger-border-subtle: #f1aeb5; + --bs-light-border-subtle: #e9ecef; + --bs-dark-border-subtle: #adb5bd; + --bs-white-rgb: 255, 255, 255; + --bs-black-rgb: 0, 0, 0; + --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); + --bs-body-font-family: var(--bs-font-sans-serif); + --bs-body-font-size: 1rem; + --bs-body-font-weight: 400; + --bs-body-line-height: 1.5; + --bs-body-color: #212529; + --bs-body-color-rgb: 33, 37, 41; + --bs-body-bg: #fff; + --bs-body-bg-rgb: 255, 255, 255; + --bs-emphasis-color: #000; + --bs-emphasis-color-rgb: 0, 0, 0; + --bs-secondary-color: rgba(33, 37, 41, 0.75); + --bs-secondary-color-rgb: 33, 37, 41; + --bs-secondary-bg: #e9ecef; + --bs-secondary-bg-rgb: 233, 236, 239; + --bs-tertiary-color: rgba(33, 37, 41, 0.5); + --bs-tertiary-color-rgb: 33, 37, 41; + --bs-tertiary-bg: #f8f9fa; + --bs-tertiary-bg-rgb: 248, 249, 250; + --bs-heading-color: inherit; + --bs-link-color: #0d6efd; + --bs-link-color-rgb: 13, 110, 253; + --bs-link-decoration: underline; + --bs-link-hover-color: #0a58ca; + --bs-link-hover-color-rgb: 10, 88, 202; + --bs-code-color: #d63384; + --bs-highlight-color: #212529; + --bs-highlight-bg: #fff3cd; + --bs-border-width: 1px; + --bs-border-style: solid; + --bs-border-color: #dee2e6; + --bs-border-color-translucent: rgba(0, 0, 0, 0.175); + --bs-border-radius: 0.375rem; + --bs-border-radius-sm: 0.25rem; + --bs-border-radius-lg: 0.5rem; + --bs-border-radius-xl: 1rem; + --bs-border-radius-xxl: 2rem; + --bs-border-radius-2xl: var(--bs-border-radius-xxl); + --bs-border-radius-pill: 50rem; + --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); + --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); + --bs-focus-ring-width: 0.25rem; + --bs-focus-ring-opacity: 0.25; + --bs-focus-ring-color: rgba(13, 110, 253, 0.25); + --bs-form-valid-color: #198754; + --bs-form-valid-border-color: #198754; + --bs-form-invalid-color: #dc3545; + --bs-form-invalid-border-color: #dc3545; +} + +[data-bs-theme=dark] { + color-scheme: dark; + --bs-body-color: #dee2e6; + --bs-body-color-rgb: 222, 226, 230; + --bs-body-bg: #212529; + --bs-body-bg-rgb: 33, 37, 41; + --bs-emphasis-color: #fff; + --bs-emphasis-color-rgb: 255, 255, 255; + --bs-secondary-color: rgba(222, 226, 230, 0.75); + --bs-secondary-color-rgb: 222, 226, 230; + --bs-secondary-bg: #343a40; + --bs-secondary-bg-rgb: 52, 58, 64; + --bs-tertiary-color: rgba(222, 226, 230, 0.5); + --bs-tertiary-color-rgb: 222, 226, 230; + --bs-tertiary-bg: #2b3035; + --bs-tertiary-bg-rgb: 43, 48, 53; + --bs-primary-text-emphasis: #6ea8fe; + --bs-secondary-text-emphasis: #a7acb1; + --bs-success-text-emphasis: #75b798; + --bs-info-text-emphasis: #6edff6; + --bs-warning-text-emphasis: #ffda6a; + --bs-danger-text-emphasis: #ea868f; + --bs-light-text-emphasis: #f8f9fa; + --bs-dark-text-emphasis: #dee2e6; + --bs-primary-bg-subtle: #031633; + --bs-secondary-bg-subtle: #161719; + --bs-success-bg-subtle: #051b11; + --bs-info-bg-subtle: #032830; + --bs-warning-bg-subtle: #332701; + --bs-danger-bg-subtle: #2c0b0e; + --bs-light-bg-subtle: #343a40; + --bs-dark-bg-subtle: #1a1d20; + --bs-primary-border-subtle: #084298; + --bs-secondary-border-subtle: #41464b; + --bs-success-border-subtle: #0f5132; + --bs-info-border-subtle: #087990; + --bs-warning-border-subtle: #997404; + --bs-danger-border-subtle: #842029; + --bs-light-border-subtle: #495057; + --bs-dark-border-subtle: #343a40; + --bs-heading-color: inherit; + --bs-link-color: #6ea8fe; + --bs-link-hover-color: #8bb9fe; + --bs-link-color-rgb: 110, 168, 254; + --bs-link-hover-color-rgb: 139, 185, 254; + --bs-code-color: #e685b5; + --bs-highlight-color: #dee2e6; + --bs-highlight-bg: #664d03; + --bs-border-color: #495057; + --bs-border-color-translucent: rgba(255, 255, 255, 0.15); + --bs-form-valid-color: #75b798; + --bs-form-valid-border-color: #75b798; + --bs-form-invalid-color: #ea868f; + --bs-form-invalid-border-color: #ea868f; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +@media (prefers-reduced-motion: no-preference) { + :root { + scroll-behavior: smooth; + } +} + +body { + margin: 0; + font-family: var(--bs-body-font-family); + font-size: var(--bs-body-font-size); + font-weight: var(--bs-body-font-weight); + line-height: var(--bs-body-line-height); + color: var(--bs-body-color); + text-align: var(--bs-body-text-align); + background-color: var(--bs-body-bg); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +hr { + margin: 1rem 0; + color: inherit; + border: 0; + border-top: var(--bs-border-width) solid; + opacity: 0.25; +} + +h6, h5, h4, h3, h2, h1 { + margin-top: 0; + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; + color: var(--bs-heading-color); +} + +h1 { + font-size: calc(1.375rem + 1.5vw); +} +@media (min-width: 1200px) { + h1 { + font-size: 2.5rem; + } +} + +h2 { + font-size: calc(1.325rem + 0.9vw); +} +@media (min-width: 1200px) { + h2 { + font-size: 2rem; + } +} + +h3 { + font-size: calc(1.3rem + 0.6vw); +} +@media (min-width: 1200px) { + h3 { + font-size: 1.75rem; + } +} + +h4 { + font-size: calc(1.275rem + 0.3vw); +} +@media (min-width: 1200px) { + h4 { + font-size: 1.5rem; + } +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title] { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul { + padding-left: 2rem; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small { + font-size: 0.875em; +} + +mark { + padding: 0.1875em; + color: var(--bs-highlight-color); + background-color: var(--bs-highlight-bg); +} + +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +a { + color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); + text-decoration: underline; +} +a:hover { + --bs-link-color-rgb: var(--bs-link-hover-color-rgb); +} + +a:not([href]):not([class]), a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: var(--bs-font-monospace); + font-size: 1em; +} + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + font-size: 0.875em; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +code { + font-size: 0.875em; + color: var(--bs-code-color); + word-wrap: break-word; +} +a > code { + color: inherit; +} + +kbd { + padding: 0.1875rem 0.375rem; + font-size: 0.875em; + color: var(--bs-body-bg); + background-color: var(--bs-body-color); + border-radius: 0.25rem; +} +kbd kbd { + padding: 0; + font-size: 1em; +} + +figure { + margin: 0 0 1rem; +} + +img, +svg { + vertical-align: middle; +} + +table { + caption-side: bottom; + border-collapse: collapse; +} + +caption { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + color: var(--bs-secondary-color); + text-align: left; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +thead, +tbody, +tfoot, +tr, +td, +th { + border-color: inherit; + border-style: solid; + border-width: 0; +} + +label { + display: inline-block; +} + +button { + border-radius: 0; +} + +button:focus:not(:focus-visible) { + outline: 0; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +select { + text-transform: none; +} + +[role=button] { + cursor: pointer; +} + +select { + word-wrap: normal; +} +select:disabled { + opacity: 1; +} + +[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { + display: none !important; +} + +button, +[type=button], +[type=reset], +[type=submit] { + -webkit-appearance: button; +} +button:not(:disabled), +[type=button]:not(:disabled), +[type=reset]:not(:disabled), +[type=submit]:not(:disabled) { + cursor: pointer; +} + +::-moz-focus-inner { + padding: 0; + border-style: none; +} + +textarea { + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + float: left; + width: 100%; + padding: 0; + margin-bottom: 0.5rem; + line-height: inherit; + font-size: calc(1.275rem + 0.3vw); +} +@media (min-width: 1200px) { + legend { + font-size: 1.5rem; + } +} +legend + * { + clear: left; +} + +::-webkit-datetime-edit-fields-wrapper, +::-webkit-datetime-edit-text, +::-webkit-datetime-edit-minute, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-year-field { + padding: 0; +} + +::-webkit-inner-spin-button { + height: auto; +} + +[type=search] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +[type=search]::-webkit-search-cancel-button { + cursor: pointer; + filter: grayscale(1); +} + +/* rtl:raw: +[type="tel"], +[type="url"], +[type="email"], +[type="number"] { + direction: ltr; +} +*/ +::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-color-swatch-wrapper { + padding: 0; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +::file-selector-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +iframe { + border: 0; +} + +summary { + display: list-item; + cursor: pointer; +} + +progress { + vertical-align: baseline; +} + +[hidden] { + display: none !important; +} + +/*# sourceMappingURL=bootstrap-reboot.css.map */ \ No newline at end of file diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map new file mode 100644 index 0000000..d718451 --- /dev/null +++ b/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_root.scss","../../scss/vendor/_rfs.scss","bootstrap-reboot.css","../../scss/mixins/_color-mode.scss","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/mixins/_border-radius.scss"],"names":[],"mappings":"AACE;;;;EAAA;ACDF;;EASI,kBAAA;EAAA,oBAAA;EAAA,oBAAA;EAAA,kBAAA;EAAA,iBAAA;EAAA,oBAAA;EAAA,oBAAA;EAAA,mBAAA;EAAA,kBAAA;EAAA,kBAAA;EAAA,gBAAA;EAAA,gBAAA;EAAA,kBAAA;EAAA,uBAAA;EAIA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAIA,qBAAA;EAAA,uBAAA;EAAA,qBAAA;EAAA,kBAAA;EAAA,qBAAA;EAAA,oBAAA;EAAA,mBAAA;EAAA,kBAAA;EAIA,8BAAA;EAAA,iCAAA;EAAA,6BAAA;EAAA,2BAAA;EAAA,6BAAA;EAAA,4BAAA;EAAA,6BAAA;EAAA,yBAAA;EAIA,mCAAA;EAAA,qCAAA;EAAA,mCAAA;EAAA,gCAAA;EAAA,mCAAA;EAAA,kCAAA;EAAA,iCAAA;EAAA,gCAAA;EAIA,+BAAA;EAAA,iCAAA;EAAA,+BAAA;EAAA,4BAAA;EAAA,+BAAA;EAAA,8BAAA;EAAA,6BAAA;EAAA,4BAAA;EAIA,mCAAA;EAAA,qCAAA;EAAA,mCAAA;EAAA,gCAAA;EAAA,mCAAA;EAAA,kCAAA;EAAA,iCAAA;EAAA,gCAAA;EAGF,6BAAA;EACA,uBAAA;EAMA,qNAAA;EACA,yGAAA;EACA,yFAAA;EAOA,gDAAA;EC2OI,yBALI;EDpOR,0BAAA;EACA,0BAAA;EAKA,wBAAA;EACA,+BAAA;EACA,kBAAA;EACA,+BAAA;EAEA,yBAAA;EACA,gCAAA;EAEA,4CAAA;EACA,oCAAA;EACA,0BAAA;EACA,oCAAA;EAEA,0CAAA;EACA,mCAAA;EACA,yBAAA;EACA,mCAAA;EAGA,2BAAA;EAEA,wBAAA;EACA,iCAAA;EACA,+BAAA;EAEA,8BAAA;EACA,sCAAA;EAMA,wBAAA;EACA,6BAAA;EACA,0BAAA;EAGA,sBAAA;EACA,wBAAA;EACA,0BAAA;EACA,mDAAA;EAEA,4BAAA;EACA,8BAAA;EACA,6BAAA;EACA,2BAAA;EACA,4BAAA;EACA,mDAAA;EACA,8BAAA;EAGA,kDAAA;EACA,2DAAA;EACA,oDAAA;EACA,2DAAA;EAIA,8BAAA;EACA,6BAAA;EACA,+CAAA;EAIA,8BAAA;EACA,qCAAA;EACA,gCAAA;EACA,uCAAA;AEHF;;AC7GI;EHsHA,kBAAA;EAGA,wBAAA;EACA,kCAAA;EACA,qBAAA;EACA,4BAAA;EAEA,yBAAA;EACA,sCAAA;EAEA,+CAAA;EACA,uCAAA;EACA,0BAAA;EACA,iCAAA;EAEA,6CAAA;EACA,sCAAA;EACA,yBAAA;EACA,gCAAA;EAGE,mCAAA;EAAA,qCAAA;EAAA,mCAAA;EAAA,gCAAA;EAAA,mCAAA;EAAA,kCAAA;EAAA,iCAAA;EAAA,gCAAA;EAIA,+BAAA;EAAA,iCAAA;EAAA,+BAAA;EAAA,4BAAA;EAAA,+BAAA;EAAA,8BAAA;EAAA,6BAAA;EAAA,4BAAA;EAIA,mCAAA;EAAA,qCAAA;EAAA,mCAAA;EAAA,gCAAA;EAAA,mCAAA;EAAA,kCAAA;EAAA,iCAAA;EAAA,gCAAA;EAGF,2BAAA;EAEA,wBAAA;EACA,8BAAA;EACA,kCAAA;EACA,wCAAA;EAEA,wBAAA;EACA,6BAAA;EACA,0BAAA;EAEA,0BAAA;EACA,wDAAA;EAEA,8BAAA;EACA,qCAAA;EACA,gCAAA;EACA,uCAAA;AEHJ;;AErKA;;;EAGE,sBAAA;AFwKF;;AEzJI;EANJ;IAOM,uBAAA;EF6JJ;AACF;;AEhJA;EACE,SAAA;EACA,uCAAA;EH6OI,mCALI;EGtOR,uCAAA;EACA,uCAAA;EACA,2BAAA;EACA,qCAAA;EACA,mCAAA;EACA,8BAAA;EACA,6CAAA;AFmJF;;AE1IA;EACE,cAAA;EACA,cCmnB4B;EDlnB5B,SAAA;EACA,wCAAA;EACA,aCynB4B;AH5e9B;;AEnIA;EACE,aAAA;EACA,qBCwjB4B;EDrjB5B,gBCwjB4B;EDvjB5B,gBCwjB4B;EDvjB5B,8BAAA;AFoIF;;AEjIA;EHuMQ,iCAAA;AClER;AD1FI;EG3CJ;IH8MQ,iBAAA;ECrEN;AACF;;AErIA;EHkMQ,iCAAA;ACzDR;ADnGI;EGtCJ;IHyMQ,eAAA;EC5DN;AACF;;AEzIA;EH6LQ,+BAAA;AChDR;AD5GI;EGjCJ;IHoMQ,kBAAA;ECnDN;AACF;;AE7IA;EHwLQ,iCAAA;ACvCR;ADrHI;EG5BJ;IH+LQ,iBAAA;EC1CN;AACF;;AEjJA;EH+KM,kBALI;ACrBV;;AEhJA;EH0KM,eALI;ACjBV;;AEzIA;EACE,aAAA;EACA,mBCwV0B;AH5M5B;;AElIA;EACE,yCAAA;EAAA,iCAAA;EACA,YAAA;EACA,sCAAA;EAAA,8BAAA;AFqIF;;AE/HA;EACE,mBAAA;EACA,kBAAA;EACA,oBAAA;AFkIF;;AE5HA;;EAEE,kBAAA;AF+HF;;AE5HA;;;EAGE,aAAA;EACA,mBAAA;AF+HF;;AE5HA;;;;EAIE,gBAAA;AF+HF;;AE5HA;EACE,gBC6b4B;AH9T9B;;AE1HA;EACE,qBAAA;EACA,cAAA;AF6HF;;AEvHA;EACE,gBAAA;AF0HF;;AElHA;;EAEE,mBCsa4B;AHjT9B;;AE7GA;EH6EM,kBALI;ACyCV;;AE1GA;EACE,iBCqf4B;EDpf5B,gCAAA;EACA,wCAAA;AF6GF;;AEpGA;;EAEE,kBAAA;EHwDI,iBALI;EGjDR,cAAA;EACA,wBAAA;AFuGF;;AEpGA;EAAM,eAAA;AFwGN;;AEvGA;EAAM,WAAA;AF2GN;;AEtGA;EACE,gEAAA;EACA,0BCgNwC;AHvG1C;AEvGE;EACE,mDAAA;AFyGJ;;AE9FE;EAEE,cAAA;EACA,qBAAA;AFgGJ;;AEzFA;;;;EAIE,qCCgV4B;EJlUxB,cALI;ACoFV;;AErFA;EACE,cAAA;EACA,aAAA;EACA,mBAAA;EACA,cAAA;EHEI,kBALI;AC4FV;AEpFE;EHHI,kBALI;EGUN,cAAA;EACA,kBAAA;AFsFJ;;AElFA;EHVM,kBALI;EGiBR,2BAAA;EACA,qBAAA;AFqFF;AElFE;EACE,cAAA;AFoFJ;;AEhFA;EACE,2BAAA;EHtBI,kBALI;EG6BR,wBC25CkC;ED15ClC,sCC25CkC;EChsDhC,sBAAA;AJyXJ;AEjFE;EACE,UAAA;EH7BE,cALI;ACsHV;;AEzEA;EACE,gBAAA;AF4EF;;AEtEA;;EAEE,sBAAA;AFyEF;;AEjEA;EACE,oBAAA;EACA,yBAAA;AFoEF;;AEjEA;EACE,mBC4X4B;ED3X5B,sBC2X4B;ED1X5B,gCC4Z4B;ED3Z5B,gBAAA;AFoEF;;AE7DA;EAEE,mBAAA;EACA,gCAAA;AF+DF;;AE5DA;;;;;;EAME,qBAAA;EACA,mBAAA;EACA,eAAA;AF+DF;;AEvDA;EACE,qBAAA;AF0DF;;AEpDA;EAEE,gBAAA;AFsDF;;AE9CA;EACE,UAAA;AFiDF;;AE5CA;;;;;EAKE,SAAA;EACA,oBAAA;EH5HI,kBALI;EGmIR,oBAAA;AF+CF;;AE3CA;;EAEE,oBAAA;AF8CF;;AEzCA;EACE,eAAA;AF4CF;;AEzCA;EAGE,iBAAA;AF0CF;AEvCE;EACE,UAAA;AFyCJ;;AElCA;EACE,wBAAA;AFqCF;;AE7BA;;;;EAIE,0BAAA;AFgCF;AE7BI;;;;EACE,eAAA;AFkCN;;AE3BA;EACE,UAAA;EACA,kBAAA;AF8BF;;AEzBA;EACE,gBAAA;AF4BF;;AElBA;EACE,YAAA;EACA,UAAA;EACA,SAAA;EACA,SAAA;AFqBF;;AEbA;EACE,WAAA;EACA,WAAA;EACA,UAAA;EACA,qBCmN4B;EDjN5B,oBAAA;EHnNM,iCAAA;ACmOR;AD/XI;EGyWJ;IHtMQ,iBAAA;ECgON;AACF;AElBE;EACE,WAAA;AFoBJ;;AEbA;;;;;;;EAOE,UAAA;AFgBF;;AEbA;EACE,YAAA;AFgBF;;AEPA;EACE,6BAAA;EACA,oBAAA;AFUF;AEPE;EACE,eAAA;EACA,oBAAA;AFSJ;;AEAA;;;;;;;CAAA;AAWA;EACE,wBAAA;AFAF;;AEKA;EACE,UAAA;AFFF;;AESA;EACE,aAAA;EACA,0BAAA;AFNF;;AEIA;EACE,aAAA;EACA,0BAAA;AFNF;;AEWA;EACE,qBAAA;AFRF;;AEaA;EACE,SAAA;AFVF;;AEiBA;EACE,kBAAA;EACA,eAAA;AFdF;;AEsBA;EACE,wBAAA;AFnBF;;AE2BA;EACE,wBAAA;AFxBF","file":"bootstrap-reboot.css","sourcesContent":["@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n",":root,\n[data-bs-theme=\"light\"] {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$prefix}#{$color}-rgb: #{$value};\n }\n\n @each $color, $value in $theme-colors-text {\n --#{$prefix}#{$color}-text-emphasis: #{$value};\n }\n\n @each $color, $value in $theme-colors-bg-subtle {\n --#{$prefix}#{$color}-bg-subtle: #{$value};\n }\n\n @each $color, $value in $theme-colors-border-subtle {\n --#{$prefix}#{$color}-border-subtle: #{$value};\n }\n\n --#{$prefix}white-rgb: #{to-rgb($white)};\n --#{$prefix}black-rgb: #{to-rgb($black)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$prefix}gradient: #{$gradient};\n\n // Root and body\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$prefix}root-font-size: #{$font-size-root};\n }\n --#{$prefix}body-font-family: #{inspect($font-family-base)};\n @include rfs($font-size-base, --#{$prefix}body-font-size);\n --#{$prefix}body-font-weight: #{$font-weight-base};\n --#{$prefix}body-line-height: #{$line-height-base};\n @if $body-text-align != null {\n --#{$prefix}body-text-align: #{$body-text-align};\n }\n\n --#{$prefix}body-color: #{$body-color};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$prefix}body-bg: #{$body-bg};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg)};\n // scss-docs-end root-body-variables\n\n --#{$prefix}heading-color: #{$headings-color};\n\n --#{$prefix}link-color: #{$link-color};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color)};\n --#{$prefix}link-decoration: #{$link-decoration};\n\n --#{$prefix}link-hover-color: #{$link-hover-color};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color)};\n\n @if $link-hover-decoration != null {\n --#{$prefix}link-hover-decoration: #{$link-hover-decoration};\n }\n\n --#{$prefix}code-color: #{$code-color};\n --#{$prefix}highlight-color: #{$mark-color};\n --#{$prefix}highlight-bg: #{$mark-bg};\n\n // scss-docs-start root-border-var\n --#{$prefix}border-width: #{$border-width};\n --#{$prefix}border-style: #{$border-style};\n --#{$prefix}border-color: #{$border-color};\n --#{$prefix}border-color-translucent: #{$border-color-translucent};\n\n --#{$prefix}border-radius: #{$border-radius};\n --#{$prefix}border-radius-sm: #{$border-radius-sm};\n --#{$prefix}border-radius-lg: #{$border-radius-lg};\n --#{$prefix}border-radius-xl: #{$border-radius-xl};\n --#{$prefix}border-radius-xxl: #{$border-radius-xxl};\n --#{$prefix}border-radius-2xl: var(--#{$prefix}border-radius-xxl); // Deprecated in v5.3.0 for consistency\n --#{$prefix}border-radius-pill: #{$border-radius-pill};\n // scss-docs-end root-border-var\n\n --#{$prefix}box-shadow: #{$box-shadow};\n --#{$prefix}box-shadow-sm: #{$box-shadow-sm};\n --#{$prefix}box-shadow-lg: #{$box-shadow-lg};\n --#{$prefix}box-shadow-inset: #{$box-shadow-inset};\n\n // Focus styles\n // scss-docs-start root-focus-variables\n --#{$prefix}focus-ring-width: #{$focus-ring-width};\n --#{$prefix}focus-ring-opacity: #{$focus-ring-opacity};\n --#{$prefix}focus-ring-color: #{$focus-ring-color};\n // scss-docs-end root-focus-variables\n\n // scss-docs-start root-form-validation-variables\n --#{$prefix}form-valid-color: #{$form-valid-color};\n --#{$prefix}form-valid-border-color: #{$form-valid-border-color};\n --#{$prefix}form-invalid-color: #{$form-invalid-color};\n --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color};\n // scss-docs-end root-form-validation-variables\n}\n\n@if $enable-dark-mode {\n @include color-mode(dark, true) {\n color-scheme: dark;\n\n // scss-docs-start root-dark-mode-vars\n --#{$prefix}body-color: #{$body-color-dark};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color-dark)};\n --#{$prefix}body-bg: #{$body-bg-dark};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg-dark)};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color-dark};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color-dark)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color-dark};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color-dark)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg-dark};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg-dark)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color-dark};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color-dark)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg-dark};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg-dark)};\n\n @each $color, $value in $theme-colors-text-dark {\n --#{$prefix}#{$color}-text-emphasis: #{$value};\n }\n\n @each $color, $value in $theme-colors-bg-subtle-dark {\n --#{$prefix}#{$color}-bg-subtle: #{$value};\n }\n\n @each $color, $value in $theme-colors-border-subtle-dark {\n --#{$prefix}#{$color}-border-subtle: #{$value};\n }\n\n --#{$prefix}heading-color: #{$headings-color-dark};\n\n --#{$prefix}link-color: #{$link-color-dark};\n --#{$prefix}link-hover-color: #{$link-hover-color-dark};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color-dark)};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color-dark)};\n\n --#{$prefix}code-color: #{$code-color-dark};\n --#{$prefix}highlight-color: #{$mark-color-dark};\n --#{$prefix}highlight-bg: #{$mark-bg-dark};\n\n --#{$prefix}border-color: #{$border-color-dark};\n --#{$prefix}border-color-translucent: #{$border-color-translucent-dark};\n\n --#{$prefix}form-valid-color: #{$form-valid-color-dark};\n --#{$prefix}form-valid-border-color: #{$form-valid-border-color-dark};\n --#{$prefix}form-invalid-color: #{$form-invalid-color-dark};\n --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color-dark};\n // scss-docs-end root-dark-mode-vars\n }\n}\n","// stylelint-disable scss/dimension-no-non-numeric-values\n\n// SCSS RFS mixin\n//\n// Automated responsive values for font sizes, paddings, margins and much more\n//\n// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)\n\n// Configuration\n\n// Base value\n$rfs-base-value: 1.25rem !default;\n$rfs-unit: rem !default;\n\n@if $rfs-unit != rem and $rfs-unit != px {\n @error \"`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.\";\n}\n\n// Breakpoint at where values start decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n}\n\n// Resize values based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != number or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Mode. Possibilities: \"min-media-query\", \"max-media-query\"\n$rfs-mode: min-media-query !default;\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-rfs to false\n$enable-rfs: true !default;\n\n// Cache $rfs-base-value unit\n$rfs-base-value-unit: unit($rfs-base-value);\n\n@function divide($dividend, $divisor, $precision: 10) {\n $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);\n $dividend: abs($dividend);\n $divisor: abs($divisor);\n @if $dividend == 0 {\n @return 0;\n }\n @if $divisor == 0 {\n @error \"Cannot divide by 0\";\n }\n $remainder: $dividend;\n $result: 0;\n $factor: 10;\n @while ($remainder > 0 and $precision >= 0) {\n $quotient: 0;\n @while ($remainder >= $divisor) {\n $remainder: $remainder - $divisor;\n $quotient: $quotient + 1;\n }\n $result: $result * 10 + $quotient;\n $factor: $factor * .1;\n $remainder: $remainder * 10;\n $precision: $precision - 1;\n @if ($precision < 0 and $remainder >= $divisor * 5) {\n $result: $result + 1;\n }\n }\n $result: $result * $factor * $sign;\n $dividend-unit: unit($dividend);\n $divisor-unit: unit($divisor);\n $unit-map: (\n \"px\": 1px,\n \"rem\": 1rem,\n \"em\": 1em,\n \"%\": 1%\n );\n @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {\n $result: $result * map-get($unit-map, $dividend-unit);\n }\n @return $result;\n}\n\n// Remove px-unit from $rfs-base-value for calculations\n@if $rfs-base-value-unit == px {\n $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);\n}\n@else if $rfs-base-value-unit == rem {\n $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == px {\n $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));\n}\n\n// Calculate the media query value\n$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});\n$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);\n$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);\n\n// Internal mixin used to determine which media query needs to be used\n@mixin _rfs-media-query {\n @if $rfs-two-dimensional {\n @if $rfs-mode == max-media-query {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {\n @content;\n }\n }\n}\n\n// Internal mixin that adds disable classes to the selector if needed.\n@mixin _rfs-rule {\n @if $rfs-class == disable and $rfs-mode == max-media-query {\n // Adding an extra class increases specificity, which prevents the media query to override the property\n &,\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @else if $rfs-class == enable and $rfs-mode == min-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Internal mixin that adds enable classes to the selector if needed.\n@mixin _rfs-media-query-rule {\n\n @if $rfs-class == enable {\n @if $rfs-mode == min-media-query {\n @content;\n }\n\n @include _rfs-media-query () {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n }\n @else {\n @if $rfs-class == disable and $rfs-mode == min-media-query {\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @include _rfs-media-query () {\n @content;\n }\n }\n}\n\n// Helper function to get the formatted non-responsive value\n@function rfs-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: \"\";\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + \" 0\";\n }\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n @if $unit == px {\n // Convert to rem if needed\n $val: $val + \" \" + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);\n }\n @else if $unit == rem {\n // Convert to px if needed\n $val: $val + \" \" + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);\n } @else {\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n $val: $val + \" \" + $value;\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// Helper function to get the responsive value calculated by RFS\n@function rfs-fluid-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: \"\";\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + \" 0\";\n } @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $unit or $unit != px and $unit != rem {\n $val: $val + \" \" + $value;\n } @else {\n // Remove unit from $value for calculations\n $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));\n\n // Only add the media query if the value is greater than the minimum value\n @if abs($value) <= $rfs-base-value or not $enable-rfs {\n $val: $val + \" \" + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);\n }\n @else {\n // Calculate the minimum value\n $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);\n\n // Calculate difference between $value and the minimum value\n $value-diff: abs($value) - $value-min;\n\n // Base value formatting\n $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);\n\n // Use negative value if needed\n $min-width: if($value < 0, -$min-width, $min-width);\n\n // Use `vmin` if two-dimensional is enabled\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};\n\n // Return the calculated value\n $val: $val + \" calc(\" + $min-width + if($value < 0, \" - \", \" + \") + $variable-width + \")\";\n }\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// RFS mixin\n@mixin rfs($values, $property: font-size) {\n @if $values != null {\n $val: rfs-value($values);\n $fluid-val: rfs-fluid-value($values);\n\n // Do not print the media query if responsive & non-responsive values are the same\n @if $val == $fluid-val {\n #{$property}: $val;\n }\n @else {\n @include _rfs-rule () {\n #{$property}: if($rfs-mode == max-media-query, $val, $fluid-val);\n\n // Include safari iframe resize fix if needed\n min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);\n }\n\n @include _rfs-media-query-rule () {\n #{$property}: if($rfs-mode == max-media-query, $fluid-val, $val);\n }\n }\n }\n}\n\n// Shorthand helper mixins\n@mixin font-size($value) {\n @include rfs($value);\n}\n\n@mixin padding($value) {\n @include rfs($value, padding);\n}\n\n@mixin padding-top($value) {\n @include rfs($value, padding-top);\n}\n\n@mixin padding-right($value) {\n @include rfs($value, padding-right);\n}\n\n@mixin padding-bottom($value) {\n @include rfs($value, padding-bottom);\n}\n\n@mixin padding-left($value) {\n @include rfs($value, padding-left);\n}\n\n@mixin margin($value) {\n @include rfs($value, margin);\n}\n\n@mixin margin-top($value) {\n @include rfs($value, margin-top);\n}\n\n@mixin margin-right($value) {\n @include rfs($value, margin-right);\n}\n\n@mixin margin-bottom($value) {\n @include rfs($value, margin-bottom);\n}\n\n@mixin margin-left($value) {\n @include rfs($value, margin-left);\n}\n","/*!\n * Bootstrap Reboot v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root,\n[data-bs-theme=light] {\n --bs-blue: #0d6efd;\n --bs-indigo: #6610f2;\n --bs-purple: #6f42c1;\n --bs-pink: #d63384;\n --bs-red: #dc3545;\n --bs-orange: #fd7e14;\n --bs-yellow: #ffc107;\n --bs-green: #198754;\n --bs-teal: #20c997;\n --bs-cyan: #0dcaf0;\n --bs-black: #000;\n --bs-white: #fff;\n --bs-gray: #6c757d;\n --bs-gray-dark: #343a40;\n --bs-gray-100: #f8f9fa;\n --bs-gray-200: #e9ecef;\n --bs-gray-300: #dee2e6;\n --bs-gray-400: #ced4da;\n --bs-gray-500: #adb5bd;\n --bs-gray-600: #6c757d;\n --bs-gray-700: #495057;\n --bs-gray-800: #343a40;\n --bs-gray-900: #212529;\n --bs-primary: #0d6efd;\n --bs-secondary: #6c757d;\n --bs-success: #198754;\n --bs-info: #0dcaf0;\n --bs-warning: #ffc107;\n --bs-danger: #dc3545;\n --bs-light: #f8f9fa;\n --bs-dark: #212529;\n --bs-primary-rgb: 13, 110, 253;\n --bs-secondary-rgb: 108, 117, 125;\n --bs-success-rgb: 25, 135, 84;\n --bs-info-rgb: 13, 202, 240;\n --bs-warning-rgb: 255, 193, 7;\n --bs-danger-rgb: 220, 53, 69;\n --bs-light-rgb: 248, 249, 250;\n --bs-dark-rgb: 33, 37, 41;\n --bs-primary-text-emphasis: #052c65;\n --bs-secondary-text-emphasis: #2b2f32;\n --bs-success-text-emphasis: #0a3622;\n --bs-info-text-emphasis: #055160;\n --bs-warning-text-emphasis: #664d03;\n --bs-danger-text-emphasis: #58151c;\n --bs-light-text-emphasis: #495057;\n --bs-dark-text-emphasis: #495057;\n --bs-primary-bg-subtle: #cfe2ff;\n --bs-secondary-bg-subtle: #e2e3e5;\n --bs-success-bg-subtle: #d1e7dd;\n --bs-info-bg-subtle: #cff4fc;\n --bs-warning-bg-subtle: #fff3cd;\n --bs-danger-bg-subtle: #f8d7da;\n --bs-light-bg-subtle: #fcfcfd;\n --bs-dark-bg-subtle: #ced4da;\n --bs-primary-border-subtle: #9ec5fe;\n --bs-secondary-border-subtle: #c4c8cb;\n --bs-success-border-subtle: #a3cfbb;\n --bs-info-border-subtle: #9eeaf9;\n --bs-warning-border-subtle: #ffe69c;\n --bs-danger-border-subtle: #f1aeb5;\n --bs-light-border-subtle: #e9ecef;\n --bs-dark-border-subtle: #adb5bd;\n --bs-white-rgb: 255, 255, 255;\n --bs-black-rgb: 0, 0, 0;\n --bs-font-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", \"Liberation Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n --bs-body-font-family: var(--bs-font-sans-serif);\n --bs-body-font-size: 1rem;\n --bs-body-font-weight: 400;\n --bs-body-line-height: 1.5;\n --bs-body-color: #212529;\n --bs-body-color-rgb: 33, 37, 41;\n --bs-body-bg: #fff;\n --bs-body-bg-rgb: 255, 255, 255;\n --bs-emphasis-color: #000;\n --bs-emphasis-color-rgb: 0, 0, 0;\n --bs-secondary-color: rgba(33, 37, 41, 0.75);\n --bs-secondary-color-rgb: 33, 37, 41;\n --bs-secondary-bg: #e9ecef;\n --bs-secondary-bg-rgb: 233, 236, 239;\n --bs-tertiary-color: rgba(33, 37, 41, 0.5);\n --bs-tertiary-color-rgb: 33, 37, 41;\n --bs-tertiary-bg: #f8f9fa;\n --bs-tertiary-bg-rgb: 248, 249, 250;\n --bs-heading-color: inherit;\n --bs-link-color: #0d6efd;\n --bs-link-color-rgb: 13, 110, 253;\n --bs-link-decoration: underline;\n --bs-link-hover-color: #0a58ca;\n --bs-link-hover-color-rgb: 10, 88, 202;\n --bs-code-color: #d63384;\n --bs-highlight-color: #212529;\n --bs-highlight-bg: #fff3cd;\n --bs-border-width: 1px;\n --bs-border-style: solid;\n --bs-border-color: #dee2e6;\n --bs-border-color-translucent: rgba(0, 0, 0, 0.175);\n --bs-border-radius: 0.375rem;\n --bs-border-radius-sm: 0.25rem;\n --bs-border-radius-lg: 0.5rem;\n --bs-border-radius-xl: 1rem;\n --bs-border-radius-xxl: 2rem;\n --bs-border-radius-2xl: var(--bs-border-radius-xxl);\n --bs-border-radius-pill: 50rem;\n --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);\n --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);\n --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);\n --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);\n --bs-focus-ring-width: 0.25rem;\n --bs-focus-ring-opacity: 0.25;\n --bs-focus-ring-color: rgba(13, 110, 253, 0.25);\n --bs-form-valid-color: #198754;\n --bs-form-valid-border-color: #198754;\n --bs-form-invalid-color: #dc3545;\n --bs-form-invalid-border-color: #dc3545;\n}\n\n[data-bs-theme=dark] {\n color-scheme: dark;\n --bs-body-color: #dee2e6;\n --bs-body-color-rgb: 222, 226, 230;\n --bs-body-bg: #212529;\n --bs-body-bg-rgb: 33, 37, 41;\n --bs-emphasis-color: #fff;\n --bs-emphasis-color-rgb: 255, 255, 255;\n --bs-secondary-color: rgba(222, 226, 230, 0.75);\n --bs-secondary-color-rgb: 222, 226, 230;\n --bs-secondary-bg: #343a40;\n --bs-secondary-bg-rgb: 52, 58, 64;\n --bs-tertiary-color: rgba(222, 226, 230, 0.5);\n --bs-tertiary-color-rgb: 222, 226, 230;\n --bs-tertiary-bg: #2b3035;\n --bs-tertiary-bg-rgb: 43, 48, 53;\n --bs-primary-text-emphasis: #6ea8fe;\n --bs-secondary-text-emphasis: #a7acb1;\n --bs-success-text-emphasis: #75b798;\n --bs-info-text-emphasis: #6edff6;\n --bs-warning-text-emphasis: #ffda6a;\n --bs-danger-text-emphasis: #ea868f;\n --bs-light-text-emphasis: #f8f9fa;\n --bs-dark-text-emphasis: #dee2e6;\n --bs-primary-bg-subtle: #031633;\n --bs-secondary-bg-subtle: #161719;\n --bs-success-bg-subtle: #051b11;\n --bs-info-bg-subtle: #032830;\n --bs-warning-bg-subtle: #332701;\n --bs-danger-bg-subtle: #2c0b0e;\n --bs-light-bg-subtle: #343a40;\n --bs-dark-bg-subtle: #1a1d20;\n --bs-primary-border-subtle: #084298;\n --bs-secondary-border-subtle: #41464b;\n --bs-success-border-subtle: #0f5132;\n --bs-info-border-subtle: #087990;\n --bs-warning-border-subtle: #997404;\n --bs-danger-border-subtle: #842029;\n --bs-light-border-subtle: #495057;\n --bs-dark-border-subtle: #343a40;\n --bs-heading-color: inherit;\n --bs-link-color: #6ea8fe;\n --bs-link-hover-color: #8bb9fe;\n --bs-link-color-rgb: 110, 168, 254;\n --bs-link-hover-color-rgb: 139, 185, 254;\n --bs-code-color: #e685b5;\n --bs-highlight-color: #dee2e6;\n --bs-highlight-bg: #664d03;\n --bs-border-color: #495057;\n --bs-border-color-translucent: rgba(255, 255, 255, 0.15);\n --bs-form-valid-color: #75b798;\n --bs-form-valid-border-color: #75b798;\n --bs-form-invalid-color: #ea868f;\n --bs-form-invalid-border-color: #ea868f;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n :root {\n scroll-behavior: smooth;\n }\n}\n\nbody {\n margin: 0;\n font-family: var(--bs-body-font-family);\n font-size: var(--bs-body-font-size);\n font-weight: var(--bs-body-font-weight);\n line-height: var(--bs-body-line-height);\n color: var(--bs-body-color);\n text-align: var(--bs-body-text-align);\n background-color: var(--bs-body-bg);\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nhr {\n margin: 1rem 0;\n color: inherit;\n border: 0;\n border-top: var(--bs-border-width) solid;\n opacity: 0.25;\n}\n\nh6, h5, h4, h3, h2, h1 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n color: var(--bs-heading-color);\n}\n\nh1 {\n font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n h1 {\n font-size: 2.5rem;\n }\n}\n\nh2 {\n font-size: calc(1.325rem + 0.9vw);\n}\n@media (min-width: 1200px) {\n h2 {\n font-size: 2rem;\n }\n}\n\nh3 {\n font-size: calc(1.3rem + 0.6vw);\n}\n@media (min-width: 1200px) {\n h3 {\n font-size: 1.75rem;\n }\n}\n\nh4 {\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n h4 {\n font-size: 1.5rem;\n }\n}\n\nh5 {\n font-size: 1.25rem;\n}\n\nh6 {\n font-size: 1rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title] {\n text-decoration: underline dotted;\n cursor: help;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: 0.5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 0.875em;\n}\n\nmark {\n padding: 0.1875em;\n color: var(--bs-highlight-color);\n background-color: var(--bs-highlight-bg);\n}\n\nsub,\nsup {\n position: relative;\n font-size: 0.75em;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\na {\n color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));\n text-decoration: underline;\n}\na:hover {\n --bs-link-color-rgb: var(--bs-link-hover-color-rgb);\n}\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: var(--bs-font-monospace);\n font-size: 1em;\n}\n\npre {\n display: block;\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n font-size: 0.875em;\n}\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\ncode {\n font-size: 0.875em;\n color: var(--bs-code-color);\n word-wrap: break-word;\n}\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.1875rem 0.375rem;\n font-size: 0.875em;\n color: var(--bs-body-bg);\n background-color: var(--bs-body-color);\n border-radius: 0.25rem;\n}\nkbd kbd {\n padding: 0;\n font-size: 1em;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: var(--bs-secondary-color);\n text-align: left;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\nlabel {\n display: inline-block;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=button] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\nselect:disabled {\n opacity: 1;\n}\n\n[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {\n display: none !important;\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n -webkit-appearance: button;\n}\nbutton:not(:disabled),\n[type=button]:not(:disabled),\n[type=reset]:not(:disabled),\n[type=submit]:not(:disabled) {\n cursor: pointer;\n}\n\n::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ntextarea {\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n float: left;\n width: 100%;\n padding: 0;\n margin-bottom: 0.5rem;\n line-height: inherit;\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n legend {\n font-size: 1.5rem;\n }\n}\nlegend + * {\n clear: left;\n}\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n padding: 0;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n[type=search] {\n -webkit-appearance: textfield;\n outline-offset: -2px;\n}\n[type=search]::-webkit-search-cancel-button {\n cursor: pointer;\n filter: grayscale(1);\n}\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n::file-selector-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\niframe {\n border: 0;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */\n","// scss-docs-start color-mode-mixin\n@mixin color-mode($mode: light, $root: false) {\n @if $color-mode-type == \"media-query\" {\n @if $root == true {\n @media (prefers-color-scheme: $mode) {\n :root {\n @content;\n }\n }\n } @else {\n @media (prefers-color-scheme: $mode) {\n @content;\n }\n }\n } @else {\n [data-bs-theme=\"#{$mode}\"] {\n @content;\n }\n }\n}\n// scss-docs-end color-mode-mixin\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n @include font-size(var(--#{$prefix}root-font-size));\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$prefix}body-font-family);\n @include font-size(var(--#{$prefix}body-font-size));\n font-weight: var(--#{$prefix}body-font-weight);\n line-height: var(--#{$prefix}body-line-height);\n color: var(--#{$prefix}body-color);\n text-align: var(--#{$prefix}body-text-align);\n background-color: var(--#{$prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n opacity: $hr-opacity;\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: var(--#{$prefix}heading-color);\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 2. Add explicit cursor to indicate changed behavior.\n// 3. Prevent the text-decoration to be skipped.\n\nabbr[title] {\n text-decoration: underline dotted; // 1\n cursor: help; // 2\n text-decoration-skip-ink: none; // 3\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n color: var(--#{$prefix}highlight-color);\n background-color: var(--#{$prefix}highlight-bg);\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1));\n text-decoration: $link-decoration;\n\n &:hover {\n --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb);\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: var(--#{$prefix}code-color);\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-` + + +

+ +@* Message Detail Modal *@ +@if (showMessageModal && selectedNotification != null) +{ + +} + +@code { + private List notifications = new List(); + private List sortedNotifications = new List(); + private List pagedNotifications = new List(); + + private Notification? selectedNotification; + private bool showMessageModal = false; + + private string sortColumn = nameof(Notification.CreatedOn); + private bool sortAscending = true; + + private int currentPage = 1; + private int pageSize = 25; + private int totalPages = 1; + private int totalRecords = 0; + + protected override async Task OnInitializedAsync() + { + await LoadNotificationsAsync(); + } + + private async Task LoadNotificationsAsync() + { + // Simulate loading notifications + await Task.Delay(1000); + notifications = await NotificationService.GetUnreadNotificationsAsync(); + + notifications = new List{ + new Notification { Id= Guid.NewGuid(), Title = "New message from John", Category = "Messages", Message = "Hey, can we meet tomorrow?", CreatedOn = DateTime.Now, IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "Your report is ready", Category = "Reports", Message = "Your monthly report is now available.", CreatedOn = DateTime.Now.AddDays(-1), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "System maintenance scheduled", Category = "System", Message = "System maintenance is scheduled for tonight at 11 PM.", CreatedOn = DateTime.Now.AddDays(-5), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "New comment on your post", Category = "Comments", Message = "Alice commented on your post.", CreatedOn = DateTime.Now.AddDays(-2), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "Password will expire soon", Category = "Security", Message = "Your password will expire in 3 days.", CreatedOn = DateTime.Now.AddDays(-3), IsRead = false } + }; + SortAndPaginateNotifications(); + + } + + private void SortTable(string column) + { + if (sortColumn == column) + { + sortAscending = !sortAscending; + } + else + { + sortColumn = column; + sortAscending = true; + } + SortAndPaginateNotifications(); + } + + private void SortAndPaginateNotifications() + { + // Sort + sortedNotifications = sortColumn switch + { + nameof(Notification.Title) => sortAscending + ? notifications.OrderBy(n => n.Title).ToList() + : notifications.OrderByDescending(n => n.Title).ToList(), + nameof(Notification.Category) => sortAscending + ? notifications.OrderBy(n => n.Category).ToList() + : notifications.OrderByDescending(n => n.Category).ToList(), + nameof(Notification.Message) => sortAscending + ? notifications.OrderBy(n => n.Message).ToList() + : notifications.OrderByDescending(n => n.Message).ToList(), + nameof(Notification.CreatedOn) => sortAscending + ? notifications.OrderBy(n => n.CreatedOn).ToList() + : notifications.OrderByDescending(n => n.CreatedOn).ToList(), + _ => notifications.OrderBy(n => n.CreatedOn).ToList() + }; + + // Paginate + totalRecords = sortedNotifications.Count; + totalPages = (int)Math.Ceiling(totalRecords / (double)pageSize); + currentPage = Math.Max(1, Math.Min(currentPage, totalPages)); + + pagedNotifications = sortedNotifications + .Skip((currentPage - 1) * pageSize) + .Take(pageSize) + .ToList(); + } + + private void UpdatePagination() + { + currentPage = 1; + SortAndPaginateNotifications(); + } + + private void FirstPage() => GoToPage(1); + private void LastPage() => GoToPage(totalPages); + private void NextPage() => GoToPage(currentPage + 1); + private void PreviousPage() => GoToPage(currentPage - 1); + + private void GoToPage(int page) + { + currentPage = Math.Max(1, Math.Min(page, totalPages)); + SortAndPaginateNotifications(); + } + + private void ViewNotification(Guid id){ + var notification = notifications.FirstOrDefault(n => n.Id == id); + if (notification != null) + { + // Implement the logic to view the notification details + } + } + + private void ToggleReadStatus(Guid id){ + var notification = notifications.FirstOrDefault(n => n.Id == id); + if (notification != null) + { + notification.IsRead = !notification.IsRead; + SortAndPaginateNotifications(); + } + } + + private void DeleteNotification(Guid id) + { + var notification = notifications.FirstOrDefault(n => n.Id == id); + if (notification != null) + { + notifications.Remove(notification); + SortAndPaginateNotifications(); + } + } + + private void BackToDashboard() + { + NavigationManager.NavigateTo("/"); + } + + // Modal Methods + private void OpenMessageModal(Guid id) + { + selectedNotification = notifications.FirstOrDefault(n => n.Id == id); + if (selectedNotification != null) + { + // Mark as read when opened + if (!selectedNotification.IsRead) + { + selectedNotification.IsRead = true; + selectedNotification.ReadOn = DateTime.UtcNow; + } + showMessageModal = true; + } + } + + private void CloseMessageModal() + { + showMessageModal = false; + selectedNotification = null; + SortAndPaginateNotifications(); + } + + private void DeleteCurrentNotification() + { + if (selectedNotification != null) + { + notifications.Remove(selectedNotification); + CloseMessageModal(); + } + } + + private void ViewRelatedEntity() + { + if (selectedNotification?.RelatedEntityId.HasValue == true) + { + var route = EntityRouteHelper.GetEntityRoute( + selectedNotification.RelatedEntityType, + selectedNotification.RelatedEntityId.Value); + NavigationManager.NavigateTo(route); + } + } + + // TODO: Implement when SenderId is added to Notification entity + // private void ReplyToMessage() + // { + // // Create new notification to sender + // } + + // TODO: Implement when SenderId is added to Notification entity + // private void ForwardMessage() + // { + // // Show user selection modal, then send to selected users + // } + + // Helper methods for badge colors + private string GetCategoryBadgeColor(string category) => category switch + { + "Lease" => "primary", + "Payment" => "success", + "Maintenance" => "warning", + "Application" => "info", + "Security" => "danger", + _ => "secondary" + }; + + private string GetTypeBadgeColor(string type) => type switch + { + "Info" => "info", + "Warning" => "warning", + "Error" => "danger", + "Success" => "success", + _ => "secondary" + }; +} \ No newline at end of file diff --git a/Aquiis.SimpleStart/Features/Notifications/Pages/NotificationPreferences.razor b/Aquiis.SimpleStart/Features/Notifications/Pages/NotificationPreferences.razor new file mode 100644 index 0000000..99acda5 --- /dev/null +++ b/Aquiis.SimpleStart/Features/Notifications/Pages/NotificationPreferences.razor @@ -0,0 +1 @@ +@page "/notifications/preferences" \ No newline at end of file diff --git a/Aquiis.SimpleStart/Infrastructure/Services/EntityRouteHelper.cs b/Aquiis.SimpleStart/Infrastructure/Services/EntityRouteHelper.cs new file mode 100644 index 0000000..d0c1e6b --- /dev/null +++ b/Aquiis.SimpleStart/Infrastructure/Services/EntityRouteHelper.cs @@ -0,0 +1,63 @@ +using Aquiis.SimpleStart.Core.Entities; + +namespace Aquiis.SimpleStart.Infrastructure.Services; + +/// +/// Provides centralized mapping between entity types and their navigation routes. +/// This ensures consistent URL generation across the application when navigating to entity details. +/// +public static class EntityRouteHelper +{ + private static readonly Dictionary RouteMap = new() + { + { "Lease", "/propertymanagement/leases/view" }, + { "Payment", "/propertymanagement/payments/view" }, + { "Invoice", "/propertymanagement/invoices/view" }, + { "Maintenance", "/propertymanagement/maintenance/view" }, + { "Application", "/propertymanagement/applications" }, + { "Property", "/propertymanagement/properties/edit" }, + { "Tenant", "/propertymanagement/tenants/view" }, + { "Prospect", "/PropertyManagement/ProspectiveTenants" } + }; + + /// + /// Gets the full navigation route for a given entity type and ID. + /// + /// The type of entity (e.g., "Lease", "Payment", "Maintenance") + /// The unique identifier of the entity + /// The full route path including the entity ID, or "/" if the entity type is not mapped + public static string GetEntityRoute(string? entityType, Guid entityId) + { + if (string.IsNullOrWhiteSpace(entityType)) + { + return "/"; + } + + if (RouteMap.TryGetValue(entityType, out var route)) + { + return $"{route}/{entityId}"; + } + + // Fallback to home if entity type not found + return "/"; + } + + /// + /// Checks if a route mapping exists for the given entity type. + /// + /// The type of entity to check + /// True if a route mapping exists, false otherwise + public static bool HasRoute(string? entityType) + { + return !string.IsNullOrWhiteSpace(entityType) && RouteMap.ContainsKey(entityType); + } + + /// + /// Gets all supported entity types that have route mappings. + /// + /// A collection of supported entity type names + public static IEnumerable GetSupportedEntityTypes() + { + return RouteMap.Keys; + } +} diff --git a/Aquiis.SimpleStart/Shared/Components/NotificationBell.razor b/Aquiis.SimpleStart/Shared/Components/NotificationBell.razor new file mode 100644 index 0000000..ecf02f3 --- /dev/null +++ b/Aquiis.SimpleStart/Shared/Components/NotificationBell.razor @@ -0,0 +1,295 @@ +@using Aquiis.SimpleStart.Infrastructure.Services +@inject NotificationService NotificationService +@inject NavigationManager NavigationManager +@rendermode InteractiveServer +@namespace Aquiis.SimpleStart.Shared.Components + +Notification Bell + +@if (isLoading) +{ +
+ +
+} +else if (notifications.Count > 0) +{ +
+} else { +
+ +
+} + + +@if (showNotificationModal && selectedNotification != null) +{ + +} + +@code { + private Notification? selectedNotification; + + private bool showNotificationModal = false; + + private bool isLoading = true; + private bool isDropdownOpen = false; + private int notificationCount = 0; + private List notifications = new List(); + + + protected override async Task OnInitializedAsync() + { + await LoadNotificationsAsync(); + } + + private async Task LoadNotificationsAsync() + { + isLoading = true; + notifications = await NotificationService.GetUnreadNotificationsAsync(); + notifications = notifications.OrderByDescending(n => n.CreatedOn).Take(5).ToList(); + notificationCount = notifications.Count; + + notifications = new List{ + new Notification { Id= Guid.NewGuid(), Title = "New message from John", Category = "Messages", Message = "Hey, can we meet tomorrow?", CreatedOn = DateTime.Now, IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "Your report is ready", Category = "Reports", Message = "Your monthly report is now available.", CreatedOn = DateTime.Now.AddDays(-1), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "System maintenance scheduled", Category = "System", Message = "System maintenance is scheduled for tonight at 11 PM.", CreatedOn = DateTime.Now.AddDays(-5), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "New comment on your post", Category = "Comments", Message = "Alice commented on your post.", CreatedOn = DateTime.Now.AddDays(-2), IsRead = false }, + new Notification { Id= Guid.NewGuid(), Title = "Password will expire soon", Category = "Security", Message = "Your password will expire in 3 days.", CreatedOn = DateTime.Now.AddDays(-3), IsRead = false } + }; + notificationCount = notifications.Count(n => !n.IsRead); + isLoading = false; + } + + private async Task ShowNotification(Notification notification) + { + selectedNotification = notification; + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + await NotificationService.MarkAsReadAsync(notification.Id); + notificationCount = notifications.Count(n => !n.IsRead); + showNotificationModal = true; + } + + private void CloseModal() + { + showNotificationModal = false; + selectedNotification = null; + } + + private void ViewRelatedEntity() + { + if (selectedNotification?.RelatedEntityId.HasValue == true) + { + var route = EntityRouteHelper.GetEntityRoute( + selectedNotification.RelatedEntityType, + selectedNotification.RelatedEntityId.Value); + NavigationManager.NavigateTo(route); + CloseModal(); + } + } + + private void ToggleDropdown() + { + isDropdownOpen = !isDropdownOpen; + } + + private async Task MarkAllAsRead() + { + foreach (var notification in notifications) + { + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + } + notificationCount = 0; + ToggleDropdown(); + StateHasChanged(); + } + + private void GoToNotificationCenter() + { + NavigationManager.NavigateTo("/notifications"); + } + + private string GetCategoryBadgeColor(string category) => category switch + { + "Lease" => "primary", + "Payment" => "success", + "Maintenance" => "warning", + "Application" => "info", + "Security" => "danger", + _ => "secondary" + }; + + private string GetTypeBadgeColor(string type) => type switch + { + "Info" => "info", + "Warning" => "warning", + "Error" => "danger", + "Success" => "success", + _ => "secondary" + }; +} + + \ No newline at end of file diff --git a/Aquiis.SimpleStart/Shared/Layout/MainLayout.razor b/Aquiis.SimpleStart/Shared/Layout/MainLayout.razor index 07cf069..92dd068 100644 --- a/Aquiis.SimpleStart/Shared/Layout/MainLayout.razor +++ b/Aquiis.SimpleStart/Shared/Layout/MainLayout.razor @@ -10,10 +10,15 @@
- About + + About + - +
+ + +
diff --git a/Aquiis.sln b/Aquiis.sln index be5fdad..ca456fe 100644 --- a/Aquiis.sln +++ b/Aquiis.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.5.2.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{C69B6EFE-BB20-41DE-8CBA-044207EBDCE1}") = "Aquiis.SimpleStart", "Aquiis.SimpleStart\Aquiis.SimpleStart.csproj", "{C69B6EFE-BB20-41DE-8CBA-044207EBDCE1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.Professional", "Aquiis.Professional\Aquiis.Professional.csproj", "{F51BA704-3BAC-F36D-5724-511615D4CBE5}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.SimpleStart.UI.Tests", "Aquiis.SimpleStart.UI.Tests\Aquiis.SimpleStart.UI.Tests.csproj", "{81401A51-BBA1-4B6B-8771-9C26A7B5356E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.SimpleStart.Tests", "Aquiis.SimpleStart.Tests\Aquiis.SimpleStart.Tests.csproj", "{D1111111-1111-4111-8111-111111111111}" @@ -32,6 +34,18 @@ Global {C69B6EFE-BB20-41DE-8CBA-044207EBDCE1}.Release|x64.Build.0 = Release|x64 {C69B6EFE-BB20-41DE-8CBA-044207EBDCE1}.Release|x86.ActiveCfg = Release|x86 {C69B6EFE-BB20-41DE-8CBA-044207EBDCE1}.Release|x86.Build.0 = Release|x86 + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Debug|x64.ActiveCfg = Debug|x64 + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Debug|x64.Build.0 = Debug|x64 + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Debug|x86.ActiveCfg = Debug|x86 + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Debug|x86.Build.0 = Debug|x86 + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Release|Any CPU.Build.0 = Release|Any CPU + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Release|x64.ActiveCfg = Release|x64 + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Release|x64.Build.0 = Release|x64 + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Release|x86.ActiveCfg = Release|x86 + {F51BA704-3BAC-F36D-5724-511615D4CBE5}.Release|x86.Build.0 = Release|x86 {D1111111-1111-4111-8111-111111111111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D1111111-1111-4111-8111-111111111111}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1111111-1111-4111-8111-111111111111}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/copilot-review-to-backlog.sh b/copilot-review-to-backlog.sh new file mode 100755 index 0000000..6b5b5af --- /dev/null +++ b/copilot-review-to-backlog.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +# Script to extract GitHub Copilot PR review suggestions and append to BACKLOG.md +# Usage: ./copilot-review-to-backlog.sh + +set -e + +PR_NUMBER=$1 +# Use absolute path to BACKLOG.md +BACKLOG_FILE="$HOME/Documents/Orion/Projects/Aquiis/Roadmap/BACKLOG.md" + +if [ -z "$PR_NUMBER" ]; then + echo "Usage: $0 " + echo "Example: $0 123" + exit 1 +fi + +# Check if gh CLI is installed +if ! command -v gh &> /dev/null; then + echo "Error: GitHub CLI (gh) is not installed." + echo "Install with: sudo dnf install gh" + exit 1 +fi + +# Check if user is authenticated +if ! gh auth status &> /dev/null; then + echo "Error: Not authenticated with GitHub CLI" + echo "Run: gh auth login" + exit 1 +fi + +# Get repository info +REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) +PR_TITLE=$(gh pr view $PR_NUMBER --json title -q .title) +PR_URL=$(gh pr view $PR_NUMBER --json url -q .url) + +echo "Fetching Copilot review suggestions from PR #$PR_NUMBER..." + +# Debug: Check what review authors exist +echo "Debug: Checking review authors..." +gh pr view $PR_NUMBER --json reviews --jq '.reviews[].author.login' | sort -u + +# Fetch Copilot review comments (copilot-pull-request-reviewer is the actual bot name) +COMMENTS=$(gh api repos/$REPO/pulls/$PR_NUMBER/comments --jq ' + .[] + | select(.user.login == "copilot-pull-request-reviewer") + | "**File:** `\(.path)` (Line \(.line))\n\n\(.body)\n" +') + +# Fetch Copilot review body (overall review) +REVIEW=$(gh pr view $PR_NUMBER --json reviews --jq ' + .reviews[] + | select(.author.login == "copilot-pull-request-reviewer") + | .body +' | head -1) + +# If still no results, try fetching ALL review comments to see structure +if [ -z "$COMMENTS" ] && [ -z "$REVIEW" ]; then + echo "Debug: No Copilot reviews found. Checking all reviews..." + gh api repos/$REPO/pulls/$PR_NUMBER/reviews --jq '.[] | {author: .user.login, body: .body}' | head -20 +fi + +if [ -z "$COMMENTS" ] && [ -z "$REVIEW" ]; then + echo "No Copilot review suggestions found on PR #$PR_NUMBER" + exit 0 +fi + +# Prepare backlog entry +TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") +ENTRY=" +## GitHub Copilot Review - PR #$PR_NUMBER +**Date:** $TIMESTAMP +**PR:** [$PR_TITLE]($PR_URL) + +" + +if [ -n "$REVIEW" ]; then + ENTRY+="### Overall Review + +$REVIEW + +" +fi + +if [ -n "$COMMENTS" ]; then + ENTRY+="### Inline Suggestions + +$COMMENTS +" +fi + +ENTRY+="--- + +" + +# Append to BACKLOG.md +if [ ! -f "$BACKLOG_FILE" ]; then + echo "Error: BACKLOG.md not found at $BACKLOG_FILE" + exit 1 +fi + +echo "$ENTRY" >> "$BACKLOG_FILE" + +echo "✅ Copilot review suggestions appended to BACKLOG.md" +echo "📄 Review PR #$PR_NUMBER at: $PR_URL" +echo "📝 Backlog updated: $BACKLOG_FILE"