diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed4926f..ba7e82a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,5 +25,11 @@ jobs: - name: Build run: dotnet build Aquiis.sln --no-restore --configuration Release - - name: Run focused tests - run: dotnet test Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj --no-build --configuration Release --verbosity normal + - name: Test Core Layer + run: dotnet test 6-Tests/Aquiis.Core.Tests/Aquiis.Core.Tests.csproj --no-build --configuration Release --verbosity normal + + - name: Test Application Layer + run: dotnet test 6-Tests/Aquiis.Application.Tests/Aquiis.Application.Tests.csproj --no-build --configuration Release --verbosity normal + + - name: Test Shared UI Components (bUnit) + run: dotnet test 6-Tests/Aquiis.UI.Shared.Tests/Aquiis.UI.Shared.Tests.csproj --no-build --configuration Release --verbosity normal diff --git a/.gitignore b/.gitignore index 3c7ca04..c4e69aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ /* Ignore file for Aquiis projects */ -# Build outputs - ignore bin/obj in project folders -Aquiis.*/bin/ -Aquiis.*/obj/ +# Build outputs - ignore bin/obj in all project folders +**/bin/ +**/obj/ # Electron host - track the folder but ignore node_modules obj/Host/node_modules/ @@ -18,6 +18,8 @@ node_modules/ Data/Backups/** /Data/Backups/** +Data/Migrations/** +/Data/Migrations/** /Data/app* diff --git a/0-Aquiis.Core/Aquiis.Core.csproj b/0-Aquiis.Core/Aquiis.Core.csproj new file mode 100644 index 0000000..08bcc56 --- /dev/null +++ b/0-Aquiis.Core/Aquiis.Core.csproj @@ -0,0 +1,10 @@ + + + + net10.0 + enable + enable + 0.2.0 + + + diff --git a/0-Aquiis.Core/Constants/ApplicationConstants.cs b/0-Aquiis.Core/Constants/ApplicationConstants.cs new file mode 100644 index 0000000..e6f7437 --- /dev/null +++ b/0-Aquiis.Core/Constants/ApplicationConstants.cs @@ -0,0 +1,789 @@ +using System.Security.Cryptography.X509Certificates; + +namespace Aquiis.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"; + } + + + + } + public 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/0-Aquiis.Core/Constants/ApplicationSettings.cs b/0-Aquiis.Core/Constants/ApplicationSettings.cs new file mode 100644 index 0000000..e15454b --- /dev/null +++ b/0-Aquiis.Core/Constants/ApplicationSettings.cs @@ -0,0 +1,108 @@ +namespace Aquiis.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/0-Aquiis.Core/Constants/NotificationConstants.cs b/0-Aquiis.Core/Constants/NotificationConstants.cs new file mode 100644 index 0000000..6037c1c --- /dev/null +++ b/0-Aquiis.Core/Constants/NotificationConstants.cs @@ -0,0 +1,66 @@ + +namespace Aquiis.Core.Constants; +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 Lease = "Lease"; + public const string Payment = "Payment"; + public const string Maintenance = "Maintenance"; + public const string Application = "Application"; + public const string Property = "Property"; + public const string Inspection = "Inspection"; + public const string Document = "Document"; + public const string System = "System"; + public const string Security = "Security"; + public const string Message = "Message"; + public const string Note = "Note"; + public const string Report = "Report"; + public const string CalendarEvent = "Calendar Event"; + public const string Other = "Other"; + } + + 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/0-Aquiis.Core/Entities/ApplicationScreening.cs b/0-Aquiis.Core/Entities/ApplicationScreening.cs new file mode 100644 index 0000000..1c4a702 --- /dev/null +++ b/0-Aquiis.Core/Entities/ApplicationScreening.cs @@ -0,0 +1,68 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/BaseModel.cs b/0-Aquiis.Core/Entities/BaseModel.cs new file mode 100644 index 0000000..815be2f --- /dev/null +++ b/0-Aquiis.Core/Entities/BaseModel.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using Aquiis.Core.Interfaces; + +namespace Aquiis.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/0-Aquiis.Core/Entities/CalendarEvent.cs b/0-Aquiis.Core/Entities/CalendarEvent.cs new file mode 100644 index 0000000..b1c820c --- /dev/null +++ b/0-Aquiis.Core/Entities/CalendarEvent.cs @@ -0,0 +1,78 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/CalendarEventTypes.cs b/0-Aquiis.Core/Entities/CalendarEventTypes.cs new file mode 100644 index 0000000..6847697 --- /dev/null +++ b/0-Aquiis.Core/Entities/CalendarEventTypes.cs @@ -0,0 +1,66 @@ +namespace Aquiis.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/0-Aquiis.Core/Entities/CalendarSettings.cs b/0-Aquiis.Core/Entities/CalendarSettings.cs new file mode 100644 index 0000000..01d76d4 --- /dev/null +++ b/0-Aquiis.Core/Entities/CalendarSettings.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Checklist.cs b/0-Aquiis.Core/Entities/Checklist.cs new file mode 100644 index 0000000..ae23e63 --- /dev/null +++ b/0-Aquiis.Core/Entities/Checklist.cs @@ -0,0 +1,67 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/ChecklistItem.cs b/0-Aquiis.Core/Entities/ChecklistItem.cs new file mode 100644 index 0000000..fbae6bc --- /dev/null +++ b/0-Aquiis.Core/Entities/ChecklistItem.cs @@ -0,0 +1,56 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/ChecklistTemplate.cs b/0-Aquiis.Core/Entities/ChecklistTemplate.cs new file mode 100644 index 0000000..d60e781 --- /dev/null +++ b/0-Aquiis.Core/Entities/ChecklistTemplate.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs b/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs new file mode 100644 index 0000000..6c06555 --- /dev/null +++ b/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Document.cs b/0-Aquiis.Core/Entities/Document.cs new file mode 100644 index 0000000..4439541 --- /dev/null +++ b/0-Aquiis.Core/Entities/Document.cs @@ -0,0 +1,84 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.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/0-Aquiis.Core/Entities/ISchedulableEntity.cs b/0-Aquiis.Core/Entities/ISchedulableEntity.cs new file mode 100644 index 0000000..d81511a --- /dev/null +++ b/0-Aquiis.Core/Entities/ISchedulableEntity.cs @@ -0,0 +1,64 @@ +namespace Aquiis.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/0-Aquiis.Core/Entities/IncomeStatement.cs b/0-Aquiis.Core/Entities/IncomeStatement.cs new file mode 100644 index 0000000..24be02f --- /dev/null +++ b/0-Aquiis.Core/Entities/IncomeStatement.cs @@ -0,0 +1,107 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Inspection.cs b/0-Aquiis.Core/Entities/Inspection.cs new file mode 100644 index 0000000..746d043 --- /dev/null +++ b/0-Aquiis.Core/Entities/Inspection.cs @@ -0,0 +1,153 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Invoice.cs b/0-Aquiis.Core/Entities/Invoice.cs new file mode 100644 index 0000000..ab7da0b --- /dev/null +++ b/0-Aquiis.Core/Entities/Invoice.cs @@ -0,0 +1,78 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Lease.cs b/0-Aquiis.Core/Entities/Lease.cs new file mode 100644 index 0000000..3dc23f3 --- /dev/null +++ b/0-Aquiis.Core/Entities/Lease.cs @@ -0,0 +1,113 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/LeaseOffer.cs b/0-Aquiis.Core/Entities/LeaseOffer.cs new file mode 100644 index 0000000..05046eb --- /dev/null +++ b/0-Aquiis.Core/Entities/LeaseOffer.cs @@ -0,0 +1,71 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.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/0-Aquiis.Core/Entities/MaintenanceRequest.cs b/0-Aquiis.Core/Entities/MaintenanceRequest.cs new file mode 100644 index 0000000..1d4a508 --- /dev/null +++ b/0-Aquiis.Core/Entities/MaintenanceRequest.cs @@ -0,0 +1,145 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Note.cs b/0-Aquiis.Core/Entities/Note.cs new file mode 100644 index 0000000..0575a23 --- /dev/null +++ b/0-Aquiis.Core/Entities/Note.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.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; } + + // CreatedBy (UserId) comes from BaseModel - no navigation property needed in Core + // Individual projects can add navigation properties to their ApplicationUser if needed + + // public partial class Note + // { + // [ForeignKey(nameof(CreatedBy))] + // public virtual ApplicationUser? User { get; set; } + // } + } +} diff --git a/0-Aquiis.Core/Entities/Notification.cs b/0-Aquiis.Core/Entities/Notification.cs new file mode 100644 index 0000000..f24b994 --- /dev/null +++ b/0-Aquiis.Core/Entities/Notification.cs @@ -0,0 +1,64 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Entities; +using Aquiis.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/0-Aquiis.Core/Entities/NotificationPreferences.cs b/0-Aquiis.Core/Entities/NotificationPreferences.cs new file mode 100644 index 0000000..2dd9626 --- /dev/null +++ b/0-Aquiis.Core/Entities/NotificationPreferences.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/OperationResult.cs b/0-Aquiis.Core/Entities/OperationResult.cs new file mode 100644 index 0000000..d3fa243 --- /dev/null +++ b/0-Aquiis.Core/Entities/OperationResult.cs @@ -0,0 +1,24 @@ +namespace Aquiis.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/0-Aquiis.Core/Entities/Organization.cs b/0-Aquiis.Core/Entities/Organization.cs new file mode 100644 index 0000000..d1521e9 --- /dev/null +++ b/0-Aquiis.Core/Entities/Organization.cs @@ -0,0 +1,49 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs b/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs new file mode 100644 index 0000000..6188ffc --- /dev/null +++ b/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs @@ -0,0 +1,72 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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; } + + public string ProviderName { get; set; } = "SMTP"; + + public string SmtpServer { get; set; } = string.Empty; + public int SmtpPort { get; set; } = 587; + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public bool EnableSsl { get; set; } = true; + + // 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/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs b/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs new file mode 100644 index 0000000..896fed1 --- /dev/null +++ b/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs @@ -0,0 +1,68 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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; } + + [StringLength(100)] + public string? ProviderName { 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/0-Aquiis.Core/Entities/OrganizationSettings.cs b/0-Aquiis.Core/Entities/OrganizationSettings.cs new file mode 100644 index 0000000..fed6227 --- /dev/null +++ b/0-Aquiis.Core/Entities/OrganizationSettings.cs @@ -0,0 +1,130 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Payment.cs b/0-Aquiis.Core/Entities/Payment.cs new file mode 100644 index 0000000..614580e --- /dev/null +++ b/0-Aquiis.Core/Entities/Payment.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Property.cs b/0-Aquiis.Core/Entities/Property.cs new file mode 100644 index 0000000..d6164cb --- /dev/null +++ b/0-Aquiis.Core/Entities/Property.cs @@ -0,0 +1,188 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using Aquiis.Core.Constants; + +namespace Aquiis.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/0-Aquiis.Core/Entities/ProspectiveTenant.cs b/0-Aquiis.Core/Entities/ProspectiveTenant.cs new file mode 100644 index 0000000..2d72942 --- /dev/null +++ b/0-Aquiis.Core/Entities/ProspectiveTenant.cs @@ -0,0 +1,87 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.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/0-Aquiis.Core/Entities/RentalApplication.cs b/0-Aquiis.Core/Entities/RentalApplication.cs new file mode 100644 index 0000000..f7feb86 --- /dev/null +++ b/0-Aquiis.Core/Entities/RentalApplication.cs @@ -0,0 +1,161 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Aquiis.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/0-Aquiis.Core/Entities/SchemaVersion.cs b/0-Aquiis.Core/Entities/SchemaVersion.cs new file mode 100644 index 0000000..8470257 --- /dev/null +++ b/0-Aquiis.Core/Entities/SchemaVersion.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Aquiis.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/0-Aquiis.Core/Entities/SecurityDeposit.cs b/0-Aquiis.Core/Entities/SecurityDeposit.cs new file mode 100644 index 0000000..6e645eb --- /dev/null +++ b/0-Aquiis.Core/Entities/SecurityDeposit.cs @@ -0,0 +1,100 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Aquiis.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/0-Aquiis.Core/Entities/SecurityDepositDividend.cs b/0-Aquiis.Core/Entities/SecurityDepositDividend.cs new file mode 100644 index 0000000..47e418c --- /dev/null +++ b/0-Aquiis.Core/Entities/SecurityDepositDividend.cs @@ -0,0 +1,114 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Aquiis.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/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs b/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs new file mode 100644 index 0000000..f5417cb --- /dev/null +++ b/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs @@ -0,0 +1,108 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Tenant.cs b/0-Aquiis.Core/Entities/Tenant.cs new file mode 100644 index 0000000..b71b70c --- /dev/null +++ b/0-Aquiis.Core/Entities/Tenant.cs @@ -0,0 +1,57 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/Tour.cs b/0-Aquiis.Core/Entities/Tour.cs new file mode 100644 index 0000000..948f72d --- /dev/null +++ b/0-Aquiis.Core/Entities/Tour.cs @@ -0,0 +1,75 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/UserOrganization.cs b/0-Aquiis.Core/Entities/UserOrganization.cs new file mode 100644 index 0000000..9789e98 --- /dev/null +++ b/0-Aquiis.Core/Entities/UserOrganization.cs @@ -0,0 +1,67 @@ + + +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Validation; + +namespace Aquiis.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/0-Aquiis.Core/Entities/WorkflowAuditLog.cs b/0-Aquiis.Core/Entities/WorkflowAuditLog.cs new file mode 100644 index 0000000..0bf156f --- /dev/null +++ b/0-Aquiis.Core/Entities/WorkflowAuditLog.cs @@ -0,0 +1,57 @@ + +namespace Aquiis.Core.Entities +{ + /// + /// 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/0-Aquiis.Core/Interfaces/IApplicationDbContext.cs b/0-Aquiis.Core/Interfaces/IApplicationDbContext.cs new file mode 100644 index 0000000..7938a99 --- /dev/null +++ b/0-Aquiis.Core/Interfaces/IApplicationDbContext.cs @@ -0,0 +1,13 @@ +namespace Aquiis.Core.Interfaces; + +/// +/// Platform-agnostic interface for the application's database context. +/// This allows the Application layer to reference the DbContext without knowing Infrastructure details. +/// +public interface IApplicationDbContext +{ + /// + /// Saves all changes made in this context to the database. + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/0-Aquiis.Core/Interfaces/IAuditable.cs b/0-Aquiis.Core/Interfaces/IAuditable.cs new file mode 100644 index 0000000..531305f --- /dev/null +++ b/0-Aquiis.Core/Interfaces/IAuditable.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.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/0-Aquiis.Core/Interfaces/ICalendarEventService.cs b/0-Aquiis.Core/Interfaces/ICalendarEventService.cs new file mode 100644 index 0000000..d2d4af6 --- /dev/null +++ b/0-Aquiis.Core/Interfaces/ICalendarEventService.cs @@ -0,0 +1,21 @@ +using Aquiis.Core.Entities; + +namespace Aquiis.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/0-Aquiis.Core/Interfaces/IPathService.cs b/0-Aquiis.Core/Interfaces/IPathService.cs new file mode 100644 index 0000000..f2bc1a6 --- /dev/null +++ b/0-Aquiis.Core/Interfaces/IPathService.cs @@ -0,0 +1,28 @@ +namespace Aquiis.Core.Interfaces; + +/// +/// Platform-agnostic interface for managing application paths and connection strings. +/// Implementations exist for Electron, Web, and future mobile platforms. +/// +public interface IPathService +{ + /// + /// Gets the connection string for the database, using platform-specific paths. + /// + Task GetConnectionStringAsync(object configuration); + + /// + /// Gets the platform-specific database file path. + /// + Task GetDatabasePathAsync(); + + /// + /// Gets the platform-specific user data directory path. + /// + Task GetUserDataPathAsync(); + + /// + /// Checks if the application is running in the current platform context. + /// + bool IsActive { get; } +} diff --git a/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs b/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs new file mode 100644 index 0000000..380c5fd --- /dev/null +++ b/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs @@ -0,0 +1,28 @@ +namespace Aquiis.Core.Interfaces.Services; + +/// +/// Service for database initialization and management operations. +/// Abstracts database access from product layers. +/// +public interface IDatabaseService +{ + /// + /// Initialize database (apply pending migrations for both business and identity contexts) + /// + Task InitializeAsync(); + + /// + /// Check if database can be connected to + /// + Task CanConnectAsync(); + + /// + /// Get count of pending migrations for business context + /// + Task GetPendingMigrationsCountAsync(); + + /// + /// Get count of pending migrations for identity context + /// + Task GetIdentityPendingMigrationsCountAsync(); +} diff --git a/0-Aquiis.Core/Interfaces/Services/IEmailService.cs b/0-Aquiis.Core/Interfaces/Services/IEmailService.cs new file mode 100644 index 0000000..f5169cd --- /dev/null +++ b/0-Aquiis.Core/Interfaces/Services/IEmailService.cs @@ -0,0 +1,9 @@ + +namespace Aquiis.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/0-Aquiis.Core/Interfaces/Services/ISMSService.cs b/0-Aquiis.Core/Interfaces/Services/ISMSService.cs new file mode 100644 index 0000000..bea70cf --- /dev/null +++ b/0-Aquiis.Core/Interfaces/Services/ISMSService.cs @@ -0,0 +1,7 @@ + +namespace Aquiis.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/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs b/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs new file mode 100644 index 0000000..78ad844 --- /dev/null +++ b/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs @@ -0,0 +1,33 @@ +namespace Aquiis.Core.Interfaces.Services; + +/// +/// Service interface for accessing user context information. +/// Implementations are project-specific and depend on ApplicationDbContext. +/// +public interface IUserContextService +{ + /// + /// Gets the current authenticated user's ID. + /// + Task GetUserIdAsync(); + + /// + /// Gets the current user's active organization ID. + /// + Task GetActiveOrganizationIdAsync(); + + /// + /// Gets the current authenticated user's full name. + /// + Task GetUserNameAsync(); + + /// + /// Gets the current user's email address. + /// + Task GetUserEmailAsync(); + + /// + /// Gets the current user's OrganizationId (DEPRECATED: Use GetActiveOrganizationIdAsync). + /// + Task GetOrganizationIdAsync(); +} \ No newline at end of file diff --git a/0-Aquiis.Core/Utilities/CalendarEventRouter.cs b/0-Aquiis.Core/Utilities/CalendarEventRouter.cs new file mode 100644 index 0000000..05050c5 --- /dev/null +++ b/0-Aquiis.Core/Utilities/CalendarEventRouter.cs @@ -0,0 +1,59 @@ +using Aquiis.Core.Entities; + +namespace Aquiis.Core.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/0-Aquiis.Core/Utilities/SchedulableEntityRegistry.cs b/0-Aquiis.Core/Utilities/SchedulableEntityRegistry.cs new file mode 100644 index 0000000..7c84f8a --- /dev/null +++ b/0-Aquiis.Core/Utilities/SchedulableEntityRegistry.cs @@ -0,0 +1,87 @@ +using System.Reflection; +using Aquiis.Core.Entities; + +namespace Aquiis.Core.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/0-Aquiis.Core/Validation/OptionalGuidAttribute.cs b/0-Aquiis.Core/Validation/OptionalGuidAttribute.cs new file mode 100644 index 0000000..c6d3bef --- /dev/null +++ b/0-Aquiis.Core/Validation/OptionalGuidAttribute.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.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/0-Aquiis.Core/Validation/RequiredGuidAttribute.cs b/0-Aquiis.Core/Validation/RequiredGuidAttribute.cs new file mode 100644 index 0000000..f91835e --- /dev/null +++ b/0-Aquiis.Core/Validation/RequiredGuidAttribute.cs @@ -0,0 +1,83 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.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/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj b/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj new file mode 100644 index 0000000..cd859f5 --- /dev/null +++ b/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + enable + enable + 0.2.0 + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/1-Aquiis.Infrastructure/Core/Services/SendGridEmailService.cs b/1-Aquiis.Infrastructure/Core/Services/SendGridEmailService.cs new file mode 100644 index 0000000..52cffc8 --- /dev/null +++ b/1-Aquiis.Infrastructure/Core/Services/SendGridEmailService.cs @@ -0,0 +1,276 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Infrastructure.Data; +using Aquiis.Infrastructure.Interfaces; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SendGrid; +using SendGrid.Helpers.Mail; + +namespace Aquiis.Infrastructure.Core.Services +{ + public class SendGridEmailService : IEmailService, IEmailProvider + { + private readonly ApplicationDbContext _context; + private readonly IUserContextService _userContext; + private readonly ILogger _logger; + private readonly IDataProtectionProvider _dataProtection; + + private const string PROTECTION_PURPOSE = "SendGridApiKey"; + + public SendGridEmailService( + ApplicationDbContext context, + IUserContextService 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(); + } + + public 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/1-Aquiis.Infrastructure/Core/Services/TwilioSMSService.cs b/1-Aquiis.Infrastructure/Core/Services/TwilioSMSService.cs new file mode 100644 index 0000000..28c9c55 --- /dev/null +++ b/1-Aquiis.Infrastructure/Core/Services/TwilioSMSService.cs @@ -0,0 +1,244 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Infrastructure.Data; +using Aquiis.Infrastructure.Interfaces; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Twilio; +using Twilio.Rest.Api.V2010.Account; +using Twilio.Types; + +namespace Aquiis.Infrastructure.Core.Services +{ + public class TwilioSMSService : ISMSService, ISMSProvider + { + private readonly ApplicationDbContext _context; + private readonly IUserContextService _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, + IUserContextService 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); + } + + // ISMSProvider implementation + public async Task VerifyCredentialsAsync(string accountSid, string authToken) + { + // Use a dummy phone number for verification - just checking credentials work + return await VerifyTwilioCredentialsAsync(accountSid, authToken, string.Empty); + } + + public string EncryptCredential(string credential) + { + // Generic encryption using AUTH_TOKEN_PURPOSE + var protector = _dataProtection.CreateProtector(AUTH_TOKEN_PURPOSE); + return protector.Protect(credential); + } + + public string DecryptCredential(string encryptedCredential) + { + var protector = _dataProtection.CreateProtector(AUTH_TOKEN_PURPOSE); + return protector.Unprotect(encryptedCredential); + } + } + + + + 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/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..825dad4 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs @@ -0,0 +1,737 @@ +using Microsoft.EntityFrameworkCore; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces; + +namespace Aquiis.Infrastructure.Data +{ + /// + /// Main application database context for business entities only. + /// Identity management is handled by product-specific contexts. + /// Products can extend via partial classes following the Portable Feature pattern. + /// + public partial class ApplicationDbContext : DbContext, IApplicationDbContext + { + 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); + + // OwnerId is a string foreign key to AspNetUsers (managed by SimpleStartDbContext) + // No navigation property configured here + }); + + // 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); + + // UserId and GrantedBy are string foreign keys to AspNetUsers (managed by SimpleStartDbContext) + // No navigation properties configured here + + // 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); + + // RecipientUserId is a string foreign key to AspNetUsers (managed by SimpleStartDbContext) + // No navigation property configured here + }); + + // 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); + + // UserId is a string foreign key to AspNetUsers (managed by SimpleStartDbContext) + // No navigation property configured here + }); + + // 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 } + ); + + // Call partial method hook for features to add custom configuration + OnModelCreatingPartial(modelBuilder); + } + + /// + /// Partial method hook for features to add custom model configuration. + /// Features can implement this in their partial ApplicationDbContext classes. + /// + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + } +} \ No newline at end of file diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs new file mode 100644 index 0000000..7cc048d --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs @@ -0,0 +1,4033 @@ +// +using System; +using Aquiis.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260104205822_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.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.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.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.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.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.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.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.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.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.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.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.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.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.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.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.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.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.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("EnableSsl") + .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("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendGridApiKeyEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SmtpPort") + .HasColumnType("INTEGER"); + + b.Property("SmtpServer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationEmailSettings"); + }); + + modelBuilder.Entity("Aquiis.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("ProviderName") + .HasMaxLength(100) + .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.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.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.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.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.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.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.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.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.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.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.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.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("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.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("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("Aquiis.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.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.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.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.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.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.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.UserOrganization", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("UserOrganizations") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + + b.Navigation("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.cs new file mode 100644 index 0000000..5257751 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.cs @@ -0,0 +1,2066 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Aquiis.Infrastructure.Data.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CalendarSettings", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", 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", 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: "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); + }); + + migrationBuilder.CreateTable( + name: "Organizations", + columns: table => new + { + Id = table.Column(type: "TEXT", 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); + }); + + 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: "ChecklistTemplateItems", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", 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: "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_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_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrganizationEmailSettings", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", nullable: false), + ProviderName = table.Column(type: "TEXT", nullable: false), + SmtpServer = table.Column(type: "TEXT", nullable: false), + SmtpPort = table.Column(type: "INTEGER", nullable: false), + Username = table.Column(type: "TEXT", nullable: false), + Password = table.Column(type: "TEXT", nullable: false), + EnableSsl = table.Column(type: "INTEGER", 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), + ProviderName = table.Column(type: "TEXT", maxLength: 100, nullable: true), + 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.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", 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_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", 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", 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", 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", 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", 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", 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), + RenewalNumber = table.Column(type: "INTEGER", nullable: false), + TerminationNoticedOn = table.Column(type: "TEXT", nullable: true), + ExpectedMoveOutDate = table.Column(type: "TEXT", nullable: true), + ActualMoveOutDate = table.Column(type: "TEXT", nullable: true), + TerminationReason = table.Column(type: "TEXT", maxLength: 500, 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", 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, null, new Guid("00000000-0000-0000-0000-000000000000"), false, 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, null, new Guid("00000000-0000-0000-0000-000000000000"), false, 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, 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, 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, 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, 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, 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, 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, null, new Guid("00000000-0000-0000-0000-000000000000"), false, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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_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_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"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationEmailSettings_OrganizationId", + table: "OrganizationEmailSettings", + column: "OrganizationId", + unique: true); + + 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_OrganizationSMSSettings_OrganizationId", + table: "OrganizationSMSSettings", + 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"); + + 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_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_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: "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: "NotificationPreferences"); + + migrationBuilder.DropTable( + name: "Notifications"); + + migrationBuilder.DropTable( + name: "OrganizationEmailSettings"); + + migrationBuilder.DropTable( + name: "OrganizationSettings"); + + migrationBuilder.DropTable( + name: "OrganizationSMSSettings"); + + migrationBuilder.DropTable( + name: "SchemaVersions"); + + migrationBuilder.DropTable( + name: "SecurityDepositDividends"); + + migrationBuilder.DropTable( + name: "Tours"); + + migrationBuilder.DropTable( + name: "UserOrganizations"); + + migrationBuilder.DropTable( + name: "WorkflowAuditLogs"); + + 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: "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/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..5eb5864 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,4030 @@ +// +using System; +using Aquiis.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Infrastructure.Data.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.ToTable("Notes"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.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.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.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("EnableSsl") + .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("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProviderName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SendGridApiKeyEncrypted") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("SmtpPort") + .HasColumnType("INTEGER"); + + b.Property("SmtpServer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatsLastUpdatedOn") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationEmailSettings"); + }); + + modelBuilder.Entity("Aquiis.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("ProviderName") + .HasMaxLength(100) + .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.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.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.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.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.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.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.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.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.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.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.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.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("IsActive"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Role"); + + b.HasIndex("UserId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.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("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("Aquiis.Core.Entities.ApplicationScreening", b => + { + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithOne("Screening") + .HasForeignKey("Aquiis.Core.Entities.ApplicationScreening", "RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.CalendarEvent", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Checklists") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.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.Core.Entities.ChecklistItem", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany("Items") + .HasForeignKey("ChecklistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Checklist"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplateItem", b => + { + b.HasOne("Aquiis.Core.Entities.ChecklistTemplate", "ChecklistTemplate") + .WithMany("Items") + .HasForeignKey("ChecklistTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChecklistTemplate"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Document", b => + { + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Documents") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Payment", "Payment") + .WithMany() + .HasForeignKey("PaymentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Documents") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.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.Core.Entities.Inspection", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany("Invoices") + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Lease"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Leases") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Leases") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany("Leases") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Property"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.LeaseOffer", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany() + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.RentalApplication", "RentalApplication") + .WithMany() + .HasForeignKey("RentalApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + + b.Navigation("RentalApplication"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.MaintenanceRequest", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Property"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.NotificationPreferences", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationEmailSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationSMSSettings", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => + { + b.HasOne("Aquiis.Core.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Properties") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "InterestedProperty") + .WithMany() + .HasForeignKey("InterestedPropertyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("InterestedProperty"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Applications") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositDividend", b => + { + b.HasOne("Aquiis.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") + .WithMany("Dividends") + .HasForeignKey("InvestmentPoolId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.SecurityDeposit", "SecurityDeposit") + .WithMany("Dividends") + .HasForeignKey("SecurityDepositId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.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.Core.Entities.Tenant", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany("Tenants") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tour", b => + { + b.HasOne("Aquiis.Core.Entities.Checklist", "Checklist") + .WithMany() + .HasForeignKey("ChecklistId"); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany() + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Aquiis.Core.Entities.ProspectiveTenant", "ProspectiveTenant") + .WithMany("Tours") + .HasForeignKey("ProspectiveTenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Checklist"); + + b.Navigation("Property"); + + b.Navigation("ProspectiveTenant"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.UserOrganization", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("UserOrganizations") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Notification", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Checklist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ChecklistTemplate", b => + { + b.Navigation("Checklists"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Lease", b => + { + b.Navigation("Documents"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + + b.Navigation("UserOrganizations"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => + { + b.Navigation("Applications"); + + b.Navigation("Tours"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.RentalApplication", b => + { + b.Navigation("Screening"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDeposit", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.SecurityDepositInvestmentPool", b => + { + b.Navigation("Dividends"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Tenant", b => + { + b.Navigation("Leases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/1-Aquiis.Infrastructure/DependencyInjection.cs b/1-Aquiis.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..9616845 --- /dev/null +++ b/1-Aquiis.Infrastructure/DependencyInjection.cs @@ -0,0 +1,40 @@ +using Aquiis.Infrastructure.Data; +using Aquiis.Infrastructure.Interfaces; +using Aquiis.Infrastructure.Core.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Aquiis.Infrastructure; + +public static class DependencyInjection +{ + /// + /// Register all Infrastructure services and data access. + /// Called internally by Application layer - products should NOT call this directly. + /// + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + string connectionString) + { + // Register ApplicationDbContext (business data) + services.AddDbContext(options => + options.UseSqlite(connectionString)); + + // Register DbContext factory for services that need it (like FinancialReportService) + // Use AddDbContextFactory instead of AddPooledDbContextFactory to avoid lifetime issues + services.AddDbContextFactory(options => + options.UseSqlite(connectionString), + ServiceLifetime.Scoped); + + // Register provider interfaces + services.AddScoped(); + services.AddScoped(sp => + sp.GetRequiredService()); + + services.AddScoped(); + services.AddScoped(sp => + sp.GetRequiredService()); + + return services; + } +} diff --git a/1-Aquiis.Infrastructure/Interfaces/IEmailProvider.cs b/1-Aquiis.Infrastructure/Interfaces/IEmailProvider.cs new file mode 100644 index 0000000..25ce9ff --- /dev/null +++ b/1-Aquiis.Infrastructure/Interfaces/IEmailProvider.cs @@ -0,0 +1,38 @@ +namespace Aquiis.Infrastructure.Interfaces; + +/// +/// Interface for email provider implementations (SendGrid, SMTP, etc.) +/// Used by EmailSettingsService to manage provider-specific configuration. +/// +public interface IEmailProvider +{ + /// + /// Verify that an API key is valid and has proper permissions + /// + Task VerifyApiKeyAsync(string apiKey); + + /// + /// Encrypt an API key for secure storage + /// + string EncryptApiKey(string apiKey); + + /// + /// Decrypt an API key for use + /// + string DecryptApiKey(string encryptedApiKey); + + /// + /// Send an email using this provider + /// + Task SendEmailAsync(string to, string subject, string body, string? fromName = null); + + /// + /// Send a templated email using this provider + /// + Task SendTemplateEmailAsync(string to, string templateId, Dictionary templateData); + + /// + /// Validate an email address format + /// + Task ValidateEmailAddressAsync(string email); +} diff --git a/1-Aquiis.Infrastructure/Interfaces/ISMSProvider.cs b/1-Aquiis.Infrastructure/Interfaces/ISMSProvider.cs new file mode 100644 index 0000000..52382d6 --- /dev/null +++ b/1-Aquiis.Infrastructure/Interfaces/ISMSProvider.cs @@ -0,0 +1,33 @@ +namespace Aquiis.Infrastructure.Interfaces; + +/// +/// Interface for SMS provider implementations (Twilio, etc.) +/// Used by SMSSettingsService to manage provider-specific configuration. +/// +public interface ISMSProvider +{ + /// + /// Verify that credentials are valid and have proper permissions + /// + Task VerifyCredentialsAsync(string accountSid, string authToken); + + /// + /// Encrypt credentials for secure storage + /// + string EncryptCredential(string credential); + + /// + /// Decrypt credentials for use + /// + string DecryptCredential(string encryptedCredential); + + /// + /// Send an SMS using this provider + /// + Task SendSMSAsync(string to, string message); + + /// + /// Validate a phone number format + /// + Task ValidatePhoneNumberAsync(string phoneNumber); +} diff --git a/2-Aquiis.Application/Aquiis.Application.csproj b/2-Aquiis.Application/Aquiis.Application.csproj new file mode 100644 index 0000000..f8594ce --- /dev/null +++ b/2-Aquiis.Application/Aquiis.Application.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + 0.2.0 + + + + + + + + + + + + + diff --git a/2-Aquiis.Application/Aquiis.Application.sln b/2-Aquiis.Application/Aquiis.Application.sln new file mode 100644 index 0000000..a8eb02e --- /dev/null +++ b/2-Aquiis.Application/Aquiis.Application.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.Application", "Aquiis.Application.csproj", "{9C13D459-27C6-2232-B1AA-B80FB8F9C47A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9C13D459-27C6-2232-B1AA-B80FB8F9C47A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C13D459-27C6-2232-B1AA-B80FB8F9C47A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C13D459-27C6-2232-B1AA-B80FB8F9C47A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C13D459-27C6-2232-B1AA-B80FB8F9C47A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {44243B58-7CCF-4956-91BF-A977807858DF} + EndGlobalSection +EndGlobal diff --git a/2-Aquiis.Application/Constants/AccountConstants.cs b/2-Aquiis.Application/Constants/AccountConstants.cs new file mode 100644 index 0000000..7f2b447 --- /dev/null +++ b/2-Aquiis.Application/Constants/AccountConstants.cs @@ -0,0 +1,13 @@ +namespace Aquiis.Application.Constants +{ + 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/2-Aquiis.Application/Constants/EntityTypeNames.cs b/2-Aquiis.Application/Constants/EntityTypeNames.cs new file mode 100644 index 0000000..baa4521 --- /dev/null +++ b/2-Aquiis.Application/Constants/EntityTypeNames.cs @@ -0,0 +1,65 @@ +using Aquiis.Core.Entities; + +namespace Aquiis.Application.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.Core.Entities.Property"; + public const string Tenant = "Aquiis.Core.Entities.Tenant"; + public const string Lease = "Aquiis.Core.Entities.Lease"; + public const string LeaseOffer = "Aquiis.Core.Entities.LeaseOffer"; + public const string Invoice = "Aquiis.Core.Entities.Invoice"; + public const string Payment = "Aquiis.Core.Entities.Payment"; + public const string MaintenanceRequest = "Aquiis.Core.Entities.MaintenanceRequest"; + public const string Inspection = "Aquiis.Core.Entities.Inspection"; + public const string Document = "Aquiis.Core.Entities.Document"; + + // Application/Prospect Domain + public const string ProspectiveTenant = "Aquiis.Core.Entities.ProspectiveTenant"; + public const string Application = "Aquiis.Core.Entities.Application"; + public const string Tour = "Aquiis.Core.Entities.Tour"; + + // Checklist Domain + public const string Checklist = "Aquiis.Core.Entities.Checklist"; + public const string ChecklistTemplate = "Aquiis.Core.Entities.ChecklistTemplate"; + + // Calendar/Events + public const string CalendarEvent = "Aquiis.Core.Entities.CalendarEvent"; + + // Security Deposits + public const string SecurityDepositPool = "Aquiis.Core.Entities.SecurityDepositPool"; + public const string SecurityDepositTransaction = "Aquiis.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/2-Aquiis.Application/DependencyInjection.cs b/2-Aquiis.Application/DependencyInjection.cs new file mode 100644 index 0000000..4b2848a --- /dev/null +++ b/2-Aquiis.Application/DependencyInjection.cs @@ -0,0 +1,60 @@ +using Aquiis.Application.Services; +using Aquiis.Application.Services.Workflows; +using Aquiis.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Aquiis.Application; + +public static class DependencyInjection +{ + /// + /// Register Application layer services and Infrastructure internally. + /// This is the ONLY method products should call for dependency registration. + /// Note: IDatabaseService must be registered by the product layer since it requires + /// the product-specific Identity context (e.g., SimpleStartDbContext). + /// + public static IServiceCollection AddApplication( + this IServiceCollection services, + string connectionString) + { + // Call Infrastructure registration internally + services.AddInfrastructure(connectionString); + + // Register all Application services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/2-Aquiis.Application/GlobalUsings.cs b/2-Aquiis.Application/GlobalUsings.cs new file mode 100644 index 0000000..332ce0a --- /dev/null +++ b/2-Aquiis.Application/GlobalUsings.cs @@ -0,0 +1,6 @@ +// Global usings for Application layer +// ApplicationUser removed - now product-specific +// Application layer uses ApplicationDbContext for business data only +global using ApplicationDbContext = Aquiis.Infrastructure.Data.ApplicationDbContext; +global using SendGridEmailService = Aquiis.Infrastructure.Core.Services.SendGridEmailService; +global using TwilioSMSService = Aquiis.Infrastructure.Core.Services.TwilioSMSService; diff --git a/2-Aquiis.Application/Models/EmailConfigurationModel.cs b/2-Aquiis.Application/Models/EmailConfigurationModel.cs new file mode 100644 index 0000000..13d1726 --- /dev/null +++ b/2-Aquiis.Application/Models/EmailConfigurationModel.cs @@ -0,0 +1,68 @@ +namespace Aquiis.Application.Models; + +/// +/// View model for email configuration in the UI. +/// Combines settings from OrganizationEmailSettings for display and editing. +/// +public class EmailConfigurationModel +{ + /// + /// SendGrid API Key (unencrypted for display/editing) + /// + public string ApiKey { get; set; } = string.Empty; + + /// + /// Email address to send from + /// + public string FromEmail { get; set; } = string.Empty; + + /// + /// Display name for sender + /// + public string FromName { get; set; } = string.Empty; + + /// + /// Whether email is currently enabled + /// + public bool IsEnabled { get; set; } + + /// + /// Whether credentials have been verified + /// + public bool IsVerified { get; set; } + + /// + /// Daily email limit + /// + public int DailyLimit { get; set; } + + /// + /// Monthly email limit + /// + public int MonthlyLimit { get; set; } + + /// + /// Emails sent today + /// + public int EmailsSentToday { get; set; } + + /// + /// Emails sent this month + /// + public int EmailsSentThisMonth { get; set; } + + /// + /// Last time email was sent + /// + public DateTime? LastEmailSentOn { get; set; } + + /// + /// Last time credentials were verified + /// + public DateTime? LastVerifiedOn { get; set; } + + /// + /// Any error message from last operation + /// + public string? LastError { get; set; } +} diff --git a/2-Aquiis.Application/Models/SMSConfigurationModel.cs b/2-Aquiis.Application/Models/SMSConfigurationModel.cs new file mode 100644 index 0000000..6fb2125 --- /dev/null +++ b/2-Aquiis.Application/Models/SMSConfigurationModel.cs @@ -0,0 +1,68 @@ +namespace Aquiis.Application.Models; + +/// +/// View model for SMS configuration in the UI. +/// Combines settings from OrganizationSMSSettings for display and editing. +/// +public class SMSConfigurationModel +{ + /// + /// Twilio Account SID (unencrypted for display/editing) + /// + public string AccountSid { get; set; } = string.Empty; + + /// + /// Twilio Auth Token (unencrypted for display/editing) + /// + public string AuthToken { get; set; } = string.Empty; + + /// + /// Phone number to send from + /// + public string FromPhoneNumber { get; set; } = string.Empty; + + /// + /// Whether SMS is currently enabled + /// + public bool IsEnabled { get; set; } + + /// + /// Whether credentials have been verified + /// + public bool IsVerified { get; set; } + + /// + /// SMS messages sent today + /// + public int SMSSentToday { get; set; } + + /// + /// SMS messages sent this month + /// + public int SMSSentThisMonth { get; set; } + + /// + /// Last time SMS was sent + /// + public DateTime? LastSMSSentOn { get; set; } + + /// + /// Last time credentials were verified + /// + public DateTime? LastVerifiedOn { get; set; } + + /// + /// Account balance (if available) + /// + public decimal? AccountBalance { get; set; } + + /// + /// Cost per SMS (if available) + /// + public decimal? CostPerSMS { get; set; } + + /// + /// Any error message from last operation + /// + public string? LastError { get; set; } +} diff --git a/2-Aquiis.Application/Services/ApplicationService.cs b/2-Aquiis.Application/Services/ApplicationService.cs new file mode 100644 index 0000000..4182b35 --- /dev/null +++ b/2-Aquiis.Application/Services/ApplicationService.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Options; +using Aquiis.Core.Entities; +using Aquiis.Core.Constants; + +namespace Aquiis.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/2-Aquiis.Application/Services/BaseService.cs b/2-Aquiis.Application/Services/BaseService.cs new file mode 100644 index 0000000..2cf1652 --- /dev/null +++ b/2-Aquiis.Application/Services/BaseService.cs @@ -0,0 +1,393 @@ +using System.Linq.Expressions; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Aquiis.Application.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 IUserContextService _userContext; + protected readonly ApplicationSettings _settings; + protected readonly DbSet _dbSet; + + protected BaseService( + ApplicationDbContext context, + ILogger> logger, + IUserContextService 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/2-Aquiis.Application/Services/CalendarEventService.cs b/2-Aquiis.Application/Services/CalendarEventService.cs new file mode 100644 index 0000000..09aeb29 --- /dev/null +++ b/2-Aquiis.Application/Services/CalendarEventService.cs @@ -0,0 +1,259 @@ +using Aquiis.Core.Interfaces.Services; +using Microsoft.EntityFrameworkCore; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces; + +namespace Aquiis.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 IUserContextService _userContextService; + + public CalendarEventService(ApplicationDbContext context, CalendarSettingsService settingsService, IUserContextService 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/2-Aquiis.Application/Services/CalendarSettingsService.cs b/2-Aquiis.Application/Services/CalendarSettingsService.cs new file mode 100644 index 0000000..37b5797 --- /dev/null +++ b/2-Aquiis.Application/Services/CalendarSettingsService.cs @@ -0,0 +1,137 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Entities; +using Aquiis.Core.Utilities; +using Aquiis.Application.Services; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Application.Services; + +public class CalendarSettingsService +{ + private readonly ApplicationDbContext _context; + private readonly IUserContextService _userContext; + + public CalendarSettingsService(ApplicationDbContext context, IUserContextService 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/2-Aquiis.Application/Services/ChecklistService.cs b/2-Aquiis.Application/Services/ChecklistService.cs new file mode 100644 index 0000000..81e95f7 --- /dev/null +++ b/2-Aquiis.Application/Services/ChecklistService.cs @@ -0,0 +1,654 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Entities; +using Aquiis.Core.Constants; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Http; + +namespace Aquiis.Application.Services +{ + public class ChecklistService + { + private readonly ApplicationDbContext _dbContext; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IUserContextService _userContext; + + public ChecklistService( + ApplicationDbContext dbContext, + IHttpContextAccessor httpContextAccessor, + IUserContextService 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/2-Aquiis.Application/Services/DatabaseService.cs b/2-Aquiis.Application/Services/DatabaseService.cs new file mode 100644 index 0000000..91ff1d5 --- /dev/null +++ b/2-Aquiis.Application/Services/DatabaseService.cs @@ -0,0 +1,88 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Aquiis.Application.Services; + +/// +/// Service for managing database initialization and migrations. +/// Handles both business (ApplicationDbContext) and product-specific Identity contexts. +/// +public class DatabaseService : IDatabaseService +{ + private readonly ApplicationDbContext _businessContext; + private readonly DbContext _identityContext; // Product-specific (SimpleStartDbContext or ProfessionalDbContext) + private readonly ILogger _logger; + + public DatabaseService( + ApplicationDbContext businessContext, + DbContext identityContext, + ILogger logger) + { + _businessContext = businessContext; + _identityContext = identityContext; + _logger = logger; + } + + public async Task InitializeAsync() + { + _logger.LogInformation("Checking for pending migrations..."); + + // Check and apply identity migrations first + var identityPending = await _identityContext.Database.GetPendingMigrationsAsync(); + if (identityPending.Any()) + { + _logger.LogInformation($"Applying {identityPending.Count()} identity migrations..."); + await _identityContext.Database.MigrateAsync(); + _logger.LogInformation("Identity migrations applied successfully."); + } + else + { + _logger.LogInformation("No pending identity migrations."); + } + + // Then check and apply business migrations + var businessPending = await _businessContext.Database.GetPendingMigrationsAsync(); + if (businessPending.Any()) + { + _logger.LogInformation($"Applying {businessPending.Count()} business migrations..."); + await _businessContext.Database.MigrateAsync(); + _logger.LogInformation("Business migrations applied successfully."); + } + else + { + _logger.LogInformation("No pending business migrations."); + } + + _logger.LogInformation("Database initialization complete."); + } + + public async Task CanConnectAsync() + { + try + { + var businessCanConnect = await _businessContext.Database.CanConnectAsync(); + var identityCanConnect = await _identityContext.Database.CanConnectAsync(); + + return businessCanConnect && identityCanConnect; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to connect to database"); + return false; + } + } + + public async Task GetPendingMigrationsCountAsync() + { + var pending = await _businessContext.Database.GetPendingMigrationsAsync(); + return pending.Count(); + } + + public async Task GetIdentityPendingMigrationsCountAsync() + { + var pending = await _identityContext.Database.GetPendingMigrationsAsync(); + return pending.Count(); + } +} diff --git a/2-Aquiis.Application/Services/DocumentService.cs b/2-Aquiis.Application/Services/DocumentService.cs new file mode 100644 index 0000000..84aadbf --- /dev/null +++ b/2-Aquiis.Application/Services/DocumentService.cs @@ -0,0 +1,431 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; +using Aquiis.Application.Services; +using Aquiis.Application.Services.PdfGenerators; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/EmailService.cs b/2-Aquiis.Application/Services/EmailService.cs new file mode 100644 index 0000000..3a5d5d4 --- /dev/null +++ b/2-Aquiis.Application/Services/EmailService.cs @@ -0,0 +1,108 @@ +using System.Net; +using System.Net.Mail; +using Aquiis.Core.Interfaces.Services; +namespace Aquiis.Application.Services{ + + public class EmailService : IEmailService + { + private readonly EmailSettingsService _emailSettingsService; + + public EmailService(EmailSettingsService emailSettingsService) + { + _emailSettingsService = emailSettingsService; + } + + public async Task GetEmailStatsAsync() + { + var settings = await _emailSettingsService.GetOrCreateSettingsAsync(); + if (settings == null) + { + return new EmailStats { IsConfigured = false }; + } + + // Example logic to get email stats + var stats = new EmailStats + { + PlanType = settings.PlanType!, + Provider = settings.ProviderName, + LastEmailSentOn = settings.LastEmailSentOn, + EmailsSentToday = settings.EmailsSentToday, + DailyLimit = settings.DailyLimit!.Value, + EmailsSentThisMonth = settings.EmailsSentThisMonth, + MonthlyLimit = settings.MonthlyLimit!.Value, + IsConfigured = settings.IsEmailEnabled + }; + + return stats; + } + + public async Task SendEmailAsync(string to, string subject, string body) + { + var settings = await _emailSettingsService.GetOrCreateSettingsAsync(); + if (settings == null) + { + throw new InvalidOperationException("Email settings are not configured."); + } + + // Implement email sending logic here using the configured settings + // Example using SMTP client + switch (settings.ProviderName) + { + case "SendGrid": + // Implement SendGrid email sending logic here + break; + case "SMTP": + using (var client = new SmtpClient(settings.SmtpServer, settings.SmtpPort)) + { + client.Credentials = new NetworkCredential(settings.Username, settings.Password); + client.EnableSsl = settings.EnableSsl; + + var mailMessage = new MailMessage + { + From = new MailAddress(settings.FromEmail!, settings.FromName), + Subject = subject, + Body = body, + IsBodyHtml = true + }; + mailMessage.To.Add(to); + + await client.SendMailAsync(mailMessage); + } + break; + default: + throw new NotSupportedException($"Email provider '{settings.ProviderName}' is not supported."); + } + + } + + public Task SendEmailAsync(string to, string subject, string body, string? fromName = null) + { + throw new NotImplementedException(); + } + + public Task SendTemplateEmailAsync(string to, string templateId, Dictionary templateData) + { + throw new NotImplementedException(); + } + + public Task ValidateEmailAddressAsync(string emailAddress) + { + throw new NotImplementedException(); + } + } + + public class EmailStats + { + public string PlanType { get; set; } = "string.Empty"; + public string Provider { get; set; } = string.Empty; + + public DateTime? LastEmailSentOn { get; set; } + public int EmailsSentToday { get; set; } + public int DailyLimit { get; set; } + public int EmailsSentThisMonth { get; set; } + public int MonthlyLimit { get; set; } + public bool IsConfigured { get; set; } + public int DailyPercentUsed => DailyLimit == 0 ? 0 : (int)((double)EmailsSentToday / DailyLimit * 100); + public int MonthlyPercentUsed => MonthlyLimit == 0 ? 0 : (int)((double)EmailsSentThisMonth / MonthlyLimit * 100); + } +} \ No newline at end of file diff --git a/2-Aquiis.Application/Services/EmailSettingsService.cs b/2-Aquiis.Application/Services/EmailSettingsService.cs new file mode 100644 index 0000000..77afb7d --- /dev/null +++ b/2-Aquiis.Application/Services/EmailSettingsService.cs @@ -0,0 +1,157 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Infrastructure.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Aquiis.Application.Services +{ + public class EmailSettingsService : BaseService + { + private readonly IEmailProvider _emailProvider; + + public EmailSettingsService( + ApplicationDbContext context, + ILogger logger, + IUserContextService userContext, + IOptions settings, + IEmailProvider emailProvider) + : base(context, logger, userContext, settings) + { + _emailProvider = emailProvider; + } + + /// + /// 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 _emailProvider.VerifyApiKeyAsync(apiKey)) + { + return OperationResult.FailureResult( + "Invalid SendGrid API key. Please verify the key has Mail Send permissions."); + } + + var settings = await GetOrCreateSettingsAsync(); + + settings.SendGridApiKeyEncrypted = _emailProvider.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 _emailProvider.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/2-Aquiis.Application/Services/FinancialReportService.cs b/2-Aquiis.Application/Services/FinancialReportService.cs new file mode 100644 index 0000000..2ad1d48 --- /dev/null +++ b/2-Aquiis.Application/Services/FinancialReportService.cs @@ -0,0 +1,286 @@ +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.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/2-Aquiis.Application/Services/InspectionService.cs b/2-Aquiis.Application/Services/InspectionService.cs new file mode 100644 index 0000000..dd4b945 --- /dev/null +++ b/2-Aquiis.Application/Services/InspectionService.cs @@ -0,0 +1,275 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces; +using Aquiis.Application.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/InvoiceService.cs b/2-Aquiis.Application/Services/InvoiceService.cs new file mode 100644 index 0000000..12dcaf5 --- /dev/null +++ b/2-Aquiis.Application/Services/InvoiceService.cs @@ -0,0 +1,464 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; +using Aquiis.Application.Services; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/LeaseOfferService.cs b/2-Aquiis.Application/Services/LeaseOfferService.cs new file mode 100644 index 0000000..e97db2b --- /dev/null +++ b/2-Aquiis.Application/Services/LeaseOfferService.cs @@ -0,0 +1,294 @@ +using Aquiis.Core.Interfaces.Services; +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Aquiis.Application.Services; +using Microsoft.Extensions.Logging; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/LeaseService.cs b/2-Aquiis.Application/Services/LeaseService.cs new file mode 100644 index 0000000..b129647 --- /dev/null +++ b/2-Aquiis.Application/Services/LeaseService.cs @@ -0,0 +1,491 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; +using Aquiis.Application.Services; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/MaintenanceService.cs b/2-Aquiis.Application/Services/MaintenanceService.cs new file mode 100644 index 0000000..1020b32 --- /dev/null +++ b/2-Aquiis.Application/Services/MaintenanceService.cs @@ -0,0 +1,492 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Application.Services.Workflows; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Logging; +using Aquiis.Application.Services; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/NoteService.cs b/2-Aquiis.Application/Services/NoteService.cs new file mode 100644 index 0000000..2d6f5b2 --- /dev/null +++ b/2-Aquiis.Application/Services/NoteService.cs @@ -0,0 +1,102 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Application.Services +{ + public class NoteService + { + private readonly ApplicationDbContext _context; + private readonly IUserContextService _userContext; + + public NoteService(ApplicationDbContext context, IUserContextService 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 + .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/2-Aquiis.Application/Services/NotificationService.cs b/2-Aquiis.Application/Services/NotificationService.cs new file mode 100644 index 0000000..b92cd30 --- /dev/null +++ b/2-Aquiis.Application/Services/NotificationService.cs @@ -0,0 +1,274 @@ + +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Application.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Aquiis.Application.Services; +public class NotificationService : BaseService +{ + private readonly IEmailService _emailService; + private readonly ISMSService _smsService; + private new readonly ILogger _logger; + + public NotificationService( + ApplicationDbContext context, + IUserContextService 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); + } + + /// + /// Mark all notifications as read for the current user + /// + public async Task MarkAllAsReadAsync(List notifications) + { + foreach (var notification in notifications) + { + if (!notification.IsRead) + { + notification.IsRead = true; + notification.ReadOn = DateTime.UtcNow; + await UpdateAsync(notification); + } + } + } + + /// + /// Get unread notifications for current user + /// Returns empty list if user is not authenticated or has no organization. + /// + public async Task> GetUnreadNotificationsAsync() + { + var userId = await _userContext.GetUserIdAsync(); + var organizationId = await _userContext.GetActiveOrganizationIdAsync(); + + // Return empty list if not authenticated or no organization + if (string.IsNullOrEmpty(userId) || !organizationId.HasValue) + { + return new List(); + } + + 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/2-Aquiis.Application/Services/OrganizationService.cs b/2-Aquiis.Application/Services/OrganizationService.cs new file mode 100644 index 0000000..9605793 --- /dev/null +++ b/2-Aquiis.Application/Services/OrganizationService.cs @@ -0,0 +1,493 @@ +using Aquiis.Core.Entities; +using Aquiis.Core.Constants; +using Aquiis.Core.Interfaces.Services; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.Application.Services +{ + public class OrganizationService + { + private readonly ApplicationDbContext _dbContext; + private readonly IUserContextService _userContext; + + public OrganizationService(ApplicationDbContext dbContext, IUserContextService 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/2-Aquiis.Application/Services/PaymentService.cs b/2-Aquiis.Application/Services/PaymentService.cs new file mode 100644 index 0000000..5536e8f --- /dev/null +++ b/2-Aquiis.Application/Services/PaymentService.cs @@ -0,0 +1,409 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.ComponentModel.DataAnnotations; +using Aquiis.Application.Services; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/PdfGenerators/ChecklistPdfGenerator.cs b/2-Aquiis.Application/Services/PdfGenerators/ChecklistPdfGenerator.cs new file mode 100644 index 0000000..50b8c8f --- /dev/null +++ b/2-Aquiis.Application/Services/PdfGenerators/ChecklistPdfGenerator.cs @@ -0,0 +1,248 @@ +using Aquiis.Core.Entities; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using QuestPDF.Drawing; + +namespace Aquiis.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/2-Aquiis.Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs b/2-Aquiis.Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs new file mode 100644 index 0000000..83b9545 --- /dev/null +++ b/2-Aquiis.Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs @@ -0,0 +1,453 @@ +using Aquiis.Core.Entities; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace Aquiis.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/2-Aquiis.Application/Services/PdfGenerators/InspectionPdfGenerator.cs b/2-Aquiis.Application/Services/PdfGenerators/InspectionPdfGenerator.cs new file mode 100644 index 0000000..b1f7faa --- /dev/null +++ b/2-Aquiis.Application/Services/PdfGenerators/InspectionPdfGenerator.cs @@ -0,0 +1,362 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Core.Entities; + +namespace Aquiis.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/2-Aquiis.Application/Services/PdfGenerators/InvoicePdfGenerator.cs b/2-Aquiis.Application/Services/PdfGenerators/InvoicePdfGenerator.cs new file mode 100644 index 0000000..e045f99 --- /dev/null +++ b/2-Aquiis.Application/Services/PdfGenerators/InvoicePdfGenerator.cs @@ -0,0 +1,244 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Core.Entities; + +namespace Aquiis.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/2-Aquiis.Application/Services/PdfGenerators/LeasePdfGenerator.cs b/2-Aquiis.Application/Services/PdfGenerators/LeasePdfGenerator.cs new file mode 100644 index 0000000..cb6c9d8 --- /dev/null +++ b/2-Aquiis.Application/Services/PdfGenerators/LeasePdfGenerator.cs @@ -0,0 +1,262 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Core.Entities; + +namespace Aquiis.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/2-Aquiis.Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs b/2-Aquiis.Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs new file mode 100644 index 0000000..39e37c4 --- /dev/null +++ b/2-Aquiis.Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs @@ -0,0 +1,238 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Core.Entities; +using PdfDocument = QuestPDF.Fluent.Document; + +namespace Aquiis.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/2-Aquiis.Application/Services/PdfGenerators/PaymentPdfGenerator.cs b/2-Aquiis.Application/Services/PdfGenerators/PaymentPdfGenerator.cs new file mode 100644 index 0000000..77d1564 --- /dev/null +++ b/2-Aquiis.Application/Services/PdfGenerators/PaymentPdfGenerator.cs @@ -0,0 +1,256 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using Aquiis.Core.Entities; + +namespace Aquiis.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/2-Aquiis.Application/Services/PropertyManagementService.cs b/2-Aquiis.Application/Services/PropertyManagementService.cs new file mode 100644 index 0000000..e73957b --- /dev/null +++ b/2-Aquiis.Application/Services/PropertyManagementService.cs @@ -0,0 +1,2672 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Aquiis.Core.Constants; +using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Http; + +namespace Aquiis.Application.Services +{ + public class PropertyManagementService + { + private readonly ApplicationDbContext _dbContext; + private readonly ApplicationSettings _applicationSettings; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IUserContextService _userContext; + private readonly CalendarEventService _calendarEventService; + private readonly ChecklistService _checklistService; + + public PropertyManagementService( + ApplicationDbContext dbContext, + IOptions settings, + IHttpContextAccessor httpContextAccessor, + IUserContextService userContext, + CalendarEventService calendarEventService, + ChecklistService checklistService) + { + _dbContext = dbContext; + _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/2-Aquiis.Application/Services/PropertyService.cs b/2-Aquiis.Application/Services/PropertyService.cs new file mode 100644 index 0000000..3cbabc5 --- /dev/null +++ b/2-Aquiis.Application/Services/PropertyService.cs @@ -0,0 +1,402 @@ +using Aquiis.Core.Interfaces.Services; +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; + +namespace Aquiis.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; + + private readonly NotificationService _notificationService; + + public PropertyService( + ApplicationDbContext context, + ILogger logger, + IUserContextService userContext, + IOptions settings, + CalendarEventService calendarEventService, NotificationService notificationService) + : base(context, logger, userContext, settings) + { + _calendarEventService = calendarEventService; + _notificationService = notificationService; + _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 _notificationService.CreateAsync(new Notification + { + Id = Guid.NewGuid(), + Type = NotificationConstants.Types.Info, + Category = NotificationConstants.Categories.CalendarEvent, + Title = "Routine Inspection Scheduled", + Message = $"A routine inspection has been scheduled for the property at {property.Address} on {calendarEvent.StartOn:d}.", + RecipientUserId = userId!, + RelatedEntityId = calendarEvent.PropertyId, + RelatedEntityType = nameof(Property), + SentOn = DateTime.UtcNow, + OrganizationId = organizationId!.Value, + CreatedBy = userId!, + CreatedOn = DateTime.UtcNow + }); + + 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/2-Aquiis.Application/Services/ProspectiveTenantService.cs b/2-Aquiis.Application/Services/ProspectiveTenantService.cs new file mode 100644 index 0000000..d4e1bde --- /dev/null +++ b/2-Aquiis.Application/Services/ProspectiveTenantService.cs @@ -0,0 +1,218 @@ +using Aquiis.Core.Interfaces.Services; +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; +using Aquiis.Application.Services; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/RentalApplicationService.cs b/2-Aquiis.Application/Services/RentalApplicationService.cs new file mode 100644 index 0000000..97e454f --- /dev/null +++ b/2-Aquiis.Application/Services/RentalApplicationService.cs @@ -0,0 +1,273 @@ +using Aquiis.Core.Interfaces.Services; +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Aquiis.Application.Services; +using Microsoft.Extensions.Logging; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/SMSService.cs b/2-Aquiis.Application/Services/SMSService.cs new file mode 100644 index 0000000..9b459cf --- /dev/null +++ b/2-Aquiis.Application/Services/SMSService.cs @@ -0,0 +1,82 @@ + +using Aquiis.Core.Interfaces.Services; + +namespace Aquiis.Application.Services +{ + public class SMSService : ISMSService + { + private readonly SMSSettingsService _smsSettingsService; + public SMSService(SMSSettingsService smsSettingsService) + { + _smsSettingsService = smsSettingsService; + } + + public async Task SendSMSAsync(string phoneNumber, string message) + { + var settings = await _smsSettingsService.GetOrCreateSettingsAsync(); + if (settings == null) + { + throw new InvalidOperationException("SMS settings are not configured."); + } + + // Implement SMS sending logic here using the configured settings + } + + public Task ValidatePhoneNumberAsync(string phoneNumber) + { + throw new NotImplementedException(); + } + + public async Task GetSMSStatsAsync() + { + var stats = await _smsSettingsService.GetOrCreateSettingsAsync(); + return new SMSStats + { + ProviderName = stats.ProviderName!, + SMSSentToday = stats.SMSSentToday, + SMSSentThisMonth = stats.SMSSentThisMonth, + LastSMSSentOn = stats.LastSMSSentOn, + StatsLastUpdatedOn = stats.StatsLastUpdatedOn, + DailyCountResetOn = stats.DailyCountResetOn, + MonthlyCountResetOn = stats.MonthlyCountResetOn, + AccountBalance = stats.AccountBalance, + CostPerSMS = stats.CostPerSMS + }; + } + public Task DisableSMSAsync() + { + throw new NotImplementedException(); + } + + public Task EnableSMSAsync() + { + throw new NotImplementedException(); + } + + public Task IsSMSEnabledAsync() + { + throw new NotImplementedException(); + } + } + + public class SMSStats + { + public string ProviderName { get; set; } = string.Empty; + public int TotalMessages { get; set; } + public int SentMessages { get; set; } + public int FailedMessages { get; set; } + public int PendingMessages { 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 + } +} \ No newline at end of file diff --git a/2-Aquiis.Application/Services/SMSSettingsService.cs b/2-Aquiis.Application/Services/SMSSettingsService.cs new file mode 100644 index 0000000..2c4a005 --- /dev/null +++ b/2-Aquiis.Application/Services/SMSSettingsService.cs @@ -0,0 +1,124 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Infrastructure.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Aquiis.Application.Services; + +namespace Aquiis.Application.Services +{ + public class SMSSettingsService : BaseService + { + private readonly ISMSProvider _smsProvider; + + public SMSSettingsService( + ApplicationDbContext context, + ILogger logger, + IUserContextService userContext, + IOptions settings, + ISMSProvider smsProvider) + : base(context, logger, userContext, settings) + { + _smsProvider = smsProvider; + } + + 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 _smsProvider.VerifyCredentialsAsync(accountSid, authToken)) + { + 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 = _smsProvider.EncryptCredential(accountSid); + settings.TwilioAuthTokenEncrypted = _smsProvider.EncryptCredential(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 _smsProvider.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/2-Aquiis.Application/Services/ScheduledTaskService.cs b/2-Aquiis.Application/Services/ScheduledTaskService.cs new file mode 100644 index 0000000..4581fb3 --- /dev/null +++ b/2-Aquiis.Application/Services/ScheduledTaskService.cs @@ -0,0 +1,802 @@ +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Aquiis.Application.Services.Workflows; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; + +namespace Aquiis.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 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, 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, + 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(organizationId); + + 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/2-Aquiis.Application/Services/SchemaValidationService.cs b/2-Aquiis.Application/Services/SchemaValidationService.cs new file mode 100644 index 0000000..49e2b32 --- /dev/null +++ b/2-Aquiis.Application/Services/SchemaValidationService.cs @@ -0,0 +1,127 @@ +using Aquiis.Core.Entities; +using Aquiis.Core.Constants; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; + +namespace Aquiis.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 + { + // Try to query the table directly - if it doesn't exist, EF will throw + 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 (Microsoft.Data.Sqlite.SqliteException ex) when (ex.SqliteErrorCode == 1) // SQLITE_ERROR - table doesn't exist + { + _logger.LogWarning("SchemaVersions table does not exist"); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting current schema version"); + return null; + } + } + } +} diff --git a/2-Aquiis.Application/Services/ScreeningService.cs b/2-Aquiis.Application/Services/ScreeningService.cs new file mode 100644 index 0000000..fe9bf42 --- /dev/null +++ b/2-Aquiis.Application/Services/ScreeningService.cs @@ -0,0 +1,237 @@ +using Aquiis.Core.Interfaces.Services; +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Aquiis.Application.Services; +using Microsoft.Extensions.Logging; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/SecurityDepositService.cs b/2-Aquiis.Application/Services/SecurityDepositService.cs new file mode 100644 index 0000000..e41b0e8 --- /dev/null +++ b/2-Aquiis.Application/Services/SecurityDepositService.cs @@ -0,0 +1,740 @@ +using Aquiis.Core.Interfaces.Services; +using Microsoft.EntityFrameworkCore; +using Aquiis.Core.Entities; +using Aquiis.Core.Constants; + +namespace Aquiis.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 IUserContextService _userContext; + + public SecurityDepositService(ApplicationDbContext context, IUserContextService 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/2-Aquiis.Application/Services/TenantConversionService.cs b/2-Aquiis.Application/Services/TenantConversionService.cs new file mode 100644 index 0000000..17fae7a --- /dev/null +++ b/2-Aquiis.Application/Services/TenantConversionService.cs @@ -0,0 +1,129 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + + +namespace Aquiis.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 IUserContextService _userContext; + + public TenantConversionService( + ApplicationDbContext context, + ILogger logger, + IUserContextService 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/2-Aquiis.Application/Services/TenantService.cs b/2-Aquiis.Application/Services/TenantService.cs new file mode 100644 index 0000000..3fe2078 --- /dev/null +++ b/2-Aquiis.Application/Services/TenantService.cs @@ -0,0 +1,418 @@ +using Aquiis.Core.Interfaces.Services; +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Aquiis.Application.Services; +using Microsoft.Extensions.Logging; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/TourService.cs b/2-Aquiis.Application/Services/TourService.cs new file mode 100644 index 0000000..8f0d510 --- /dev/null +++ b/2-Aquiis.Application/Services/TourService.cs @@ -0,0 +1,490 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Aquiis.Application.Services; +using Microsoft.Extensions.Logging; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/Workflows/AccountWorkflowService.cs b/2-Aquiis.Application/Services/Workflows/AccountWorkflowService.cs new file mode 100644 index 0000000..a84adc3 --- /dev/null +++ b/2-Aquiis.Application/Services/Workflows/AccountWorkflowService.cs @@ -0,0 +1,36 @@ +using Aquiis.Core.Interfaces.Services; + +namespace Aquiis.Application.Services.Workflows +{ + public enum AccountStatus + { + Created, + Active, + Locked, + Closed + } + public class AccountWorkflowService : BaseWorkflowService, IWorkflowState + { + public AccountWorkflowService(ApplicationDbContext context, + IUserContextService 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/2-Aquiis.Application/Services/Workflows/ApplicationWorkflowService.cs b/2-Aquiis.Application/Services/Workflows/ApplicationWorkflowService.cs new file mode 100644 index 0000000..074dce3 --- /dev/null +++ b/2-Aquiis.Application/Services/Workflows/ApplicationWorkflowService.cs @@ -0,0 +1,1275 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.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, + IUserContextService 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/2-Aquiis.Application/Services/Workflows/BaseWorkflowService.cs b/2-Aquiis.Application/Services/Workflows/BaseWorkflowService.cs new file mode 100644 index 0000000..9fb9ab1 --- /dev/null +++ b/2-Aquiis.Application/Services/Workflows/BaseWorkflowService.cs @@ -0,0 +1,208 @@ +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace Aquiis.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 IUserContextService _userContext; + + protected BaseWorkflowService( + ApplicationDbContext context, + IUserContextService 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/2-Aquiis.Application/Services/Workflows/IWorkflowState.cs b/2-Aquiis.Application/Services/Workflows/IWorkflowState.cs new file mode 100644 index 0000000..493105f --- /dev/null +++ b/2-Aquiis.Application/Services/Workflows/IWorkflowState.cs @@ -0,0 +1,32 @@ +namespace Aquiis.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/2-Aquiis.Application/Services/Workflows/LeaseWorkflowService.cs b/2-Aquiis.Application/Services/Workflows/LeaseWorkflowService.cs new file mode 100644 index 0000000..a7b0f10 --- /dev/null +++ b/2-Aquiis.Application/Services/Workflows/LeaseWorkflowService.cs @@ -0,0 +1,855 @@ +using Aquiis.Core.Interfaces.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Aquiis.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, + IUserContextService 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() + { + var orgId = await GetActiveOrganizationIdAsync(); + return await ExpireOverdueLeaseAsync(orgId); + } + + /// + /// Expires leases that have passed their end date without renewal. + /// This overload accepts organizationId for background service contexts. + /// + public async Task> ExpireOverdueLeaseAsync(Guid organizationId) + { + return await ExecuteWorkflowAsync(async () => + { + var userId = await _userContext.GetUserIdAsync() ?? "System"; + + // Find active leases past their end date + var expiredLeases = await _context.Leases + .Include(l => l.Property) + .Include(l => l.Tenant) + .Where(l => l.OrganizationId == organizationId && + 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/2-Aquiis.Application/Services/Workflows/WorkflowResult.cs b/2-Aquiis.Application/Services/Workflows/WorkflowResult.cs new file mode 100644 index 0000000..dd4cd70 --- /dev/null +++ b/2-Aquiis.Application/Services/Workflows/WorkflowResult.cs @@ -0,0 +1,78 @@ +namespace Aquiis.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/3-Aquiis.UI.Shared/Aquiis.UI.Shared.csproj b/3-Aquiis.UI.Shared/Aquiis.UI.Shared.csproj new file mode 100644 index 0000000..d2a1ecc --- /dev/null +++ b/3-Aquiis.UI.Shared/Aquiis.UI.Shared.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/3-Aquiis.UI.Shared/COMPONENT-USAGE-GUIDE.md b/3-Aquiis.UI.Shared/COMPONENT-USAGE-GUIDE.md new file mode 100644 index 0000000..f76da4c --- /dev/null +++ b/3-Aquiis.UI.Shared/COMPONENT-USAGE-GUIDE.md @@ -0,0 +1,1109 @@ +# Aquiis.UI.Shared - Component Usage Guide + +This guide provides detailed usage examples and patterns for all shared components in the Aquiis.UI.Shared library. + +## Table of Contents + +1. [Common Components](#common-components) + - [Modal](#modal) + - [Card](#card) + - [DataTable](#datatable) + - [FormField](#formfield) +2. [Layout Components](#layout-components) + - [SharedMainLayout](#sharedmainlayout) +3. [Feature Components](#feature-components) + - [Notification Components](#notification-components) +4. [Testing Patterns](#testing-patterns) + +--- + +## Common Components + +### Modal + +**File**: `Components/Common/Modal.razor` + +Display content in an overlay dialog with backdrop, header, body, and footer sections. + +#### Basic Example + +```razor +@page "/modal-demo" + + + + + +

This is the modal content.

+
+ + + +
+ +@code { + private bool isModalVisible = false; + + private void OpenModal() => isModalVisible = true; + private void CloseModal() => isModalVisible = false; +} +``` + +#### All Parameters + +```csharp +[Parameter] public bool IsVisible { get; set; } // Controls modal visibility +[Parameter] public string Title { get; set; } = ""; // Modal header title +[Parameter] public ModalSize Size { get; set; } = ModalSize.Default; // Small, Default, Large, ExtraLarge +[Parameter] public ModalPosition Position { get; set; } // Top, Centered + = ModalPosition.Top; +[Parameter] public bool ShowCloseButton { get; set; } = true; // Show X button in header +[Parameter] public bool CloseOnBackdropClick { get; set; } = true; // Close when clicking backdrop +[Parameter] public EventCallback OnClose { get; set; } // Called when modal closes +[Parameter] public RenderFragment? HeaderContent { get; set; } // Custom header (overrides Title) +[Parameter] public RenderFragment? ChildContent { get; set; } // Main modal body +[Parameter] public RenderFragment? FooterContent { get; set; } // Footer buttons/actions +``` + +#### Size Variants + +```razor + + + Compact content + + + + + More content space + + + + + Maximum content space + +``` + +#### Positioning + +```razor + + + Modal appears at top of viewport + + + + + Modal appears in viewport center + +``` + +#### Custom Header + +```razor + + +
+ +
Warning: Data Will Be Lost
+
+
+ +

Are you sure you want to proceed? This action cannot be undone.

+
+ + + + +
+``` + +#### Form in Modal + +```razor + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### Confirmation Dialog Pattern + +```razor +@code { + private bool showDeleteConfirm = false; + private int itemToDelete; + + private void ShowDeleteConfirmation(int itemId) + { + itemToDelete = itemId; + showDeleteConfirm = true; + } + + private async Task ConfirmDelete() + { + await DeleteItem(itemToDelete); + showDeleteConfirm = false; + // Show success message + } +} + + + +
+ +

Are you sure you want to delete this item?

+

This action cannot be undone.

+
+
+ + + + +
+``` + +--- + +### Card + +**File**: `Components/Common/Card.razor` + +Container for related content with optional header, body, and footer sections. + +#### Basic Example + +```razor + + +

Name: John Doe

+

Email: john@example.com

+
+
+``` + +#### All Parameters + +```csharp +[Parameter] public string Title { get; set; } = ""; // Card title in header +[Parameter] public bool FullHeight { get; set; } // Stretch to 100% height +[Parameter] public string CssClass { get; set; } = ""; // Additional CSS classes +[Parameter] public string HeaderCssClass { get; set; } = ""; // Header-specific classes +[Parameter] public string BodyCssClass { get; set; } = ""; // Body-specific classes +[Parameter] public string FooterCssClass { get; set; } = ""; // Footer-specific classes +[Parameter] public RenderFragment? HeaderContent { get; set; } // Custom header +[Parameter] public RenderFragment? ChildContent { get; set; } // Card body content +[Parameter] public RenderFragment? FooterContent { get; set; } // Card footer +``` + +#### Card with Footer + +```razor + + +
    +
  • Order #12345 - $299.99
  • +
  • Order #12346 - $149.50
  • +
  • Order #12347 - $89.99
  • +
+
+ + View All Orders + +
+``` + +#### Custom Header with Actions + +```razor + + +
+
Dashboard Stats
+
+ +
+
+
+ +
+
+

@totalUsers

+ Total Users +
+
+

@activeUsers

+ Active Users +
+
+

@revenue

+ Revenue +
+
+
+
+``` + +#### Full Height Card (Dashboard Layout) + +```razor +
+
+ + + @foreach (var activity in activities) + { +
+ @activity.User @activity.Action +
+ @activity.Time.ToString("g") +
+ } +
+
+
+
+ + + + + +
+
+``` + +#### Styled Cards + +```razor + + + + This is an important notice. + + + + + + + Operation completed successfully! + + + + + + + This card has a prominent shadow. + + +``` + +--- + +### DataTable + +**File**: `Components/Common/DataTable.razor` + +Display tabular data with customizable headers and row templates. + +#### Basic Example + +```razor + + + Name + Email + Role + + + @user.Name + @user.Email + @user.Role + + + +@code { + private List users = new(); +} +``` + +#### All Parameters + +```csharp +[Parameter] public IEnumerable? Items { get; set; } // Data collection +[Parameter] public RenderFragment? HeaderTemplate { get; set; } // Table header row +[Parameter] public RenderFragment? RowTemplate { get; set; } // Row template +[Parameter] public RenderFragment? EmptyMessage { get; set; } // Custom empty message +[Parameter] public bool ShowHeader { get; set; } = true; // Display header +[Parameter] public string TableCssClass { get; set; } // Table CSS classes + = "table table-striped table-hover"; +``` + +#### With Actions Column + +```razor + + + Order # + Customer + Total + Status + Actions + + + @order.OrderNumber + @order.CustomerName + @order.Total.ToString("C") + + + @order.Status + + + + + + + + + +``` + +#### Custom Empty State + +```razor + + + Name + SKU + Price + Stock + + + @product.Name + @product.SKU + @product.Price.ToString("C") + @product.Stock + + +
+ +

No Products Found

+

Start by adding your first product.

+ +
+
+
+``` + +#### Sortable Table Pattern + +```razor +@page "/sortable-users" + + + + + Name @GetSortIcon(nameof(User.Name)) + + + Email @GetSortIcon(nameof(User.Email)) + + + Created @GetSortIcon(nameof(User.Created)) + + + + @user.Name + @user.Email + @user.Created.ToString("d") + + + +@code { + private List users = new(); + private string sortColumn = nameof(User.Name); + private bool sortAscending = true; + + private IEnumerable SortedUsers => sortColumn switch + { + nameof(User.Name) => sortAscending + ? users.OrderBy(u => u.Name) + : users.OrderByDescending(u => u.Name), + nameof(User.Email) => sortAscending + ? users.OrderBy(u => u.Email) + : users.OrderByDescending(u => u.Email), + nameof(User.Created) => sortAscending + ? users.OrderBy(u => u.Created) + : users.OrderByDescending(u => u.Created), + _ => users + }; + + private void SetSortColumn(string column) + { + if (sortColumn == column) + { + sortAscending = !sortAscending; + } + else + { + sortColumn = column; + sortAscending = true; + } + } + + private string GetSortIcon(string column) + { + if (sortColumn != column) return ""; + return sortAscending ? "↑" : "↓"; + } +} +``` + +#### Nested Data with Expandable Rows + +```razor +@foreach (var order in orders) +{ + + + + @order.OrderNumber + + @order.CustomerName + @order.Total.ToString("C") + + + @if (expandedOrders.Contains(order.Id)) + { + + + + + @item.ProductName + Qty: @item.Quantity + @item.Price.ToString("C") + + + + + } +} + +@code { + private HashSet expandedOrders = new(); + + private void ToggleDetails(int orderId) + { + if (expandedOrders.Contains(orderId)) + expandedOrders.Remove(orderId); + else + expandedOrders.Add(orderId); + } +} +``` + +--- + +### FormField + +**File**: `Components/Common/FormField.razor` + +Consistent form field layout with label, input, help text, and validation. + +#### Basic Example + +```razor + + + +``` + +#### All Parameters + +```csharp +[Parameter] public string Label { get; set; } = ""; // Field label text +[Parameter] public bool Required { get; set; } // Show required indicator (*) +[Parameter] public string HelpText { get; set; } = ""; // Help text below input +[Parameter] public string CssClass { get; set; } = ""; // Container CSS classes +[Parameter] public string LabelCssClass { get; set; } = ""; // Label-specific classes +[Parameter] public RenderFragment? ChildContent { get; set; } // Input control +``` + +#### Complete Form Example + +```razor +@page "/create-user" + +

Create New User

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + +
+ + +
+
+ +@code { + private CreateUserModel model = new(); + + private async Task HandleSubmit() + { + // Save user + await UserService.CreateAsync(model); + NavigationManager.NavigateTo("/users"); + } + + private void Cancel() + { + NavigationManager.NavigateTo("/users"); + } +} +``` + +#### Horizontal Form Layout + +```razor + + + +
+ + + +
+ +
+ + + +
+
+``` + +#### Custom Input Components + +```razor + + + + + + + + + + + + + + + + + + +
+ + to + +
+
+``` + +--- + +## Layout Components + +### SharedMainLayout + +**File**: `Components/Layout/SharedMainLayout.razor` + +Base layout structure providing consistent navigation, header, content area, and footer across products. + +#### Basic Example + +```razor +@inherits LayoutComponentBase +@layout SharedMainLayout + + + + + + + + + + @Body + + +
+ © 2026 Aquiis. All rights reserved. +
+
+
+ +@code { + private string currentTheme = "light"; +} +``` + +#### All Parameters + +```csharp +[Parameter] public string Theme { get; set; } = "light"; // Theme name +[Parameter] public RenderFragment? SidebarContent { get; set; } // Navigation sidebar +[Parameter] public RenderFragment? AuthorizedHeaderContent { get; set; } // Header (authenticated) +[Parameter] public RenderFragment? NotAuthorizedContent { get; set; } // Header (anonymous) +[Parameter] public RenderFragment? ChildContent { get; set; } // Main content +[Parameter] public RenderFragment? FooterContent { get; set; } // Footer content +``` + +#### Complete Layout Implementation + +```razor + +@inherits LayoutComponentBase + + + + + + + +
+ +
+ + + + +
+ + + + + + +
+
+ + +
+ Login + Sign Up +
+
+ + + + +
+ @Body +
+
+ +
+

An error occurred

+

@exception.Message

+
+
+
+
+ + + + +
+ +@code { + [Inject] private IThemeService _themeService { get; set; } = default!; + + private string userName = "John Doe"; + private string userAvatar = "/images/default-avatar.png"; + + private void Logout() + { + // Handle logout + } +} +``` + +--- + +## Feature Components + +### Notification Components + +**Files**: + +- `Features/Notifications/NotificationBell.razor` +- `Features/Notifications/NotificationCenter.razor` +- `Features/Notifications/NotificationPreferences.razor` + +**Status**: ⚠️ These are placeholder components awaiting `INotificationService` implementation. + +#### NotificationBell Usage (Planned) + +```razor + + + + + +``` + +#### NotificationCenter Usage (Planned) + +```razor +@page "/notifications" + +

Notification Center

+ + +``` + +#### NotificationPreferences Usage (Planned) + +```razor +@page "/settings/notifications" + +

Notification Settings

+ + +``` + +--- + +## Testing Patterns + +### Basic Component Test + +```csharp +using Aquiis.UI.Shared.Components.Common; +using Bunit; +using FluentAssertions; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Components.Common; + +public class CardTests : TestContext +{ + [Fact] + public void Card_Renders_Title_Successfully() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Title, "Test Card") + ); + + // Assert + cut.Markup.Should().Contain("Test Card"); + } + + [Fact] + public void Card_Renders_ChildContent() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Title, "Card") + .AddChildContent("

Content

") + ); + + // Assert + cut.Markup.Should().Contain("

Content

"); + } +} +``` + +### Testing Components with HTML Content + +Use `AddChildContent()` for HTML markup: + +```csharp +[Fact] +public void FormField_Renders_Input_Control() +{ + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Email") + .AddChildContent("") + ); + + // Assert + cut.Markup.Should().Contain("( + new TestAuthStateProvider(authState)); + Services.AddSingleton( + new TestAuthorizationService()); + Services.AddSingleton( + new TestAuthorizationPolicyProvider()); + } + + [Fact] + public void SharedMainLayout_Renders_AuthorizedContent() + { + // Arrange & Act + var cut = RenderLayoutWithAuth(parameters => parameters + .AddChildContent("

Test content

") + ); + + // Assert + cut.Markup.Should().Contain("Test content"); + } + + // Helper method to wrap in CascadingAuthenticationState + private IRenderedComponent RenderLayoutWithAuth( + Action> parameters) + { + return Render(cascadingParams => + { + cascadingParams.AddChildContent(parameters); + }).FindComponent(); + } +} + +// Test authorization helper classes +public class TestAuthStateProvider : AuthenticationStateProvider +{ + private readonly Task _authState; + public TestAuthStateProvider(Task authState) + => _authState = authState; + public override Task GetAuthenticationStateAsync() + => _authState; +} + +public class TestAuthorizationService : IAuthorizationService +{ + public Task AuthorizeAsync( + ClaimsPrincipal user, object? resource, + IEnumerable requirements) + => Task.FromResult(AuthorizationResult.Success()); + + public Task AuthorizeAsync( + ClaimsPrincipal user, object? resource, string policyName) + => Task.FromResult(AuthorizationResult.Success()); +} + +public class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider +{ + public Task GetDefaultPolicyAsync() + => Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); + + public Task GetPolicyAsync(string policyName) + => Task.FromResult( + new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()); + + public Task GetFallbackPolicyAsync() + => Task.FromResult(null); +} +``` + +### Running Tests + +```bash +# Run all tests +cd /home/cisguru/Source/Aquiis +dotnet test 6-Tests/Aquiis.UI.Shared.Tests + +# Run specific test class +dotnet test 6-Tests/Aquiis.UI.Shared.Tests --filter "FullyQualifiedName~CardTests" + +# Run with detailed output +dotnet test 6-Tests/Aquiis.UI.Shared.Tests --logger "console;verbosity=detailed" +``` + +--- + +## Best Practices Summary + +1. **Component Parameters**: Always document with XML comments +2. **HTML Content**: Use `AddChildContent()` in tests for proper HTML rendering +3. **Authorization**: Wrap auth-aware components in `CascadingAuthenticationState` +4. **Customization**: Provide `CssClass` parameters for styling flexibility +5. **Defaults**: Set sensible default values for all parameters +6. **Events**: Use `EventCallback` for component events +7. **Testing**: Achieve >80% code coverage for all components +8. **Documentation**: Update this guide when adding new components + +--- + +## Version History + +- **v0.2.0** (January 2026) - Complete test suite, documentation +- **v0.1.0** (December 2025) - Initial component library release + +## Support + +- See [README.md](README.md) for architecture and conventions +- Check test files for usage examples +- Refer to implementation plan for roadmap diff --git a/3-Aquiis.UI.Shared/Components/Common/Card.razor b/3-Aquiis.UI.Shared/Components/Common/Card.razor new file mode 100644 index 0000000..eb33b98 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Common/Card.razor @@ -0,0 +1,73 @@ +@namespace Aquiis.UI.Shared.Components.Common + +
+ @if (HeaderContent != null || !string.IsNullOrEmpty(Title)) + { +
+ @if (HeaderContent != null) + { + @HeaderContent + } + else + { +
@Title
+ } +
+ } +
+ @ChildContent +
+ @if (FooterContent != null) + { + + } +
+ +@code { + /// + /// Card title (only used if HeaderContent is not provided) + /// + [Parameter] public string? Title { get; set; } + + /// + /// Whether the card should take full height of its container + /// + [Parameter] public bool FullHeight { get; set; } + + /// + /// Additional CSS classes for the card container + /// + [Parameter] public string CssClass { get; set; } = ""; + + /// + /// Additional CSS classes for the header + /// + [Parameter] public string HeaderCssClass { get; set; } = ""; + + /// + /// Additional CSS classes for the body + /// + [Parameter] public string BodyCssClass { get; set; } = ""; + + /// + /// Additional CSS classes for the footer + /// + [Parameter] public string FooterCssClass { get; set; } = ""; + + /// + /// Custom header content (overrides Title) + /// + [Parameter] public RenderFragment? HeaderContent { get; set; } + + /// + /// Main card body content + /// + [Parameter] public RenderFragment? ChildContent { get; set; } + + /// + /// Footer content + /// + [Parameter] public RenderFragment? FooterContent { get; set; } +} diff --git a/3-Aquiis.UI.Shared/Components/Common/DataTable.razor b/3-Aquiis.UI.Shared/Components/Common/DataTable.razor new file mode 100644 index 0000000..3c41c58 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Common/DataTable.razor @@ -0,0 +1,76 @@ +@namespace Aquiis.UI.Shared.Components.Common +@typeparam TItem + +
+ + @if (ShowHeader) + { + + + @HeaderTemplate + + + } + + @if (Items != null && Items.Any()) + { + foreach (var item in Items) + { + + @if (RowTemplate != null) + { + @RowTemplate(item) + } + + } + } + else + { + + + + } + +
+ @if (EmptyMessage != null) + { + @EmptyMessage + } + else + { + No data available. + } +
+
+ +@code { + /// + /// The collection of items to display + /// + [Parameter] public IEnumerable? Items { get; set; } + + /// + /// Template for the table header row + /// + [Parameter] public RenderFragment? HeaderTemplate { get; set; } + + /// + /// Template for each data row (receives the item as context) + /// + [Parameter] public RenderFragment? RowTemplate { get; set; } + + /// + /// Message or content to show when no items are available + /// + [Parameter] public RenderFragment? EmptyMessage { get; set; } + + /// + /// Whether to show the header row + /// + [Parameter] public bool ShowHeader { get; set; } = true; + + /// + /// Additional CSS classes for the table + /// + [Parameter] public string TableCssClass { get; set; } = "table-striped table-hover"; +} diff --git a/3-Aquiis.UI.Shared/Components/Common/FormField.razor b/3-Aquiis.UI.Shared/Components/Common/FormField.razor new file mode 100644 index 0000000..5637b0d --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Common/FormField.razor @@ -0,0 +1,51 @@ +@namespace Aquiis.UI.Shared.Components.Common + +
+ @if (!string.IsNullOrEmpty(Label)) + { + + } + @ChildContent + @if (!string.IsNullOrEmpty(HelpText)) + { +
@HelpText
+ } +
+ +@code { + /// + /// Label text for the form field + /// + [Parameter] public string? Label { get; set; } + + /// + /// Whether the field is required (adds asterisk to label) + /// + [Parameter] public bool Required { get; set; } + + /// + /// Help text to display below the input + /// + [Parameter] public string? HelpText { get; set; } + + /// + /// Additional CSS classes for the container div + /// + [Parameter] public string CssClass { get; set; } = ""; + + /// + /// Additional CSS classes for the label + /// + [Parameter] public string LabelCssClass { get; set; } = ""; + + /// + /// The input control content + /// + [Parameter] public RenderFragment? ChildContent { get; set; } +} diff --git a/3-Aquiis.UI.Shared/Components/Common/Modal.razor b/3-Aquiis.UI.Shared/Components/Common/Modal.razor new file mode 100644 index 0000000..7a59a71 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Common/Modal.razor @@ -0,0 +1,133 @@ +@namespace Aquiis.UI.Shared.Components.Common + +@if (IsVisible) +{ + +} + +@code { + /// + /// Controls the visibility of the modal + /// + [Parameter] public bool IsVisible { get; set; } + + /// + /// Modal title text (only used if HeaderContent is not provided) + /// + [Parameter] public string? Title { get; set; } + + /// + /// Modal size: sm, default (md), lg, xl + /// + [Parameter] public ModalSize Size { get; set; } = ModalSize.Default; + + /// + /// Modal position: default (top), centered + /// + [Parameter] public ModalPosition Position { get; set; } = ModalPosition.Top; + + /// + /// Whether to show the close button in header + /// + [Parameter] public bool ShowCloseButton { get; set; } = true; + + /// + /// Whether clicking the backdrop closes the modal + /// + [Parameter] public bool CloseOnBackdropClick { get; set; } = true; + + /// + /// Additional CSS classes for the header + /// + [Parameter] public string HeaderCssClass { get; set; } = ""; + + /// + /// Additional CSS classes for the body + /// + [Parameter] public string BodyCssClass { get; set; } = ""; + + /// + /// Additional CSS classes for the footer + /// + [Parameter] public string FooterCssClass { get; set; } = ""; + + /// + /// Custom header content (overrides Title) + /// + [Parameter] public RenderFragment? HeaderContent { get; set; } + + /// + /// Main modal body content + /// + [Parameter] public RenderFragment? ChildContent { get; set; } + + /// + /// Footer content (typically buttons) + /// + [Parameter] public RenderFragment? FooterContent { get; set; } + + /// + /// Event callback when modal is closed + /// + [Parameter] public EventCallback OnClose { get; set; } + + private string GetModalSizeClass() + { + return Size switch + { + ModalSize.Small => "modal-sm", + ModalSize.Large => "modal-lg", + ModalSize.ExtraLarge => "modal-xl", + _ => "" + }; + } + + private string GetModalPositionClass() + { + return Position == ModalPosition.Centered ? "modal-dialog-centered" : ""; + } + + private async Task HandleClose() + { + await OnClose.InvokeAsync(); + } + + private async Task HandleBackdropClick() + { + if (CloseOnBackdropClick) + { + await HandleClose(); + } + } +} diff --git a/3-Aquiis.UI.Shared/Components/Common/ModalPosition.cs b/3-Aquiis.UI.Shared/Components/Common/ModalPosition.cs new file mode 100644 index 0000000..b3836b9 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Common/ModalPosition.cs @@ -0,0 +1,7 @@ +namespace Aquiis.UI.Shared.Components.Common; + +public enum ModalPosition +{ + Top, + Centered +} diff --git a/3-Aquiis.UI.Shared/Components/Common/ModalSize.cs b/3-Aquiis.UI.Shared/Components/Common/ModalSize.cs new file mode 100644 index 0000000..46fcee0 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Common/ModalSize.cs @@ -0,0 +1,9 @@ +namespace Aquiis.UI.Shared.Components.Common; + +public enum ModalSize +{ + Small, + Default, + Large, + ExtraLarge +} diff --git a/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor b/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor new file mode 100644 index 0000000..039975d --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor @@ -0,0 +1,43 @@ +@inherits LayoutComponentBase +@namespace Aquiis.UI.Shared.Components.Layout +@using Microsoft.AspNetCore.Components.Authorization + +
+ + +
+
+ + + @NotAuthorizedContent + + + @AuthorizedHeaderContent + + +
+ +
+ @ChildContent +
+
+
+ +@FooterContent + +
+ An unhandled error has occurred. + Reload + 🗙 +
+ +@code { + [Parameter] public string Theme { get; set; } = "light"; + [Parameter] public RenderFragment? SidebarContent { get; set; } + [Parameter] public RenderFragment? NotAuthorizedContent { get; set; } + [Parameter] public RenderFragment? AuthorizedHeaderContent { get; set; } + [Parameter] public RenderFragment? FooterContent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } +} diff --git a/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor.css b/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor.css new file mode 100644 index 0000000..a5e98b0 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor.css @@ -0,0 +1,109 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; + min-height: 100vh; +} + +article.content { + background-color: var(--bs-body-bg) !important; + color: var(--bs-body-color) !important; +} + +.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/3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor b/3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor new file mode 100644 index 0000000..e69c61a --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor @@ -0,0 +1,290 @@ +@inject NotificationService NotificationService +@inject NavigationManager NavigationManager +@namespace Aquiis.UI.Shared.Features.Notifications +@using Aquiis.Application.Services + +Notification Bell + +@if (isLoading) +{ +
+ +
+} +else if (notifications.Count > 0) +{ + +} else { +
+ +
+} + + +@if (showNotificationModal && selectedNotification != null) +{ + +} + +@code { + [Parameter] public Func? GetEntityRoute { get; set; } + + 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; + + 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 && GetEntityRoute != null) + { + var route = 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(); + await NotificationService.MarkAllAsReadAsync(notifications); + } + + 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/3-Aquiis.UI.Shared/Features/Notifications/NotificationCenter.razor b/3-Aquiis.UI.Shared/Features/Notifications/NotificationCenter.razor new file mode 100644 index 0000000..ecd335c --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/Notifications/NotificationCenter.razor @@ -0,0 +1,640 @@ +@using Aquiis.Core.Constants +@inject NotificationService NotificationService +@inject NavigationManager NavigationManager +@namespace Aquiis.UI.Shared.Features.Notifications + +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 { + [Parameter] public Func? GetEntityRoute { get; set; } + + 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() + { + notifications = await NotificationService.GetUnreadNotificationsAsync(); + 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 && GetEntityRoute != null) + { + var route = 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/3-Aquiis.UI.Shared/Features/Notifications/NotificationPreferences.razor b/3-Aquiis.UI.Shared/Features/Notifications/NotificationPreferences.razor new file mode 100644 index 0000000..fc6939b --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/Notifications/NotificationPreferences.razor @@ -0,0 +1,400 @@ +@using Aquiis.Application.Services +@using Microsoft.JSInterop +@using PreferencesEntity = Aquiis.Core.Entities.NotificationPreferences +@inject NotificationService NotificationService +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@namespace Aquiis.UI.Shared.Features.Notifications + +
+ +
+
+

+ 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 { + [Parameter, EditorRequired] public dynamic ToastService { get; set; } = default!; + + 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/3-Aquiis.UI.Shared/Features/PropertiesManagement/Properties/PropertyList.razor b/3-Aquiis.UI.Shared/Features/PropertiesManagement/Properties/PropertyList.razor new file mode 100644 index 0000000..80f9762 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertiesManagement/Properties/PropertyList.razor @@ -0,0 +1,28 @@ +@using Aquiis.UI.Shared.Components.Common +@using Aquiis.Core.Entities +@using Aquiis.Application.Services + +@inject PropertyService PropertyService + +
+

Property List

+ + + Property + Status + + + @prop.Address + @prop.Status + + +
+ +@code { + private IEnumerable properties = new List(); + + protected override async Task OnInitializedAsync() + { + properties = await PropertyService.GetPropertiesWithRelationsAsync(); + } +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/README.md b/3-Aquiis.UI.Shared/README.md new file mode 100644 index 0000000..94c4d66 --- /dev/null +++ b/3-Aquiis.UI.Shared/README.md @@ -0,0 +1,645 @@ +# Aquiis.UI.Shared - Shared UI Component Library + +## Overview + +`Aquiis.UI.Shared` is a Razor Class Library (RCL) that provides reusable UI components, layouts, and assets shared between Aquiis.SimpleStart and Aquiis.Professional. This library eliminates code duplication and ensures a consistent user experience across all Aquiis products. + +## Purpose + +- **Single-Source Development**: Build UI components once, use in multiple products +- **Consistency**: Maintain uniform UI/UX across all Aquiis applications +- **Maintainability**: Fix bugs and add features in one place +- **Testing**: Comprehensive test coverage with bUnit (47 unit tests, 100% passing) +- **Scalability**: Clear architecture for adding new shared components + +## Architecture + +### Folder Structure + +``` +Aquiis.UI.Shared/ +├── Components/ +│ ├── Common/ # Generic, reusable UI components +│ │ ├── Modal.razor +│ │ ├── Card.razor +│ │ ├── DataTable.razor +│ │ └── FormField.razor +│ └── Layout/ # Layout components +│ └── SharedMainLayout.razor +├── Features/ # Feature-specific shared components +│ └── Notifications/ +│ ├── NotificationBell.razor +│ ├── NotificationCenter.razor +│ └── NotificationPreferences.razor +├── wwwroot/ +│ ├── css/ # Shared stylesheets +│ └── js/ # Shared JavaScript +└── 6-Tests/Aquiis.UI.Shared.Tests/ # Unit tests (bUnit) +``` + +### Design Principles + +1. **Components/Common**: Generic, reusable UI elements (modals, cards, tables, forms) +2. **Components/Layout**: Shared layout structures and navigation +3. **Features**: Domain-specific components organized by feature area +4. **Stateless by Default**: Components receive data via parameters +5. **Customizable**: Support CSS classes, custom content, and event callbacks + +## Dependencies + +- **Can Reference**: `Aquiis.Application`, `Aquiis.Core` +- **Can Reference**: Microsoft.AspNetCore.Components packages +- **Cannot Reference**: Product-specific projects (SimpleStart, Professional) + +## Getting Started + +### Adding the Shared Library to Your Product + +1. Add project reference: + + ```xml + + ``` + +2. Add using directive in `_Imports.razor`: + + ```razor + @using Aquiis.UI.Shared.Components.Common + @using Aquiis.UI.Shared.Components.Layout + @using Aquiis.UI.Shared.Features.Notifications + ``` + +3. Use components in your pages: + ```razor + + Modal content here + + ``` + +## Component Usage Guide + +### Modal Component + +**Purpose**: Display content in an overlay dialog + +**Basic Usage**: + +```razor + + +

Modal content goes here

+
+ + + + +
+``` + +**Parameters**: + +- `IsVisible` (bool): Controls visibility +- `Title` (string): Modal title +- `Size` (ModalSize): Small, Default, Large, ExtraLarge +- `Position` (ModalPosition): Top, Centered +- `ShowCloseButton` (bool): Show X button (default: true) +- `CloseOnBackdropClick` (bool): Close on backdrop click (default: true) +- `OnClose` (EventCallback): Called when modal closes +- `HeaderContent` (RenderFragment): Custom header (overrides Title) +- `ChildContent` (RenderFragment): Main modal body +- `FooterContent` (RenderFragment): Footer buttons/actions + +**Example - Confirmation Dialog**: + +```razor + + + Are you sure you want to delete this item? + + + + + + +``` + +### Card Component + +**Purpose**: Container for related content with header, body, and footer + +**Basic Usage**: + +```razor + + + Card body content + + +``` + +**Parameters**: + +- `Title` (string): Card title in header +- `FullHeight` (bool): Stretch to container height +- `CssClass` (string): Additional CSS classes +- `HeaderCssClass`, `BodyCssClass`, `FooterCssClass` (string): Section-specific classes +- `HeaderContent` (RenderFragment): Custom header (overrides Title) +- `ChildContent` (RenderFragment): Main card body +- `FooterContent` (RenderFragment): Card footer + +**Example - Dashboard Widget**: + +```razor + + +
    + @foreach (var activity in recentActivities) + { +
  • @activity.Description - @activity.Date
  • + } +
+
+ + View All + +
+``` + +### DataTable Component + +**Purpose**: Display tabular data with customizable headers and rows + +**Basic Usage**: + +```razor + + + Name + Email + Status + + + @user.Name + @user.Email + @user.Status + + +``` + +**Parameters**: + +- `Items` (IEnumerable): Data collection +- `HeaderTemplate` (RenderFragment): Table header row +- `RowTemplate` (RenderFragment): Template for each data row +- `EmptyMessage` (RenderFragment): Custom message when no data +- `ShowHeader` (bool): Display header (default: true) +- `TableCssClass` (string): CSS classes (default: "table-striped table-hover") + +**Example - With Empty State**: + +```razor + + + Order # + Customer + Total + Status + + + @order.OrderNumber + @order.CustomerName + @order.Total.ToString("C") + @order.Status + + +
+ +

No orders found

+
+
+
+``` + +### FormField Component + +**Purpose**: Consistent form field layout with label, input, and help text + +**Basic Usage**: + +```razor + + + +``` + +**Parameters**: + +- `Label` (string): Field label text +- `Required` (bool): Show required indicator (\*) +- `HelpText` (string): Help text below input +- `CssClass` (string): Additional container classes +- `LabelCssClass` (string): Label-specific classes +- `ChildContent` (RenderFragment): Input control + +**Example - Complete Form**: + +```razor + + + + + + + + + + + + + + + + + +``` + +### SharedMainLayout Component + +**Purpose**: Base layout structure with sidebar, header, and content areas + +**Basic Usage**: + +```razor +@inherits LayoutComponentBase +@layout SharedMainLayout + + + + + + + + + + @Body + + +
© 2026 Aquiis
+
+
+``` + +**Parameters**: + +- `Theme` (string): Theme name (e.g., "light", "dark") +- `SidebarContent` (RenderFragment): Navigation sidebar +- `AuthorizedHeaderContent` (RenderFragment): Header for authenticated users +- `NotAuthorizedContent` (RenderFragment): Header for anonymous users +- `ChildContent` (RenderFragment): Main page content +- `FooterContent` (RenderFragment): Footer content + +### Notification Components + +**Note**: Notification components are feature placeholders awaiting NotificationService implementation. + +**NotificationBell** - Notification indicator with dropdown +**NotificationCenter** - Full notification management page +**NotificationPreferences** - User notification settings + +## Adding New Shared Components + +### 1. Determine Component Category + +- **Common**: Generic UI element (button, input, dialog) → `Components/Common/` +- **Layout**: Layout structure (header, footer, nav) → `Components/Layout/` +- **Feature**: Domain-specific (notifications, reports) → `Features/[FeatureName]/` + +### 2. Create Component File + +```bash +cd 3-Aquiis.UI.Shared +# For common components +touch Components/Common/YourComponent.razor +# For feature components +mkdir -p Features/YourFeature +touch Features/YourFeature/YourComponent.razor +``` + +### 3. Define Component + +```razor +@namespace Aquiis.UI.Shared.Components.Common + +
+ @ChildContent +
+ +@code { + /// + /// Additional CSS classes to apply + /// + [Parameter] public string CssClass { get; set; } = ""; + + /// + /// Content to render inside the component + /// + [Parameter] public RenderFragment? ChildContent { get; set; } +} +``` + +### 4. Write Tests + +```bash +cd 6-Tests/Aquiis.UI.Shared.Tests +touch Components/Common/YourComponentTests.cs +``` + +```csharp +using Aquiis.UI.Shared.Components.Common; +using Bunit; +using FluentAssertions; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Components.Common; + +public class YourComponentTests : TestContext +{ + [Fact] + public void YourComponent_Renders_Successfully() + { + // Arrange & Act + var cut = Render(parameters => parameters + .AddChildContent("Test Content") + ); + + // Assert + cut.Markup.Should().Contain("Test Content"); + cut.Markup.Should().Contain("your-component"); + } +} +``` + +### 5. Update Product \_Imports.razor + +```razor +@using Aquiis.UI.Shared.Components.Common +``` + +### 6. Use in Product Pages + +```razor +Content here +``` + +## Component Parameter Conventions + +### Standard Parameters + +- **CssClass**: Additional CSS classes to apply to root element +- **ChildContent**: Main content inside component +- **OnClick / OnChange**: Event callbacks +- **IsVisible / IsOpen**: Visibility toggles + +### Naming Conventions + +- Use **PascalCase** for all parameters +- Prefix boolean parameters with **Is/Has/Show** (e.g., `IsVisible`, `HasError`, `ShowHeader`) +- Event callbacks use **On** prefix (e.g., `OnClose`, `OnSave`, `OnItemSelected`) + +### Documentation + +Add XML documentation for all parameters: + +```csharp +/// +/// The title displayed in the modal header +/// +[Parameter] public string Title { get; set; } = ""; +``` + +## Styling Guidelines + +### CSS Scoping + +Components use CSS isolation (`ComponentName.razor.css`) to prevent style leakage: + +```css +/* SharedMainLayout.razor.css */ +.page { + display: flex; + height: 100vh; +} +``` + +### Bootstrap Integration + +Components use Bootstrap 5 classes: + +- Layout: `container`, `row`, `col` +- Spacing: `m-*`, `p-*`, `mb-3` +- Display: `d-flex`, `d-none`, `d-block` +- Components: `btn`, `card`, `modal`, `table` + +### Custom Styling + +Allow customization via `CssClass` parameters: + +```razor + + Content + +``` + +## Testing Requirements + +All shared components **must** have unit tests. + +### Test Setup + +Tests use: + +- **xUnit**: Test framework +- **bUnit 2.4.2**: Blazor component testing +- **FluentAssertions 8.8.0**: Readable assertions + +### Test Template + +```csharp +public class ComponentTests : TestContext +{ + [Fact] + public void Component_Renders_Parameter_Value() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.ParameterName, "value") + ); + + // Assert + cut.Markup.Should().Contain("value"); + } +} +``` + +### Running Tests + +```bash +cd /home/cisguru/Source/Aquiis +dotnet test 6-Tests/Aquiis.UI.Shared.Tests +``` + +**Current Status**: 47 tests, 100% passing + +## Best Practices + +### 1. Keep Components Generic + +❌ **Avoid**: Product-specific logic + +```razor +@if (IsSimpleStartEdition) +{ +
SimpleStart-only content
+} +``` + +✅ **Better**: Use parameters for customization + +```razor +@if (ShowAdvancedFeatures) +{ +
Advanced content
+} +``` + +### 2. Support Composition + +Components should support both content and customization: + +```razor + + +

Custom Header

+
+ + Main content + +
+``` + +### 3. Provide Sensible Defaults + +```csharp +[Parameter] public string CssClass { get; set; } = ""; +[Parameter] public bool ShowHeader { get; set; } = true; +[Parameter] public ModalSize Size { get; set; } = ModalSize.Default; +``` + +### 4. Document Complex Behavior + +Use XML comments and examples: + +```csharp +/// +/// Displays a modal dialog overlay. +/// The modal can be closed by clicking the close button, pressing Escape, +/// or clicking the backdrop (if CloseOnBackdropClick is true). +/// +``` + +## Troubleshooting + +### Build Errors + +**Problem**: Component not found + +``` +error CS0246: The type or namespace name 'Modal' could not be found +``` + +**Solution**: Add using directive to `_Imports.razor`: + +```razor +@using Aquiis.UI.Shared.Components.Common +``` + +### CSS Not Applying + +**Problem**: Component styles not showing + +**Solution**: Ensure scoped CSS files are included: + +1. Check `ComponentName.razor.css` exists +2. Verify CSS isolation is enabled in project +3. Rebuild the project + +### Test Failures + +**Problem**: AuthorizeView components fail in tests + +**Solution**: Add authorization context: + +```csharp +public class LayoutTests : TestContext +{ + public LayoutTests() + { + // Add test authorization + Services.AddSingleton( + new TestAuthStateProvider()); + } +} +``` + +## Contributing + +### Before Submitting + +1. ✅ Add XML documentation to all public members +2. ✅ Write unit tests (target >80% coverage) +3. ✅ Test in both SimpleStart and Professional +4. ✅ Update this README if adding new patterns +5. ✅ Follow existing naming conventions + +### Review Checklist + +- [ ] Component is generic and reusable +- [ ] Parameters have XML documentation +- [ ] Unit tests cover main scenarios +- [ ] CSS uses scoped styles +- [ ] Bootstrap classes used where appropriate +- [ ] No product-specific dependencies + +## Resources + +- [Blazor Component Documentation](https://learn.microsoft.com/aspnet/core/blazor/components/) +- [bUnit Testing Guide](https://bunit.dev/) +- [Bootstrap 5 Documentation](https://getbootstrap.com/docs/5.0/) +- [Razor Class Libraries](https://learn.microsoft.com/aspnet/core/razor-pages/ui-class) + +## Version History + +- **v0.2.0** (January 2026) + - ✅ Phase 3.5: Features structure (Notifications moved to Features/) + - ✅ Phase 6: Complete test suite (47 tests, 100% passing) + - ✅ Phase 7: Documentation and guidelines +- **v0.1.0** (December 2025) + - Initial RCL creation + - SharedMainLayout, Common components (Modal, Card, DataTable, FormField) + - Notification placeholders + - Static assets (CSS, JS) + +## Support + +For questions or issues: + +1. Check this README +2. Review existing component implementations +3. Consult the test suite for usage examples +4. Refer to the [11-Shared-UI-Implementation-Plan.md](../../Documents/Orion/Projects/Aquiis/Plans%20Pending%20Scope/11-Shared-UI-Implementation-Plan.md) diff --git a/3-Aquiis.UI.Shared/_Imports.razor b/3-Aquiis.UI.Shared/_Imports.razor new file mode 100644 index 0000000..ab14507 --- /dev/null +++ b/3-Aquiis.UI.Shared/_Imports.razor @@ -0,0 +1,9 @@ +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.JSInterop +@using Aquiis.Application.Services +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.Core.Utilities diff --git a/Aquiis.Professional/wwwroot/css/organization-switcher.css b/3-Aquiis.UI.Shared/wwwroot/css/organization-switcher.css similarity index 100% rename from Aquiis.Professional/wwwroot/css/organization-switcher.css rename to 3-Aquiis.UI.Shared/wwwroot/css/organization-switcher.css diff --git a/Aquiis.Professional/wwwroot/js/theme.js b/3-Aquiis.UI.Shared/wwwroot/js/theme.js similarity index 100% rename from Aquiis.Professional/wwwroot/js/theme.js rename to 3-Aquiis.UI.Shared/wwwroot/js/theme.js diff --git a/Aquiis.SimpleStart/.gitattributes b/4-Aquiis.SimpleStart/.gitattributes similarity index 100% rename from Aquiis.SimpleStart/.gitattributes rename to 4-Aquiis.SimpleStart/.gitattributes diff --git a/Aquiis.SimpleStart/.github/copilot-instructions.md b/4-Aquiis.SimpleStart/.github/copilot-instructions.md similarity index 100% rename from Aquiis.SimpleStart/.github/copilot-instructions.md rename to 4-Aquiis.SimpleStart/.github/copilot-instructions.md diff --git a/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj b/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj new file mode 100644 index 0000000..1f49b85 --- /dev/null +++ b/4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj @@ -0,0 +1,56 @@ + + + + net10.0 + enable + enable + aspnet-Aquiis.SimpleStart-c69b6efe-bb20-41de-8cba-044207ebdce1 + true + 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/4-Aquiis.SimpleStart/Data/Migrations/20260104205913_InitialCreate.Designer.cs b/4-Aquiis.SimpleStart/Data/Migrations/20260104205913_InitialCreate.Designer.cs new file mode 100644 index 0000000..52c638b --- /dev/null +++ b/4-Aquiis.SimpleStart/Data/Migrations/20260104205913_InitialCreate.Designer.cs @@ -0,0 +1,294 @@ +// +using System; +using Aquiis.SimpleStart.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.SimpleStart.Data.Migrations +{ + [DbContext(typeof(SimpleStartDbContext))] + [Migration("20260104205913_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.SimpleStart.Entities.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("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.SimpleStart.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.SimpleStart.Entities.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.SimpleStart.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.SimpleStart.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/4-Aquiis.SimpleStart/Data/Migrations/20260104205913_InitialCreate.cs b/4-Aquiis.SimpleStart/Data/Migrations/20260104205913_InitialCreate.cs new file mode 100644 index 0000000..7839b88 --- /dev/null +++ b/4-Aquiis.SimpleStart/Data/Migrations/20260104205913_InitialCreate.cs @@ -0,0 +1,230 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.SimpleStart.Data.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: "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.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); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/4-Aquiis.SimpleStart/Data/Migrations/SimpleStartDbContextModelSnapshot.cs b/4-Aquiis.SimpleStart/Data/Migrations/SimpleStartDbContextModelSnapshot.cs new file mode 100644 index 0000000..c6e8961 --- /dev/null +++ b/4-Aquiis.SimpleStart/Data/Migrations/SimpleStartDbContextModelSnapshot.cs @@ -0,0 +1,291 @@ +// +using System; +using Aquiis.SimpleStart.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.SimpleStart.Data.Migrations +{ + [DbContext(typeof(SimpleStartDbContext))] + partial class SimpleStartDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Aquiis.SimpleStart.Entities.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("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.SimpleStart.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.SimpleStart.Entities.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.SimpleStart.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.SimpleStart.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/4-Aquiis.SimpleStart/Data/SimpleStartDbContext.cs b/4-Aquiis.SimpleStart/Data/SimpleStartDbContext.cs new file mode 100644 index 0000000..0d7f5fc --- /dev/null +++ b/4-Aquiis.SimpleStart/Data/SimpleStartDbContext.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Aquiis.SimpleStart.Entities; + +namespace Aquiis.SimpleStart.Data; + +/// +/// SimpleStart database context for Identity management. +/// Handles all ASP.NET Core Identity tables and SimpleStart-specific user data. +/// Shares the same database as ApplicationDbContext using the same connection string. +/// +public class SimpleStartDbContext : IdentityDbContext +{ + public SimpleStartDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // Identity table configuration is handled by base IdentityDbContext + // Add any SimpleStart-specific user configurations here if needed + } +} diff --git a/4-Aquiis.SimpleStart/Entities/ApplicationUser.cs b/4-Aquiis.SimpleStart/Entities/ApplicationUser.cs new file mode 100644 index 0000000..c351cbd --- /dev/null +++ b/4-Aquiis.SimpleStart/Entities/ApplicationUser.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Identity; + +namespace Aquiis.SimpleStart.Entities; + +/// +/// SimpleStart user entity for authentication and authorization. +/// Extends IdentityUser with SimpleStart-specific properties. +/// +public class ApplicationUser : IdentityUser +{ + /// + /// The currently active organization ID for this user session. + /// + public Guid ActiveOrganizationId { get; set; } = Guid.Empty; + + /// + /// The primary organization ID this user belongs to. + /// DEPRECATED in multi-org scenarios - use ActiveOrganizationId instead. + /// + public Guid OrganizationId { get; set; } = Guid.Empty; + + /// + /// User's first name. + /// + public string FirstName { get; set; } = string.Empty; + + /// + /// User's last name. + /// + public string LastName { get; set; } = string.Empty; + + /// + /// The timestamp of the user's most recent login. + /// + public DateTime? LastLoginDate { get; set; } + + /// + /// The timestamp of the user's previous login (before LastLoginDate). + /// + public DateTime? PreviousLoginDate { get; set; } + + /// + /// Total number of times the user has logged in. + /// + public int LoginCount { get; set; } = 0; + + /// + /// The IP address from the user's last login. + /// + public string? LastLoginIP { get; set; } +} diff --git a/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs new file mode 100644 index 0000000..41753e3 --- /dev/null +++ b/4-Aquiis.SimpleStart/Extensions/ElectronServiceExtensions.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore; +using Aquiis.Core.Interfaces; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Application; // ✅ Application facade +using Aquiis.Application.Services; +using Aquiis.SimpleStart.Data; +using Aquiis.SimpleStart.Entities; +using Aquiis.SimpleStart.Services; + +namespace Aquiis.SimpleStart.Extensions; + +/// +/// Extension methods for configuring Electron-specific services for SimpleStart. +/// +public static class ElectronServiceExtensions +{ + /// + /// Adds all Electron-specific infrastructure services including database, identity, and path services. + /// + /// The service collection. + /// The application configuration. + /// The service collection for chaining. + public static IServiceCollection AddElectronServices( + this IServiceCollection services, + IConfiguration configuration) + { + // Register path service + services.AddScoped(); + + // Get connection string using the path service + var connectionString = GetElectronConnectionString(configuration).GetAwaiter().GetResult(); + + // ✅ Register Application layer (includes Infrastructure internally) + services.AddApplication(connectionString); + + // Register Identity database context (SimpleStart-specific) + services.AddDbContext(options => + options.UseSqlite(connectionString)); + + // Register DatabaseService now that both contexts are available + services.AddScoped(sp => + new DatabaseService( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>())); + + services.AddDatabaseDeveloperPageExceptionFilter(); + + // Configure Identity with Electron-specific settings + services.AddIdentity(options => { + // For desktop app, simplify registration (email confirmation can be enabled later via settings) + options.SignIn.RequireConfirmedAccount = false; // Electron mode + options.Password.RequireDigit = true; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = true; + options.Password.RequireLowercase = true; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // Configure cookie authentication for Electron + services.ConfigureApplicationCookie(options => + { + options.LoginPath = "/Account/Login"; + options.LogoutPath = "/Account/Logout"; + options.AccessDeniedPath = "/Account/AccessDenied"; + + // For Electron desktop app, use longer cookie lifetime + options.ExpireTimeSpan = TimeSpan.FromDays(30); + options.SlidingExpiration = true; + }); + + return services; + } + + /// + /// Gets the connection string for Electron mode using the path service. + /// + private static async Task GetElectronConnectionString(IConfiguration configuration) + { + var pathService = new ElectronPathService(configuration); + return await pathService.GetConnectionStringAsync(configuration); + } +} diff --git a/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs new file mode 100644 index 0000000..363ce47 --- /dev/null +++ b/4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore; +using Aquiis.Core.Interfaces; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Application; // ✅ Application facade +using Aquiis.Application.Services; +using Aquiis.SimpleStart.Data; +using Aquiis.SimpleStart.Entities; +using Aquiis.SimpleStart.Services; + +namespace Aquiis.SimpleStart.Extensions; + +/// +/// Extension methods for configuring Web-specific services for SimpleStart. +/// +public static class WebServiceExtensions +{ + /// + /// Adds all Web-specific infrastructure services including database and identity. + /// + /// The service collection. + /// The application configuration. + /// The service collection for chaining. + public static IServiceCollection AddWebServices( + this IServiceCollection services, + IConfiguration configuration) + { + // Register path service + services.AddScoped(); + + // Get connection string from configuration + var connectionString = configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + + // ✅ Register Application layer (includes Infrastructure internally) + services.AddApplication(connectionString); + + // Register Identity database context (SimpleStart-specific) + services.AddDbContext(options => + options.UseSqlite(connectionString)); + + // Register DatabaseService now that both contexts are available + services.AddScoped(sp => + new DatabaseService( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>())); + + services.AddDatabaseDeveloperPageExceptionFilter(); + + // Configure Identity with Web-specific settings + services.AddIdentity(options => { + // For web app, require confirmed email + options.SignIn.RequireConfirmedAccount = true; + options.Password.RequireDigit = true; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = true; + options.Password.RequireLowercase = true; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // Configure cookie authentication for Web + services.ConfigureApplicationCookie(options => + { + options.LoginPath = "/Account/Login"; + options.LogoutPath = "/Account/Logout"; + options.AccessDeniedPath = "/Account/AccessDenied"; + }); + + return services; + } +} diff --git a/4-Aquiis.SimpleStart/Features/Administration/Application/Pages/DailyReport.razor b/4-Aquiis.SimpleStart/Features/Administration/Application/Pages/DailyReport.razor new file mode 100644 index 0000000..5db5067 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Application/Pages/DailyReport.razor @@ -0,0 +1,148 @@ +@page "/administration/application/dailyreport" + +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Application/Pages/InitializeSchemaVersion.razor b/4-Aquiis.SimpleStart/Features/Administration/Application/Pages/InitializeSchemaVersion.razor new file mode 100644 index 0000000..b68a944 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Application/Pages/InitializeSchemaVersion.razor @@ -0,0 +1,64 @@ +@page "/administration/application/initialize-schema" +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Application/Pages/ManageDatabase.razor b/4-Aquiis.SimpleStart/Features/Administration/Application/Pages/ManageDatabase.razor new file mode 100644 index 0000000..1852039 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Application/Pages/ManageDatabase.razor @@ -0,0 +1,778 @@ +@page "/administration/application/database" +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Constants +@using Aquiis.Core.Interfaces +@using Microsoft.AspNetCore.Authorization +@using Microsoft.Extensions.Options +@using ElectronNET.API +@inject DatabaseBackupService BackupService +@inject IPathService PathService +@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 PathService.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 PathService.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.SimpleStart/Features/Administration/Dashboard.razor b/4-Aquiis.SimpleStart/Features/Administration/Dashboard.razor similarity index 100% rename from Aquiis.SimpleStart/Features/Administration/Dashboard.razor rename to 4-Aquiis.SimpleStart/Features/Administration/Dashboard.razor diff --git a/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/CreateOrganization.razor b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/CreateOrganization.razor new file mode 100644 index 0000000..ac988c8 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/CreateOrganization.razor @@ -0,0 +1,197 @@ +@page "/administration/organizations/create" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor new file mode 100644 index 0000000..e5f3d50 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor @@ -0,0 +1,296 @@ +@page "/administration/organizations/edit/{Id:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor new file mode 100644 index 0000000..70ad1b3 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor @@ -0,0 +1,454 @@ +@page "/administration/organizations/{Id:guid}/users" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.SimpleStart.Shared.Components.Account +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor new file mode 100644 index 0000000..061475c --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor @@ -0,0 +1,214 @@ +@page "/administration/organizations" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Components.Shared +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor new file mode 100644 index 0000000..d06a445 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor @@ -0,0 +1,344 @@ +@page "/administration/organizations/view/{Id:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.SimpleStart.Shared.Components.Account +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor new file mode 100644 index 0000000..0edf1f2 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor @@ -0,0 +1,378 @@ +@page "/administration/settings/calendar" +@using Aquiis.Core.Entities +@using CalendarSettingsEntity = Aquiis.Core.Entities.CalendarSettings +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.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/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/EmailSettings.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/EmailSettings.razor new file mode 100644 index 0000000..0bc0866 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/EmailSettings.razor @@ -0,0 +1,454 @@ +@page "/administration/settings/email" +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Components.Account.Pages.Manage +@using SocketIOClient.Messages +@using System.ComponentModel.DataAnnotations +@inject EmailSettingsService EmailSettingsService +@inject EmailService 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 EmailStats? 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 GetEmailStatsAsync is implemented + if (settings.IsEmailEnabled) + { + stats = await EmailService.GetEmailStatsAsync(); + } + } + + 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/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/LateFeeSettings.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/LateFeeSettings.razor new file mode 100644 index 0000000..f9dd31b --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/LateFeeSettings.razor @@ -0,0 +1,439 @@ +@page "/administration/settings/latefees" +@using Aquiis.Core.Constants +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor new file mode 100644 index 0000000..4058714 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor @@ -0,0 +1,566 @@ +@page "/administration/settings/organization" + +@using Aquiis.Core.Entities +@using OrganizationSettingsEntity = Aquiis.Core.Entities.OrganizationSettings +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/SMSSettings.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/SMSSettings.razor new file mode 100644 index 0000000..bc1ada1 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/SMSSettings.razor @@ -0,0 +1,417 @@ +@page "/administration/settings/sms" +@using Aquiis.Application.Services +@using SocketIOClient.Messages +@using System.ComponentModel.DataAnnotations +@inject SMSSettingsService SMSSettingsService +@inject SMSService SMSService + +@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 SMSStats? 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 GetSMSStatsAsync is implemented + if (settings.IsSMSEnabled) + { + stats = await SMSService.GetSMSStatsAsync(); + } + } + + 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/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor new file mode 100644 index 0000000..93ecea1 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor @@ -0,0 +1,464 @@ +@page "/administration/settings/services" +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Infrastructure.Data +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor b/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor new file mode 100644 index 0000000..699877a --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor @@ -0,0 +1,612 @@ +@page "/administration/users/manage" + +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Components.Shared +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Constants +@using Microsoft.AspNetCore.Identity +@using Microsoft.EntityFrameworkCore +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Aquiis.SimpleStart.Shared.Components.Account +@using Aquiis.SimpleStart.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/4-Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor new file mode 100644 index 0000000..2c40516 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor @@ -0,0 +1,311 @@ +@page "/Administration/Users/Create" + +@using Aquiis.SimpleStart.Shared.Components.Account +@using Aquiis.Core.Constants +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.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/4-Aquiis.SimpleStart/Features/Administration/Users/View.razor b/4-Aquiis.SimpleStart/Features/Administration/Users/View.razor new file mode 100644 index 0000000..6230093 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Administration/Users/View.razor @@ -0,0 +1,508 @@ +@page "/administration/users/view/{UserId}" + +@using Aquiis.SimpleStart.Shared.Components.Shared +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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.SimpleStart.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; } = default!; + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + 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/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor b/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor new file mode 100644 index 0000000..064c6a1 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor @@ -0,0 +1,1670 @@ +@page "/Calendar" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Utilities +@using Aquiis.Core.Constants +@using Aquiis.SimpleStart.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/{request.Id}"); + } + + private void ShowPropertyDetail(Property property) + { + // Navigate to property detail page + Navigation.NavigateTo($"/propertymanagement/properties/{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("/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/4-Aquiis.SimpleStart/Features/Calendar/CalendarListView.razor b/4-Aquiis.SimpleStart/Features/Calendar/CalendarListView.razor new file mode 100644 index 0000000..ba124cc --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Calendar/CalendarListView.razor @@ -0,0 +1,877 @@ +@page "/Calendar/ListView" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Utilities +@using Aquiis.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("/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/4-Aquiis.SimpleStart/Features/Notifications/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/Notifications/Pages/Index.razor new file mode 100644 index 0000000..a0be3d9 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Notifications/Pages/Index.razor @@ -0,0 +1,8 @@ +@page "/notifications" +@using Aquiis.SimpleStart.Shared.Services +@attribute [OrganizationAuthorize] +@rendermode InteractiveServer + +Notification Center + + diff --git a/4-Aquiis.SimpleStart/Features/Notifications/Pages/Preferences.razor b/4-Aquiis.SimpleStart/Features/Notifications/Pages/Preferences.razor new file mode 100644 index 0000000..9c78fda --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/Notifications/Pages/Preferences.razor @@ -0,0 +1,9 @@ +@page "/notifications/preferences" +@using Aquiis.SimpleStart.Shared.Services +@inject ToastService ToastService +@attribute [OrganizationAuthorize] +@rendermode InteractiveServer + +Notification Preferences - Aquiis + + diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Edit.razor new file mode 100644 index 0000000..4a66b6e --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Edit.razor @@ -0,0 +1,616 @@ +@page "/propertymanagement/prospects/{ProspectId:guid}/submit-application" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Validation +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Application.Services.Workflows +@using Aquiis.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.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Index.razor new file mode 100644 index 0000000..90eeb6c --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Index.razor @@ -0,0 +1,250 @@ +@page "/propertymanagement/applications" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/View.razor new file mode 100644 index 0000000..33a5503 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/View.razor @@ -0,0 +1,1177 @@ +@page "/propertymanagement/applications/{ApplicationId:guid}/view" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Application.Services.Workflows +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/ChecklistTemplates/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/ChecklistTemplates/Edit.razor new file mode 100644 index 0000000..c874469 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/ChecklistTemplates/Edit.razor @@ -0,0 +1,344 @@ +@page "/propertymanagement/checklists/templates/edit/{TemplateId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/ChecklistTemplates/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/ChecklistTemplates/Index.razor new file mode 100644 index 0000000..04f2d70 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/ChecklistTemplates/Index.razor @@ -0,0 +1,349 @@ +@page "/propertymanagement/checklists/templates" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor new file mode 100644 index 0000000..71d846f --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor @@ -0,0 +1,176 @@ +@page "/propertymanagement/checklists" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor new file mode 100644 index 0000000..4ec0c1a --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor @@ -0,0 +1,713 @@ +@page "/propertymanagement/checklists/complete/{ChecklistId:guid}" +@page "/propertymanagement/checklists/complete/new" + +@using Aquiis.Core.Entities +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor new file mode 100644 index 0000000..74354ec --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor @@ -0,0 +1,352 @@ +@page "/propertymanagement/checklists/create" + +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor new file mode 100644 index 0000000..554d65b --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor @@ -0,0 +1,368 @@ +@page "/propertymanagement/checklists/mychecklists" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor new file mode 100644 index 0000000..3dddfdc --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor @@ -0,0 +1,588 @@ +@page "/propertymanagement/checklists/view/{ChecklistId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Core.Constants +@using Aquiis.Application.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/Documents.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/Documents.razor new file mode 100644 index 0000000..889cc6d --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/Documents.razor @@ -0,0 +1,586 @@ +@page "/propertymanagement/documents" + +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.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/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor new file mode 100644 index 0000000..3d9b46e --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor @@ -0,0 +1,347 @@ +@page "/propertymanagement/leases/{LeaseId:guid}/documents" + +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.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/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/_Imports.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/_Imports.razor new file mode 100644 index 0000000..6aeb0b5 --- /dev/null +++ b/4-Aquiis.SimpleStart/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.SimpleStart +@using Aquiis.Infrastructure.Data +@using Aquiis.Core.Entities +@using System.ComponentModel.DataAnnotations diff --git a/Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor diff --git a/Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionSectionView.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/InspectionSectionView.razor similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionSectionView.razor rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/InspectionSectionView.razor diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Create.razor new file mode 100644 index 0000000..bfec3fd --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Create.razor @@ -0,0 +1,552 @@ +@page "/propertymanagement/inspections/create" +@page "/propertymanagement/inspections/create/{PropertyId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Validation +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Schedule.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Schedule.razor new file mode 100644 index 0000000..6b69da9 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Schedule.razor @@ -0,0 +1,323 @@ +@page "/propertymanagement/inspections/schedule" + +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.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/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/View.razor new file mode 100644 index 0000000..c165f71 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/View.razor @@ -0,0 +1,426 @@ +@page "/propertymanagement/inspections/view/{InspectionId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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.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/{inspection.PropertyId}"); + } + else + { + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Create.razor new file mode 100644 index 0000000..b022f89 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Create.razor @@ -0,0 +1,317 @@ +@page "/propertymanagement/invoices/create" +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Edit.razor new file mode 100644 index 0000000..0dc81cd --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Edit.razor @@ -0,0 +1,396 @@ +@page "/propertymanagement/invoices/edit/{Id:guid}" + +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.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/{Id}"); + } + + private void ViewLease() + { + if (invoice?.LeaseId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Index.razor new file mode 100644 index 0000000..7f12872 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Index.razor @@ -0,0 +1,592 @@ +@page "/propertymanagement/invoices" +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.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/{id}"); + } + + private void EditInvoice(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/invoices/{id}/edit"); + } + + 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/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/View.razor new file mode 100644 index 0000000..bb72d9b --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/View.razor @@ -0,0 +1,408 @@ +@page "/propertymanagement/invoices/view/{Id:guid}" +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.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/{Id}/edit"); + } + + private void RecordPayment() + { + NavigationManager.NavigateTo($"/propertymanagement/payments/create?invoiceId={Id}"); + } + + private void ViewLease() + { + if (invoice?.LeaseId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{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.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/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/Accept.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/Accept.razor new file mode 100644 index 0000000..7f820b1 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/Accept.razor @@ -0,0 +1,637 @@ +@page "/propertymanagement/leases/{LeaseId:guid}/accept" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/Create.razor new file mode 100644 index 0000000..5b1912d --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/Create.razor @@ -0,0 +1,449 @@ +@page "/propertymanagement/applications/{ApplicationId:guid}/generate-lease-offer" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Application.Services.Workflows +@using Aquiis.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.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/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/Index.razor new file mode 100644 index 0000000..3e18f4b --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/Index.razor @@ -0,0 +1,165 @@ +@page "/propertymanagement/leaseoffers" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/View.razor new file mode 100644 index 0000000..9fc5926 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/View.razor @@ -0,0 +1,574 @@ +@page "/propertymanagement/leaseoffers/view/{Id:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Application.Services.Workflows +@using Aquiis.Infrastructure.Data +@using Aquiis.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/{result.Data.Id}/view"); + } + } + 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/{leaseOffer.ConvertedLeaseId.Value}/view"); + } + } + + private void BackToApplication() + { + if (leaseOffer?.RentalApplicationId != null) + { + Navigation.NavigateTo($"/propertymanagement/applications/{leaseOffer.RentalApplicationId}/view"); + } + 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/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Create.razor new file mode 100644 index 0000000..3e6b16b --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Create.razor @@ -0,0 +1,383 @@ +@page "/propertymanagement/leases/create" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Edit.razor new file mode 100644 index 0000000..fd234a1 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Edit.razor @@ -0,0 +1,357 @@ +@page "/propertymanagement/leases/edit/{Id:guid}" + +@using Aquiis.Core.Entities +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Web +@using Aquiis.SimpleStart.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/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Index.razor new file mode 100644 index 0000000..4639e55 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Index.razor @@ -0,0 +1,837 @@ +@page "/propertymanagement/leases" + +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.EntityFrameworkCore +@using Aquiis.Infrastructure.Data +@using Aquiis.Core.Entities +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.SimpleStart.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/{id}"); + } + + private void EditLease(Guid id) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{id}/edit"); + } + + 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/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor new file mode 100644 index 0000000..dee2f21 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor @@ -0,0 +1,1263 @@ +@page "/propertymanagement/leases/{Id:guid}/view" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Validation +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Web +@using Aquiis.SimpleStart.Features.PropertyManagement +@using System.ComponentModel.DataAnnotations +@using Aquiis.Application.Services +@using Aquiis.Application.Services.Workflows +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/{Id}/edit"); + } + + 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/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor new file mode 100644 index 0000000..f66997f --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor @@ -0,0 +1,354 @@ +@page "/propertymanagement/maintenance/create/{PropertyId:int?}" +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor new file mode 100644 index 0000000..17f15b2 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.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/{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/{Id}"); + } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor new file mode 100644 index 0000000..c0f61d9 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.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/{requestId}"); + } + + private void ViewProperty(Guid propertyId) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/{propertyId}"); + } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor new file mode 100644 index 0000000..a6a78a5 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor @@ -0,0 +1,309 @@ +@page "/propertymanagement/maintenance/view/{Id:guid}" + +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/{Id}/edit"); + } + + private void ViewProperty() + { + if (maintenanceRequest?.PropertyId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/{maintenanceRequest.PropertyId}"); + } + } + + private void GoBack() + { + NavigationManager.NavigateTo("/propertymanagement/maintenance"); + } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor new file mode 100644 index 0000000..538a4b3 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor @@ -0,0 +1,4 @@ +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Microsoft.AspNetCore.Authorization diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/CreatePayment.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Create.razor similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/CreatePayment.razor rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Create.razor diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/EditPayment.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Edit.razor similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/EditPayment.razor rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Edit.razor diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Index.razor new file mode 100644 index 0000000..2eb4402 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Index.razor @@ -0,0 +1,492 @@ +@page "/propertymanagement/payments" +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.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/{id}"); + } + + private void EditPayment(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/payments/{id}/edit"); + } + + 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/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/View.razor new file mode 100644 index 0000000..e04acdc --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/View.razor @@ -0,0 +1,417 @@ +@page "/propertymanagement/payments/view/{PaymentId:guid}" + +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.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/{PaymentId}/edit"); + } + + 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.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/_Imports.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/_Imports.razor new file mode 100644 index 0000000..74a1e88 --- /dev/null +++ b/4-Aquiis.SimpleStart/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.SimpleStart +@using Aquiis.Infrastructure.Data +@using Aquiis.Core.Entities diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor new file mode 100644 index 0000000..ad962d9 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor @@ -0,0 +1,260 @@ +@page "/propertymanagement/properties/create" +@using Aquiis.Core.Constants +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor new file mode 100644 index 0000000..13454ba --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor @@ -0,0 +1,399 @@ +@page "/propertymanagement/properties/edit/{PropertyId:guid}" + +@using System.ComponentModel.DataAnnotations +@using Aquiis.Core.Entities +@using Aquiis.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/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor new file mode 100644 index 0000000..e75aac6 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor @@ -0,0 +1,561 @@ +@page "/propertymanagement/properties" +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Core.Constants +@using Aquiis.UI.Shared.Features.PropertiesManagement.Properties +@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/{propertyId}"); + } + + private void EditProperty(Guid propertyId) + { + Navigation.NavigateTo($"/propertymanagement/properties/{propertyId}/edit"); + } + + 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/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor new file mode 100644 index 0000000..4fed820 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor @@ -0,0 +1,626 @@ +@page "/propertymanagement/properties/view/{PropertyId:guid}" +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.Core.Constants +@using Aquiis.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/{PropertyId}/edit"); + } + + private void CreateLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/create/?propertyid={PropertyId}"); + } + + private void ViewLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{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/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/Prospects/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Prospects/Index.razor new file mode 100644 index 0000000..3709731 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Prospects/Index.razor @@ -0,0 +1,475 @@ +@page "/PropertyManagement/ProspectiveTenants" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Constants +@using Aquiis.Core.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Prospects/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Prospects/View.razor new file mode 100644 index 0000000..a85dae3 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Prospects/View.razor @@ -0,0 +1,813 @@ +@page "/PropertyManagement/ProspectiveTenants/{ProspectId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Constants +@using Aquiis.Core.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}/view"); + } + } + + 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/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor new file mode 100644 index 0000000..85f4160 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor @@ -0,0 +1,240 @@ +@page "/reports/income-statement" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor new file mode 100644 index 0000000..734a268 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor @@ -0,0 +1,259 @@ +@page "/reports/property-performance" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor new file mode 100644 index 0000000..803ae8a --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor @@ -0,0 +1,243 @@ +@page "/reports/rentroll" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor new file mode 100644 index 0000000..67c6d56 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor @@ -0,0 +1,278 @@ +@page "/reports" +@using Microsoft.AspNetCore.Authorization +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor new file mode 100644 index 0000000..27e7b80 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor @@ -0,0 +1,287 @@ +@page "/reports/tax-report" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Components/SampleFeatureCompnentOne.razor similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Components/SampleFeatureCompnentOne.razor rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Components/SampleFeatureCompnentOne.razor diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Create.razor similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Create.razor rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Create.razor diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Edit.razor similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Edit.razor rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Edit.razor diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Index.razor similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Index.razor rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Index.razor diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/View.razor similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/View.razor rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/View.razor diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs b/4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs b/4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs diff --git a/Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs b/4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs rename to 4-Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor new file mode 100644 index 0000000..41863b6 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor @@ -0,0 +1,337 @@ +@page "/property-management/security-deposits/calculate-dividends/{PoolId:guid}" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor new file mode 100644 index 0000000..9d48e66 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor @@ -0,0 +1,345 @@ +@page "/property-management/security-deposits/investment-pools" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor new file mode 100644 index 0000000..36b41fb --- /dev/null +++ b/4-Aquiis.SimpleStart/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.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor new file mode 100644 index 0000000..1269555 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor @@ -0,0 +1,401 @@ +@page "/property-management/security-deposits" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Constants +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor new file mode 100644 index 0000000..a499b79 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor @@ -0,0 +1,419 @@ +@page "/property-management/security-deposits/investment-pool/{PoolId:guid}" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Create.razor new file mode 100644 index 0000000..9eff5e7 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Create.razor @@ -0,0 +1,217 @@ +@page "/propertymanagement/tenants/create" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Edit.razor new file mode 100644 index 0000000..2768da8 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Edit.razor @@ -0,0 +1,339 @@ +@page "/propertymanagement/tenants/edit/{Id:guid}" +@using Aquiis.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/{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/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Index.razor new file mode 100644 index 0000000..b5e4a7b --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Index.razor @@ -0,0 +1,528 @@ +@page "/propertymanagement/tenants" +@using Aquiis.SimpleStart.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/{id}"); + } + + private void EditTenant(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/tenants/{id}/edit"); + } + + 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/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor new file mode 100644 index 0000000..2e5021f --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor @@ -0,0 +1,241 @@ +@page "/propertymanagement/tenants/view/{Id:guid}" +@using Aquiis.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/{Id}/edit"); + } + + 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/4-Aquiis.SimpleStart/Features/PropertyManagement/Tours/Calendar.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tours/Calendar.razor new file mode 100644 index 0000000..624ad29 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tours/Calendar.razor @@ -0,0 +1,667 @@ +@page "/PropertyManagement/Tours/Calendar" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Tours/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tours/Create.razor new file mode 100644 index 0000000..84175eb --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tours/Create.razor @@ -0,0 +1,326 @@ +@page "/PropertyManagement/Tours/Schedule/{ProspectId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/PropertyManagement/Tours/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tours/Index.razor new file mode 100644 index 0000000..1205059 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tours/Index.razor @@ -0,0 +1,397 @@ +@page "/PropertyManagement/Tours" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/4-Aquiis.SimpleStart/Features/_Imports.razor b/4-Aquiis.SimpleStart/Features/_Imports.razor new file mode 100644 index 0000000..cf019eb --- /dev/null +++ b/4-Aquiis.SimpleStart/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.SimpleStart +@using Aquiis.Application.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.SimpleStart.Shared.Layout +@using Aquiis.SimpleStart.Shared.Components +@using Aquiis.SimpleStart.Shared.Components.Account +@using Aquiis.SimpleStart.Shared.Authorization +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.SimpleStart.Features.Administration diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Bold.ttf b/4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-Bold.ttf similarity index 100% rename from Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Bold.ttf rename to 4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-Bold.ttf diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf b/4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf similarity index 100% rename from Aquiis.Professional/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf rename to 4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf b/4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf similarity index 100% rename from Aquiis.Professional/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf rename to 4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Oblique.ttf b/4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-Oblique.ttf similarity index 100% rename from Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Oblique.ttf rename to 4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-Oblique.ttf diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSans.ttf b/4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans.ttf similarity index 100% rename from Aquiis.Professional/Fonts/DejaVu/DejaVuSans.ttf rename to 4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans.ttf diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf b/4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf similarity index 100% rename from Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf rename to 4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf b/4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf similarity index 100% rename from Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf rename to 4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf b/4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf similarity index 100% rename from Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf rename to 4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf diff --git a/Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed.ttf b/4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed.ttf similarity index 100% rename from Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed.ttf rename to 4-Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Black.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Black.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-Black.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Black.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-BlackItalic.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-BlackItalic.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-BlackItalic.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-BlackItalic.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Bold.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Bold.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-Bold.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Bold.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-BoldItalic.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-BoldItalic.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-BoldItalic.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-BoldItalic.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBold.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraBold.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBold.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraBold.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLight.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraLight.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLight.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraLight.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLightItalic.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraLightItalic.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLightItalic.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraLightItalic.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Italic.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Italic.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-Italic.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Italic.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Light.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Light.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-Light.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Light.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-LightItalic.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-LightItalic.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-LightItalic.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-LightItalic.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Medium.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Medium.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-Medium.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Medium.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-MediumItalic.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-MediumItalic.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-MediumItalic.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-MediumItalic.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Regular.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Regular.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-Regular.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Regular.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-SemiBold.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-SemiBold.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-SemiBold.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-SemiBold.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-SemiBoldItalic.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-SemiBoldItalic.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-SemiBoldItalic.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-SemiBoldItalic.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-Thin.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Thin.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-Thin.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-Thin.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/Lato-ThinItalic.ttf b/4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ThinItalic.ttf similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/Lato-ThinItalic.ttf rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/Lato-ThinItalic.ttf diff --git a/Aquiis.Professional/Fonts/LatoFont/OFL.txt b/4-Aquiis.SimpleStart/Fonts/LatoFont/OFL.txt similarity index 100% rename from Aquiis.Professional/Fonts/LatoFont/OFL.txt rename to 4-Aquiis.SimpleStart/Fonts/LatoFont/OFL.txt diff --git a/4-Aquiis.SimpleStart/Program.cs b/4-Aquiis.SimpleStart/Program.cs new file mode 100644 index 0000000..a6869e6 --- /dev/null +++ b/4-Aquiis.SimpleStart/Program.cs @@ -0,0 +1,584 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Aquiis.Core.Constants; +using Aquiis.Core.Interfaces; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Shared.Services; +using Aquiis.SimpleStart.Shared.Authorization; +using Aquiis.SimpleStart.Extensions; +using Aquiis.Application.Services; +using Aquiis.Application.Services.Workflows; +using Aquiis.SimpleStart.Data; +using Aquiis.SimpleStart.Entities; +using ElectronNET.API; +using Microsoft.Extensions.Options; +using Aquiis.Application.Services.PdfGenerators; +using Aquiis.SimpleStart.Shared.Components.Account; + + +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(); + +// Add antiforgery services with options for Blazor +builder.Services.AddAntiforgery(options => +{ + options.HeaderName = "X-CSRF-TOKEN"; + // Allow cookies over HTTP for Electron/Development + if (HybridSupport.IsElectronActive || builder.Environment.IsDevelopment()) + { + options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + } +}); + + + //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(); + +// Add platform-specific infrastructure services (Database, Identity, Path services) +if (HybridSupport.IsElectronActive) +{ + builder.Services.AddElectronServices(builder.Configuration); +} +else +{ + builder.Services.AddWebServices(builder.Configuration); +} + +// 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 events (cookie lifetime already configured in extension methods) +builder.Services.ConfigureApplicationCookie(options => +{ + 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(); // Concrete class for components that need it +builder.Services.AddScoped(sp => sp.GetRequiredService()); // Interface alias +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(); +// SendGridEmailService and TwilioSMSService registered in extension methods + +// Workflow services +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()) +{ + // Get services + var dbService = scope.ServiceProvider.GetRequiredService(); + var identityContext = 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); + + // Check pending migrations for both contexts + var businessPendingCount = await dbService.GetPendingMigrationsCountAsync(); + var identityPendingCount = await dbService.GetIdentityPendingMigrationsCountAsync(); + + if (businessPendingCount > 0 || identityPendingCount > 0) + { + var totalCount = businessPendingCount + identityPendingCount; + app.Logger.LogInformation("Found {Count} pending migrations ({BusinessCount} business, {IdentityCount} identity)", + totalCount, businessPendingCount, identityPendingCount); + + // 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 using DatabaseService + await dbService.InitializeAsync(); + + 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); + + // Apply migrations using DatabaseService + await dbService.InitializeAsync(); + + 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"); + + // 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 for both contexts + var businessPendingCount = await dbService.GetPendingMigrationsCountAsync(); + var identityPendingCount = await dbService.GetIdentityPendingMigrationsCountAsync(); + + var isNewDatabase = businessPendingCount == 0 && identityPendingCount == 0; + + if (businessPendingCount > 0 || identityPendingCount > 0) + { + var totalCount = businessPendingCount + identityPendingCount; + app.Logger.LogInformation("Found {Count} pending migrations ({BusinessCount} business, {IdentityCount} identity)", + totalCount, businessPendingCount, identityPendingCount); + + // Create backup before migration + var backupPath = await backupService.CreatePreMigrationBackupAsync(); + if (backupPath != null) + { + app.Logger.LogInformation("Database backed up to {BackupPath}", backupPath); + } + } + + // Apply migrations to both contexts + if (identityPendingCount > 0 || businessPendingCount > 0) + { + app.Logger.LogInformation("Applying migrations ({Identity} identity, {Business} business)", + identityPendingCount, businessPendingCount); + await dbService.InitializeAsync(); + } + + 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.UseAuthentication(); +app.UseAuthorization(); +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.SimpleStart/Properties/launchSettings.json b/4-Aquiis.SimpleStart/Properties/launchSettings.json similarity index 100% rename from Aquiis.SimpleStart/Properties/launchSettings.json rename to 4-Aquiis.SimpleStart/Properties/launchSettings.json diff --git a/Aquiis.SimpleStart/README.md b/4-Aquiis.SimpleStart/README.md similarity index 100% rename from Aquiis.SimpleStart/README.md rename to 4-Aquiis.SimpleStart/README.md diff --git a/4-Aquiis.SimpleStart/Services/ElectronPathService.cs b/4-Aquiis.SimpleStart/Services/ElectronPathService.cs new file mode 100644 index 0000000..d462718 --- /dev/null +++ b/4-Aquiis.SimpleStart/Services/ElectronPathService.cs @@ -0,0 +1,76 @@ +using ElectronNET.API; +using ElectronNET.API.Entities; +using Microsoft.Extensions.Configuration; +using Aquiis.Core.Interfaces; + +namespace Aquiis.SimpleStart.Services; + +/// +/// Electron-specific implementation of path service. +/// Manages file paths and connection strings for Electron desktop applications. +/// +public class ElectronPathService : IPathService +{ + private readonly IConfiguration _configuration; + + public ElectronPathService(IConfiguration configuration) + { + _configuration = configuration; + } + + /// + public bool IsActive => HybridSupport.IsElectronActive; + + /// + public async Task GetConnectionStringAsync(object configuration) + { + var dbPath = await GetDatabasePathAsync(); + return $"DataSource={dbPath};Cache=Shared"; + } + + /// + public async Task GetDatabasePathAsync() + { + var dbFileName = _configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; + + if (HybridSupport.IsElectronActive) + { + var userDataPath = await GetUserDataPathAsync(); + 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 + { + // Fallback to local path if not in Electron mode + var dataDir = Path.Combine(Directory.GetCurrentDirectory(), "Data"); + if (!Directory.Exists(dataDir)) + { + Directory.CreateDirectory(dataDir); + } + return Path.Combine(dataDir, dbFileName); + } + } + + /// + public async Task GetUserDataPathAsync() + { + if (HybridSupport.IsElectronActive) + { + return await Electron.App.GetPathAsync(PathName.UserData); + } + else + { + // Fallback for non-Electron mode + return Path.Combine(Directory.GetCurrentDirectory(), "Data"); + } + } + +} diff --git a/4-Aquiis.SimpleStart/Services/WebPathService.cs b/4-Aquiis.SimpleStart/Services/WebPathService.cs new file mode 100644 index 0000000..ae32fed --- /dev/null +++ b/4-Aquiis.SimpleStart/Services/WebPathService.cs @@ -0,0 +1,50 @@ +using Aquiis.Core.Interfaces; +using Microsoft.Extensions.Configuration; + +namespace Aquiis.SimpleStart.Services; + +/// +/// Path service for web/server applications. +/// Uses standard server file system paths. +/// +public class WebPathService : IPathService +{ + private readonly IConfiguration _configuration; + + public WebPathService(IConfiguration configuration) + { + _configuration = configuration; + } + + public bool IsActive => true; + + public async Task GetConnectionStringAsync(object configuration) + { + if (configuration is IConfiguration config) + { + return await Task.Run(() => config.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.")); + } + throw new ArgumentException("Configuration must be IConfiguration", nameof(configuration)); + } + + public async Task GetDatabasePathAsync() + { + var connectionString = await GetConnectionStringAsync(_configuration); + // Extract Data Source from connection string + var dataSource = connectionString.Split(';') + .FirstOrDefault(s => s.Trim().StartsWith("Data Source=", StringComparison.OrdinalIgnoreCase)); + + if (dataSource != null) + { + return dataSource.Split('=')[1].Trim(); + } + + return "aquiis.db"; // Default + } + + public async Task GetUserDataPathAsync() + { + return await Task.Run(() => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); + } +} diff --git a/4-Aquiis.SimpleStart/Shared/App.razor b/4-Aquiis.SimpleStart/Shared/App.razor new file mode 100644 index 0000000..dee388b --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/App.razor @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/4-Aquiis.SimpleStart/Shared/Authorization/OrganizationAuthorizeAttribute.cs b/4-Aquiis.SimpleStart/Shared/Authorization/OrganizationAuthorizeAttribute.cs new file mode 100644 index 0000000..718313d --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Authorization/OrganizationAuthorizeAttribute.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Authorization/OrganizationPolicyProvider.cs b/4-Aquiis.SimpleStart/Shared/Authorization/OrganizationPolicyProvider.cs similarity index 81% rename from Aquiis.SimpleStart/Shared/Authorization/OrganizationPolicyProvider.cs rename to 4-Aquiis.SimpleStart/Shared/Authorization/OrganizationPolicyProvider.cs index d6a988c..bbde0c1 100644 --- a/Aquiis.SimpleStart/Shared/Authorization/OrganizationPolicyProvider.cs +++ b/4-Aquiis.SimpleStart/Shared/Authorization/OrganizationPolicyProvider.cs @@ -29,6 +29,14 @@ public Task GetDefaultPolicyAsync() 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(','); diff --git a/4-Aquiis.SimpleStart/Shared/Authorization/OrganizationRoleAuthorizationHandler copy.cs b/4-Aquiis.SimpleStart/Shared/Authorization/OrganizationRoleAuthorizationHandler copy.cs new file mode 100644 index 0000000..c6a4ec4 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Authorization/OrganizationRoleAuthorizationHandler copy.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Aquiis.SimpleStart.Shared.Components.Account; +using Aquiis.Infrastructure.Data; +using Aquiis.Core.Constants; +using System.Security.Claims; +using Aquiis.SimpleStart.Entities; + +namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Authorization/OrganizationRoleRequirement.cs b/4-Aquiis.SimpleStart/Shared/Authorization/OrganizationRoleRequirement.cs similarity index 100% rename from Aquiis.SimpleStart/Shared/Authorization/OrganizationRoleRequirement.cs rename to 4-Aquiis.SimpleStart/Shared/Authorization/OrganizationRoleRequirement.cs diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/AccountConstants.cs b/4-Aquiis.SimpleStart/Shared/Components/Account/AccountConstants.cs new file mode 100644 index 0000000..cb29ff1 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/AccountConstants.cs @@ -0,0 +1,14 @@ +using Aquiis.SimpleStart.Entities; +namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs similarity index 97% rename from Aquiis.SimpleStart/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs rename to 4-Aquiis.SimpleStart/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs index f43ebdc..adb875b 100644 --- a/Aquiis.SimpleStart/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -1,3 +1,4 @@ +using Aquiis.SimpleStart.Entities; using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Authentication; diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityNoOpEmailSender.cs b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityNoOpEmailSender.cs new file mode 100644 index 0000000..6c0faa4 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityNoOpEmailSender.cs @@ -0,0 +1,21 @@ +using Aquiis.SimpleStart.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Aquiis.Infrastructure.Data; + +namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Account/IdentityRedirectManager.cs b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityRedirectManager.cs similarity index 95% rename from Aquiis.SimpleStart/Shared/Components/Account/IdentityRedirectManager.cs rename to 4-Aquiis.SimpleStart/Shared/Components/Account/IdentityRedirectManager.cs index c55deff..7478bed 100644 --- a/Aquiis.SimpleStart/Shared/Components/Account/IdentityRedirectManager.cs +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityRedirectManager.cs @@ -1,3 +1,4 @@ +using Aquiis.SimpleStart.Entities; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components; diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs new file mode 100644 index 0000000..b48b51b --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs @@ -0,0 +1,48 @@ +using Aquiis.SimpleStart.Entities; +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Aquiis.Infrastructure.Data; + +namespace Aquiis.SimpleStart.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/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityUserAccessor.cs b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityUserAccessor.cs new file mode 100644 index 0000000..1b58775 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/IdentityUserAccessor.cs @@ -0,0 +1,20 @@ +using Aquiis.SimpleStart.Entities; +using Microsoft.AspNetCore.Identity; +using Aquiis.Infrastructure.Data; + +namespace Aquiis.SimpleStart.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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/AccessDenied.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/AccessDenied.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/AccessDenied.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmail.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ConfirmEmail.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmail.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ConfirmEmail.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmailChange.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ConfirmEmailChange.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmailChange.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ConfirmEmailChange.razor diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ExternalLogin.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ExternalLogin.razor new file mode 100644 index 0000000..1fd966c --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ExternalLogin.razor @@ -0,0 +1,205 @@ +@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; } = default!; + + [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() + { + Input = Input ?? new InputModel(); + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ForgotPassword.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ForgotPassword.razor new file mode 100644 index 0000000..caf13c7 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ForgotPassword.razor @@ -0,0 +1,73 @@ +@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; } = default!; + + protected override async Task OnInitializedAsync() + { + Input = Input ?? new InputModel(); + await Task.CompletedTask; + } + + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/InvalidPasswordReset.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/InvalidPasswordReset.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/InvalidPasswordReset.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/InvalidPasswordReset.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/InvalidUser.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/InvalidUser.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/InvalidUser.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/InvalidUser.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Pages/Lockout.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Lockout.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/Lockout.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Lockout.razor diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Login.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Login.razor new file mode 100644 index 0000000..fe3f02b --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Login.razor @@ -0,0 +1,129 @@ +@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; } = default!; + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + Input = Input ?? new InputModel(); + + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWith2fa.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWith2fa.razor new file mode 100644 index 0000000..2ad3ae2 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWith2fa.razor @@ -0,0 +1,102 @@ +@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; } = default!; + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + [SupplyParameterFromQuery] + private bool RememberMe { get; set; } + + protected override async Task OnInitializedAsync() + { + Input = Input ?? new InputModel(); + + // 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor new file mode 100644 index 0000000..b6ae816 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor @@ -0,0 +1,87 @@ +@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; } = default!; + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + Input = Input ?? new InputModel(); + await base.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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ChangePassword.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ChangePassword.razor new file mode 100644 index 0000000..082cd71 --- /dev/null +++ b/4-Aquiis.SimpleStart/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; } = default!; + + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor new file mode 100644 index 0000000..0970813 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor @@ -0,0 +1,84 @@ +@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; } = default!; + + protected override async Task OnInitializedAsync() + { + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Disable2fa.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Disable2fa.razor new file mode 100644 index 0000000..ad461c8 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Disable2fa.razor @@ -0,0 +1,61 @@ +@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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor new file mode 100644 index 0000000..434373d --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor @@ -0,0 +1,121 @@ +@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; } = default!; + + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor new file mode 100644 index 0000000..0ffa261 --- /dev/null +++ b/4-Aquiis.SimpleStart/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; } = default!; + + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ExternalLogins.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ExternalLogins.razor new file mode 100644 index 0000000..5f772dc --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ExternalLogins.razor @@ -0,0 +1,137 @@ +@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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor new file mode 100644 index 0000000..093516e --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor @@ -0,0 +1,66 @@ +@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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Index.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Index.razor new file mode 100644 index 0000000..5fc0b84 --- /dev/null +++ b/4-Aquiis.SimpleStart/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; } = default!; + + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/PersonalData.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/PersonalData.razor new file mode 100644 index 0000000..5ca6b75 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/PersonalData.razor @@ -0,0 +1,33 @@ +@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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor new file mode 100644 index 0000000..73fad62 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor @@ -0,0 +1,50 @@ +@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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/SetPassword.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/SetPassword.razor new file mode 100644 index 0000000..a3ae060 --- /dev/null +++ b/4-Aquiis.SimpleStart/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; } = default!; + + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor new file mode 100644 index 0000000..0084aea --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor @@ -0,0 +1,99 @@ +@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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/_Imports.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/Manage/_Imports.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/_Imports.razor diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Register.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Register.razor new file mode 100644 index 0000000..4af2615 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Register.razor @@ -0,0 +1,273 @@ +@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.Infrastructure.Data +@using Aquiis.Application.Services +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Microsoft.EntityFrameworkCore + +@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; } = default!; + + [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 users = await UserManager.Users + .Where(u => u.Id != ApplicationConstants.SystemUser.Id) + .ToListAsync(); + + var userCount = users.Count; + + _isFirstUser = userCount == 0; + _allowRegistration = _isFirstUser; // Only allow registration if this is the first user + + Input = Input ?? new InputModel(); + await base.OnInitializedAsync(); + StateHasChanged(); + } + + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/RegisterConfirmation.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/RegisterConfirmation.razor diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ResendEmailConfirmation.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ResendEmailConfirmation.razor new file mode 100644 index 0000000..92d3d4e --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ResendEmailConfirmation.razor @@ -0,0 +1,68 @@ +@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; } = default!; + + private async Task OnValidSubmitAsync() + { + Input = Input ?? new InputModel(); + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ResetPassword.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ResetPassword.razor new file mode 100644 index 0000000..5734579 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ResetPassword.razor @@ -0,0 +1,104 @@ +@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; } = default!; + + [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() + { + Input = Input ?? new InputModel(); + + 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/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/_Imports.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/_Imports.razor new file mode 100644 index 0000000..3750079 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/_Imports.razor @@ -0,0 +1 @@ +@using Aquiis.SimpleStart.Shared.Components.Account.Shared diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/ExternalLoginPicker.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Shared/ExternalLoginPicker.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Shared/ExternalLoginPicker.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Shared/ExternalLoginPicker.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Shared/ManageLayout.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Shared/ManageLayout.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Shared/ManageLayout.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Shared/ManageLayout.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/ManageNavMenu.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Shared/ManageNavMenu.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Shared/ManageNavMenu.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Shared/ManageNavMenu.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/RedirectToLogin.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Shared/RedirectToLogin.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Shared/RedirectToLogin.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Shared/RedirectToLogin.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/ShowRecoveryCodes.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Shared/ShowRecoveryCodes.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Shared/ShowRecoveryCodes.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Shared/ShowRecoveryCodes.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/StatusMessage.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Shared/StatusMessage.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Shared/StatusMessage.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Account/Shared/StatusMessage.razor diff --git a/4-Aquiis.SimpleStart/Shared/Components/AuthorizedHeaderSection.razor b/4-Aquiis.SimpleStart/Shared/Components/AuthorizedHeaderSection.razor new file mode 100644 index 0000000..cd69e63 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/AuthorizedHeaderSection.razor @@ -0,0 +1,7 @@ +@using Aquiis.SimpleStart.Shared.Components +@rendermode InteractiveServer + +
+ + +
diff --git a/4-Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor b/4-Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor new file mode 100644 index 0000000..ce2c286 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor @@ -0,0 +1,180 @@ +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.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/4-Aquiis.SimpleStart/Shared/Components/NotesTimeline.razor b/4-Aquiis.SimpleStart/Shared/Components/NotesTimeline.razor new file mode 100644 index 0000000..91d49ce --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/NotesTimeline.razor @@ -0,0 +1,246 @@ +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Entities +@using Aquiis.SimpleStart.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/4-Aquiis.SimpleStart/Shared/Components/NotificationBellWrapper.razor b/4-Aquiis.SimpleStart/Shared/Components/NotificationBellWrapper.razor new file mode 100644 index 0000000..9a7377b --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/NotificationBellWrapper.razor @@ -0,0 +1,4 @@ +@using Aquiis.UI.Shared.Features.Notifications +@using Aquiis.SimpleStart.Shared.Services + + diff --git a/4-Aquiis.SimpleStart/Shared/Components/OrganizationSwitcher.razor b/4-Aquiis.SimpleStart/Shared/Components/OrganizationSwitcher.razor new file mode 100644 index 0000000..8405134 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/OrganizationSwitcher.razor @@ -0,0 +1,189 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Core.Constants +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@implements IDisposable + +@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/4-Aquiis.SimpleStart/Shared/Components/Pages/About.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Pages/About.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Pages/About.razor diff --git a/Aquiis.Professional/Shared/Components/Pages/Error.razor b/4-Aquiis.SimpleStart/Shared/Components/Pages/Error.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Pages/Error.razor rename to 4-Aquiis.SimpleStart/Shared/Components/Pages/Error.razor diff --git a/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor b/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor new file mode 100644 index 0000000..cff06f9 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor @@ -0,0 +1,479 @@ +@page "/" +@using Aquiis.Application.Services +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.EntityFrameworkCore +@using Aquiis.Infrastructure.Data +@using Aquiis.Core.Entities +@using Aquiis.SimpleStart.Features.PropertyManagement +@using Aquiis.SimpleStart.Shared.Components.Account +@using Aquiis.SimpleStart.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("/calendar"); + } +} diff --git a/4-Aquiis.SimpleStart/Shared/Components/Pages/TestSharedComponents.razor b/4-Aquiis.SimpleStart/Shared/Components/Pages/TestSharedComponents.razor new file mode 100644 index 0000000..cc4009d --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Pages/TestSharedComponents.razor @@ -0,0 +1,143 @@ +@page "/test/shared-components" +@using Aquiis.UI.Shared.Components.Common + +Shared Components Test + +

Shared UI Components Test Page

+ +
+
+

Modal Component

+ + + + +

This is the modal body content.

+

The modal supports different sizes and positions.

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

Card Component

+ +

This is a card with a title.

+

Cards support headers, bodies, and footers.

+
+
+ +
+ + +
+
Custom Header
+ Active +
+
+ +

This card has a custom header with a badge.

+
+ + + +
+
+ +
+ + +
Styled Card
+
+ +

This card has custom styling on the header.

+
+
+
+
+ +
+
+

DataTable Component

+ + + ID + Name + Status + Actions + + + @context.Id + @context.Name + @(context.IsActive ? "Active" : "Inactive") + + + + + + +
+
+ +
+
+

FormField Component

+ + + + + + + + + + + + + + + +
+
+ +@code { + private bool showModal = false; + + private class TestItem + { + public int Id { get; set; } + public string Name { get; set; } = ""; + public bool IsActive { get; set; } + } + + private List testItems = new() + { + new TestItem { Id = 1, Name = "Item One", IsActive = true }, + new TestItem { Id = 2, Name = "Item Two", IsActive = true }, + new TestItem { Id = 3, Name = "Item Three", IsActive = false }, + new TestItem { Id = 4, Name = "Item Four", IsActive = true }, + }; + + private class FormModel + { + public string Username { get; set; } = ""; + public string Email { get; set; } = ""; + public string Description { get; set; } = ""; + } + + private FormModel formModel = new(); + + private void HandleSubmit() + { + // Form submitted + } +} diff --git a/4-Aquiis.SimpleStart/Shared/Components/SchemaValidationWarning.razor b/4-Aquiis.SimpleStart/Shared/Components/SchemaValidationWarning.razor new file mode 100644 index 0000000..9e09d9b --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/SchemaValidationWarning.razor @@ -0,0 +1,66 @@ +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Shared/Components/SessionTimeoutModal.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/SessionTimeoutModal.razor rename to 4-Aquiis.SimpleStart/Shared/Components/SessionTimeoutModal.razor diff --git a/4-Aquiis.SimpleStart/Shared/Components/Shared/OrganizationAuthorizeView.razor b/4-Aquiis.SimpleStart/Shared/Components/Shared/OrganizationAuthorizeView.razor new file mode 100644 index 0000000..f2576b1 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Shared/OrganizationAuthorizeView.razor @@ -0,0 +1,62 @@ +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Shared/Components/ToastContainer.razor b/4-Aquiis.SimpleStart/Shared/Components/ToastContainer.razor new file mode 100644 index 0000000..1bc5ad2 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/ToastContainer.razor @@ -0,0 +1,164 @@ +@using Aquiis.Application.Services +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.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/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor b/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor new file mode 100644 index 0000000..7b3ff61 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor @@ -0,0 +1,45 @@ +@inherits LayoutComponentBase +@using Aquiis.SimpleStart.Shared.Components +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.UI.Shared.Components.Layout +@inject ThemeService ThemeService +@implements IDisposable + + + + + + + + About + + + + + + + + @Body + + + + + + + + + + + + +@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/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor.css similarity index 100% rename from Aquiis.Professional/Shared/Layout/MainLayout.razor.css rename to 4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor.css diff --git a/Aquiis.SimpleStart/Shared/Layout/NavMenu.razor b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Layout/NavMenu.razor rename to 4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor diff --git a/Aquiis.Professional/Shared/Layout/NavMenu.razor.css b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor.css similarity index 100% rename from Aquiis.Professional/Shared/Layout/NavMenu.razor.css rename to 4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor.css diff --git a/Aquiis.SimpleStart/Shared/Routes.razor b/4-Aquiis.SimpleStart/Shared/Routes.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Routes.razor rename to 4-Aquiis.SimpleStart/Shared/Routes.razor diff --git a/4-Aquiis.SimpleStart/Shared/Services/DatabaseBackupService.cs b/4-Aquiis.SimpleStart/Shared/Services/DatabaseBackupService.cs new file mode 100644 index 0000000..610d889 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Services/DatabaseBackupService.cs @@ -0,0 +1,415 @@ +using Aquiis.Infrastructure.Data; +using Aquiis.Core.Interfaces; +using Microsoft.EntityFrameworkCore; +using ElectronNET.API; + +namespace Aquiis.SimpleStart.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 IPathService _pathService; + + public DatabaseBackupService( + ILogger logger, + ApplicationDbContext dbContext, + IConfiguration configuration, + IPathService pathService) + { + _logger = logger; + _dbContext = dbContext; + _configuration = configuration; + _pathService = pathService; + } + + /// + /// 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 _pathService.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/4-Aquiis.SimpleStart/Shared/Services/EntityRouteHelper.cs b/4-Aquiis.SimpleStart/Shared/Services/EntityRouteHelper.cs new file mode 100644 index 0000000..98b0b53 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Services/EntityRouteHelper.cs @@ -0,0 +1,131 @@ +using Aquiis.Core.Entities; + +namespace Aquiis.SimpleStart.Shared.Services; + +/// +/// Provides centralized mapping between entity types and their navigation routes. +/// Follows RESTful routing conventions: /resource/{id} for details, /resource/{id}/action for specific actions. +/// This ensures consistent URL generation across the application when navigating to entity details. +/// +public static class EntityRouteHelper +{ + /// + /// RESTful route mapping: entity type to base resource path. + /// Detail view: {basePath}/{id} + /// Actions: {basePath}/{id}/{action} + /// + private static readonly Dictionary RouteMap = new() + { + { "Lease", "/propertymanagement/leases" }, + { "Payment", "/propertymanagement/payments" }, + { "Invoice", "/propertymanagement/invoices" }, + { "Maintenance", "/propertymanagement/maintenance" }, + { "Application", "/propertymanagement/applications" }, + { "Property", "/propertymanagement/properties" }, + { "Tenant", "/propertymanagement/tenants" }, + { "Prospect", "/propertymanagement/prospects" } + }; + + /// + /// Gets the detail view route for a given entity (RESTful: /resource/{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 "/"; + } + + /// + /// Gets the route for a specific action on an entity (RESTful: /resource/{id}/{action}). + /// + /// The type of entity + /// The unique identifier of the entity + /// The action to perform (e.g., "edit", "accept", "approve", "submit-application") + /// The full route path including the entity ID and action + public static string GetEntityActionRoute(string? entityType, Guid entityId, string action) + { + if (string.IsNullOrWhiteSpace(entityType) || string.IsNullOrWhiteSpace(action)) + { + return "/"; + } + + if (RouteMap.TryGetValue(entityType, out var route)) + { + return $"{route}/{entityId}/{action}"; + } + + return "/"; + } + + /// + /// Gets the list view route for a given entity type (RESTful: /resource). + /// + /// The type of entity + /// The list view route path + public static string GetListRoute(string? entityType) + { + if (string.IsNullOrWhiteSpace(entityType)) + { + return "/"; + } + + if (RouteMap.TryGetValue(entityType, out var route)) + { + return route; + } + + return "/"; + } + + /// + /// Gets the create route for a given entity type (RESTful: /resource/create). + /// + /// The type of entity + /// The create route path + public static string GetCreateRoute(string? entityType) + { + if (string.IsNullOrWhiteSpace(entityType)) + { + return "/"; + } + + if (RouteMap.TryGetValue(entityType, out var route)) + { + return $"{route}/create"; + } + + 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/Services/SessionTimeoutService.cs b/4-Aquiis.SimpleStart/Shared/Services/SessionTimeoutService.cs similarity index 100% rename from Aquiis.SimpleStart/Shared/Services/SessionTimeoutService.cs rename to 4-Aquiis.SimpleStart/Shared/Services/SessionTimeoutService.cs diff --git a/Aquiis.SimpleStart/Shared/Services/ThemeService.cs b/4-Aquiis.SimpleStart/Shared/Services/ThemeService.cs similarity index 100% rename from Aquiis.SimpleStart/Shared/Services/ThemeService.cs rename to 4-Aquiis.SimpleStart/Shared/Services/ThemeService.cs diff --git a/Aquiis.SimpleStart/Shared/Services/ToastService.cs b/4-Aquiis.SimpleStart/Shared/Services/ToastService.cs similarity index 100% rename from Aquiis.SimpleStart/Shared/Services/ToastService.cs rename to 4-Aquiis.SimpleStart/Shared/Services/ToastService.cs diff --git a/4-Aquiis.SimpleStart/Shared/Services/UserContextService.cs b/4-Aquiis.SimpleStart/Shared/Services/UserContextService.cs new file mode 100644 index 0000000..5224618 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Services/UserContextService.cs @@ -0,0 +1,311 @@ +using Aquiis.SimpleStart.Entities; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Aquiis.Core.Entities; +using System.Security.Claims; +using Aquiis.Core.Constants; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Application.Services; + +namespace Aquiis.SimpleStart.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 : IUserContextService + { + 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). + /// Returns null if user is not authenticated or has no active organization. + /// Callers should handle null appropriately. + /// + public async Task GetActiveOrganizationIdAsync() + { + // Check if user is authenticated first + if (!await IsAuthenticatedAsync()) + { + return null; // Not authenticated - no organization + } + + await EnsureInitializedAsync(); + + // Return null if no active organization (e.g., fresh database, new user) + if (!_activeOrganizationId.HasValue || _activeOrganizationId == Guid.Empty) + { + return null; + } + + 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/4-Aquiis.SimpleStart/Shared/_Imports.razor b/4-Aquiis.SimpleStart/Shared/_Imports.razor new file mode 100644 index 0000000..3107264 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/_Imports.razor @@ -0,0 +1,17 @@ +@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.SimpleStart +@using Aquiis.SimpleStart.Shared.Services +@using Aquiis.SimpleStart.Shared.Layout +@using Aquiis.SimpleStart.Shared.Components +@using Aquiis.SimpleStart.Shared.Components.Account +@using Aquiis.SimpleStart.Shared.Components.Shared +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants diff --git a/4-Aquiis.SimpleStart/_Imports.razor b/4-Aquiis.SimpleStart/_Imports.razor new file mode 100644 index 0000000..2b1af3a --- /dev/null +++ b/4-Aquiis.SimpleStart/_Imports.razor @@ -0,0 +1,18 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Authorization +@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.SimpleStart +@using Aquiis.SimpleStart.Shared.Components +@using Aquiis.SimpleStart.Entities +@using Aquiis.Application.Services +@using Aquiis.Core.Entities +@using Aquiis.Core.Interfaces +@using Aquiis.UI.Shared.Features.Notifications +@using Aquiis.UI.Shared.Components.Common diff --git a/Aquiis.Professional/appsettings.Development.json b/4-Aquiis.SimpleStart/appsettings.Development.json similarity index 100% rename from Aquiis.Professional/appsettings.Development.json rename to 4-Aquiis.SimpleStart/appsettings.Development.json diff --git a/4-Aquiis.SimpleStart/appsettings.json b/4-Aquiis.SimpleStart/appsettings.json new file mode 100644 index 0000000..0258005 --- /dev/null +++ b/4-Aquiis.SimpleStart/appsettings.json @@ -0,0 +1,49 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "DataSource=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.SimpleStart/electron.manifest.json b/4-Aquiis.SimpleStart/electron.manifest.json similarity index 100% rename from Aquiis.SimpleStart/electron.manifest.json rename to 4-Aquiis.SimpleStart/electron.manifest.json diff --git a/Aquiis.Professional/libman.json b/4-Aquiis.SimpleStart/libman.json similarity index 100% rename from Aquiis.Professional/libman.json rename to 4-Aquiis.SimpleStart/libman.json diff --git a/Aquiis.Professional/wwwroot/app.css b/4-Aquiis.SimpleStart/wwwroot/app.css similarity index 100% rename from Aquiis.Professional/wwwroot/app.css rename to 4-Aquiis.SimpleStart/wwwroot/app.css diff --git a/Aquiis.Professional/wwwroot/assets/database-fill-gear.svg b/4-Aquiis.SimpleStart/wwwroot/assets/database-fill-gear.svg similarity index 100% rename from Aquiis.Professional/wwwroot/assets/database-fill-gear.svg rename to 4-Aquiis.SimpleStart/wwwroot/assets/database-fill-gear.svg diff --git a/Aquiis.Professional/wwwroot/assets/database.svg b/4-Aquiis.SimpleStart/wwwroot/assets/database.svg similarity index 100% rename from Aquiis.Professional/wwwroot/assets/database.svg rename to 4-Aquiis.SimpleStart/wwwroot/assets/database.svg diff --git a/Aquiis.Professional/wwwroot/assets/splash.png b/4-Aquiis.SimpleStart/wwwroot/assets/splash.png similarity index 100% rename from Aquiis.Professional/wwwroot/assets/splash.png rename to 4-Aquiis.SimpleStart/wwwroot/assets/splash.png diff --git a/Aquiis.Professional/wwwroot/assets/splash.svg b/4-Aquiis.SimpleStart/wwwroot/assets/splash.svg similarity index 100% rename from Aquiis.Professional/wwwroot/assets/splash.svg rename to 4-Aquiis.SimpleStart/wwwroot/assets/splash.svg diff --git a/Aquiis.Professional/wwwroot/favicon.png b/4-Aquiis.SimpleStart/wwwroot/favicon.png similarity index 100% rename from Aquiis.Professional/wwwroot/favicon.png rename to 4-Aquiis.SimpleStart/wwwroot/favicon.png diff --git a/Aquiis.Professional/wwwroot/js/fileDownload.js b/4-Aquiis.SimpleStart/wwwroot/js/fileDownload.js similarity index 100% rename from Aquiis.Professional/wwwroot/js/fileDownload.js rename to 4-Aquiis.SimpleStart/wwwroot/js/fileDownload.js diff --git a/Aquiis.Professional/wwwroot/js/sessionTimeout.js b/4-Aquiis.SimpleStart/wwwroot/js/sessionTimeout.js similarity index 100% rename from Aquiis.Professional/wwwroot/js/sessionTimeout.js rename to 4-Aquiis.SimpleStart/wwwroot/js/sessionTimeout.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.min.js b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.min.js similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.min.js rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.min.js diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.min.js.map b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.min.js.map similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.min.js.map rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.min.js.map diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_accordion.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_accordion.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_accordion.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_accordion.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_alert.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_alert.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_alert.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_alert.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_badge.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_badge.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_badge.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_badge.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_breadcrumb.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_breadcrumb.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_breadcrumb.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_breadcrumb.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_button-group.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_button-group.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_button-group.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_button-group.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_buttons.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_buttons.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_buttons.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_buttons.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_card.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_card.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_card.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_card.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_carousel.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_carousel.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_carousel.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_carousel.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_close.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_close.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_close.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_close.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_containers.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_containers.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_containers.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_containers.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_dropdown.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_dropdown.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_dropdown.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_dropdown.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_forms.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_forms.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_forms.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_forms.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_functions.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_functions.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_functions.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_functions.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_grid.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_grid.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_grid.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_grid.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_helpers.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_helpers.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_helpers.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_helpers.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_images.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_images.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_images.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_images.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_list-group.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_list-group.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_list-group.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_list-group.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_maps.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_maps.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_maps.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_maps.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_mixins.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_mixins.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_mixins.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_mixins.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_modal.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_modal.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_modal.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_modal.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_nav.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_nav.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_nav.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_nav.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_navbar.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_navbar.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_navbar.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_navbar.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_offcanvas.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_offcanvas.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_offcanvas.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_offcanvas.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_pagination.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_pagination.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_pagination.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_pagination.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_placeholders.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_placeholders.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_placeholders.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_placeholders.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_popover.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_popover.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_popover.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_popover.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_progress.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_progress.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_progress.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_progress.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_reboot.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_reboot.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_reboot.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_reboot.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_root.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_root.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_root.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_root.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_spinners.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_spinners.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_spinners.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_spinners.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_tables.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_tables.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_tables.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_tables.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_toasts.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_toasts.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_toasts.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_toasts.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_tooltip.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_tooltip.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_tooltip.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_tooltip.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_transitions.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_transitions.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_transitions.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_transitions.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_type.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_type.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_type.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_type.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_utilities.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_utilities.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_utilities.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_utilities.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_variables-dark.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_variables-dark.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_variables-dark.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_variables-dark.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/_variables.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_variables.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/_variables.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_variables.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-grid.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-grid.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-grid.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-grid.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-reboot.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-reboot.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-reboot.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-reboot.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-utilities.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-utilities.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-utilities.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-utilities.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_floating-labels.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_floating-labels.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_floating-labels.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_floating-labels.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-check.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-check.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-check.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-check.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-control.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-control.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-control.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-control.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-range.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-range.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-range.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-range.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-select.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-select.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-select.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-select.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-text.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-text.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-text.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-text.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_input-group.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_input-group.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_input-group.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_input-group.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_labels.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_labels.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_labels.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_labels.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_validation.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_validation.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_validation.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_validation.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_clearfix.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_clearfix.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_clearfix.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_clearfix.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_color-bg.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_color-bg.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_color-bg.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_color-bg.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_colored-links.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_colored-links.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_colored-links.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_colored-links.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_focus-ring.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_focus-ring.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_focus-ring.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_focus-ring.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_icon-link.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_icon-link.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_icon-link.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_icon-link.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_position.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_position.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_position.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_position.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_ratio.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_ratio.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_ratio.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_ratio.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_stacks.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_stacks.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_stacks.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_stacks.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_stretched-link.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_stretched-link.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_stretched-link.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_stretched-link.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_text-truncation.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_text-truncation.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_text-truncation.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_text-truncation.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_visually-hidden.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_visually-hidden.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_visually-hidden.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_visually-hidden.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_vr.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_vr.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_vr.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_vr.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_alert.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_alert.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_alert.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_alert.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_backdrop.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_backdrop.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_backdrop.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_backdrop.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_banner.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_banner.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_banner.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_banner.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_border-radius.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_border-radius.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_border-radius.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_border-radius.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_box-shadow.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_box-shadow.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_box-shadow.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_box-shadow.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_breakpoints.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_breakpoints.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_breakpoints.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_breakpoints.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_buttons.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_buttons.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_buttons.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_buttons.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_caret.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_caret.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_caret.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_caret.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_clearfix.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_clearfix.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_clearfix.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_clearfix.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_color-mode.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_color-mode.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_color-mode.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_color-mode.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_color-scheme.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_color-scheme.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_color-scheme.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_color-scheme.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_container.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_container.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_container.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_container.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_deprecate.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_deprecate.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_deprecate.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_deprecate.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_forms.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_forms.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_forms.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_forms.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_gradients.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_gradients.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_gradients.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_gradients.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_grid.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_grid.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_grid.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_grid.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_image.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_image.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_image.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_image.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_list-group.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_list-group.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_list-group.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_list-group.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_lists.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_lists.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_lists.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_lists.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_pagination.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_pagination.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_pagination.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_pagination.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_reset-text.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_reset-text.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_reset-text.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_reset-text.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_resize.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_resize.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_resize.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_resize.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_table-variants.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_table-variants.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_table-variants.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_table-variants.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_text-truncate.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_text-truncate.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_text-truncate.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_text-truncate.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_transition.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_transition.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_transition.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_transition.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_utilities.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_utilities.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_utilities.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_utilities.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_visually-hidden.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_visually-hidden.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_visually-hidden.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_visually-hidden.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/utilities/_api.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/utilities/_api.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/utilities/_api.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/utilities/_api.scss diff --git a/Aquiis.Professional/wwwroot/lib/bootstrap/scss/vendor/_rfs.scss b/4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/vendor/_rfs.scss similarity index 100% rename from Aquiis.Professional/wwwroot/lib/bootstrap/scss/vendor/_rfs.scss rename to 4-Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/vendor/_rfs.scss diff --git a/Aquiis.Professional/Aquiis.Professional.csproj b/5-Aquiis.Professional/Aquiis.Professional.csproj similarity index 77% rename from Aquiis.Professional/Aquiis.Professional.csproj rename to 5-Aquiis.Professional/Aquiis.Professional.csproj index 82f8cf0..ce47093 100644 --- a/Aquiis.Professional/Aquiis.Professional.csproj +++ b/5-Aquiis.Professional/Aquiis.Professional.csproj @@ -1,12 +1,12 @@ - net9.0 + net10.0 enable enable aspnet-Aquiis.Professional-ae1e0851-3ba3-4d71-bc57-597eb787b7d8 true - Infrastructure/Data/Migrations + Data/Migrations 0.2.0 @@ -33,18 +33,24 @@ + + + + + + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + diff --git a/5-Aquiis.Professional/Data/Migrations/20260106195859_InitialCreate.Designer.cs b/5-Aquiis.Professional/Data/Migrations/20260106195859_InitialCreate.Designer.cs new file mode 100644 index 0000000..a29f510 --- /dev/null +++ b/5-Aquiis.Professional/Data/Migrations/20260106195859_InitialCreate.Designer.cs @@ -0,0 +1,294 @@ +// +using System; +using Aquiis.Professional.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Professional.Data.Migrations +{ + [DbContext(typeof(ProfessionalDbContext))] + [Migration("20260106195859_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + modelBuilder.Entity("Aquiis.Professional.Entities.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("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.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.Professional.Entities.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.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.Professional.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/5-Aquiis.Professional/Data/Migrations/20260106195859_InitialCreate.cs b/5-Aquiis.Professional/Data/Migrations/20260106195859_InitialCreate.cs new file mode 100644 index 0000000..fef9877 --- /dev/null +++ b/5-Aquiis.Professional/Data/Migrations/20260106195859_InitialCreate.cs @@ -0,0 +1,230 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Professional.Data.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: "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.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); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/5-Aquiis.Professional/Data/Migrations/ProfessionalDbContextModelSnapshot.cs b/5-Aquiis.Professional/Data/Migrations/ProfessionalDbContextModelSnapshot.cs new file mode 100644 index 0000000..20d8b6a --- /dev/null +++ b/5-Aquiis.Professional/Data/Migrations/ProfessionalDbContextModelSnapshot.cs @@ -0,0 +1,291 @@ +// +using System; +using Aquiis.Professional.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aquiis.Professional.Data.Migrations +{ + [DbContext(typeof(ProfessionalDbContext))] + partial class ProfessionalDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + modelBuilder.Entity("Aquiis.Professional.Entities.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("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.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aquiis.Professional.Entities.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.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aquiis.Professional.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/5-Aquiis.Professional/Data/ProfessionalDbContext.cs b/5-Aquiis.Professional/Data/ProfessionalDbContext.cs new file mode 100644 index 0000000..b5ee39f --- /dev/null +++ b/5-Aquiis.Professional/Data/ProfessionalDbContext.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Aquiis.Professional.Entities; + +namespace Aquiis.Professional.Data; + +/// +/// Professional database context for Identity management. +/// Handles all ASP.NET Core Identity tables and Professional-specific user data. +/// Shares the same database as ApplicationDbContext using the same connection string. +/// +public class ProfessionalDbContext : IdentityDbContext +{ + public ProfessionalDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // Identity table configuration is handled by base IdentityDbContext + // Add any Professional-specific user configurations here if needed + } +} diff --git a/5-Aquiis.Professional/Entities/ApplicationUser.cs b/5-Aquiis.Professional/Entities/ApplicationUser.cs new file mode 100644 index 0000000..d34faef --- /dev/null +++ b/5-Aquiis.Professional/Entities/ApplicationUser.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Identity; + +namespace Aquiis.Professional.Entities; + +/// +/// Professional user entity for authentication and authorization. +/// Extends IdentityUser with Professional-specific properties. +/// +public class ApplicationUser : IdentityUser +{ + /// + /// The currently active organization ID for this user session. + /// + public Guid ActiveOrganizationId { get; set; } = Guid.Empty; + + /// + /// The primary organization ID this user belongs to. + /// DEPRECATED in multi-org scenarios - use ActiveOrganizationId instead. + /// + public Guid OrganizationId { get; set; } = Guid.Empty; + + /// + /// User's first name. + /// + public string FirstName { get; set; } = string.Empty; + + /// + /// User's last name. + /// + public string LastName { get; set; } = string.Empty; + + /// + /// The timestamp of the user's most recent login. + /// + public DateTime? LastLoginDate { get; set; } + + /// + /// The timestamp of the user's previous login (before LastLoginDate). + /// + public DateTime? PreviousLoginDate { get; set; } + + /// + /// Total number of times the user has logged in. + /// + public int LoginCount { get; set; } = 0; + + /// + /// The IP address from the user's last login. + /// + public string? LastLoginIP { get; set; } +} diff --git a/5-Aquiis.Professional/Extensions/ElectronServiceExtensions.cs b/5-Aquiis.Professional/Extensions/ElectronServiceExtensions.cs new file mode 100644 index 0000000..be24494 --- /dev/null +++ b/5-Aquiis.Professional/Extensions/ElectronServiceExtensions.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore; +using Aquiis.Core.Interfaces; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Application; +using Aquiis.Application.Services; +using Aquiis.Professional.Data; +using Aquiis.Professional.Entities; +using Aquiis.Professional.Services; + +namespace Aquiis.Professional.Extensions; + +/// +/// Extension methods for configuring Electron-specific services for Professional. +/// +public static class ElectronServiceExtensions +{ + /// + /// Adds all Electron-specific infrastructure services including database, identity, and path services. + /// + /// The service collection. + /// The application configuration. + /// The service collection for chaining. + public static IServiceCollection AddElectronServices( + this IServiceCollection services, + IConfiguration configuration) + { + // Register path service + services.AddScoped(); + + // Get connection string using the path service + var connectionString = GetElectronConnectionString(configuration).GetAwaiter().GetResult(); + + // Register Application layer (includes Infrastructure internally) + services.AddApplication(connectionString); + + // Register Identity database context (Professional-specific) + services.AddDbContext(options => + options.UseSqlite(connectionString)); + + // Register DatabaseService now that both contexts are available + services.AddScoped(sp => + new DatabaseService( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>())); + + services.AddDatabaseDeveloperPageExceptionFilter(); + + // Configure Identity with Electron-specific settings + services.AddIdentity(options => { + // For desktop app, simplify registration (email confirmation can be enabled later via settings) + options.SignIn.RequireConfirmedAccount = false; // Electron mode + options.Password.RequireDigit = true; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = true; + options.Password.RequireLowercase = true; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // Configure cookie authentication for Electron + services.ConfigureApplicationCookie(options => + { + options.LoginPath = "/Account/Login"; + options.LogoutPath = "/Account/Logout"; + options.AccessDeniedPath = "/Account/AccessDenied"; + + // For Electron desktop app, use longer cookie lifetime + options.ExpireTimeSpan = TimeSpan.FromDays(30); + options.SlidingExpiration = true; + }); + + return services; + } + + /// + /// Gets the connection string for Electron mode using the path service. + /// + private static async Task GetElectronConnectionString(IConfiguration configuration) + { + var pathService = new ElectronPathService(configuration); + return await pathService.GetConnectionStringAsync(configuration); + } +} diff --git a/5-Aquiis.Professional/Extensions/WebServiceExtensions.cs b/5-Aquiis.Professional/Extensions/WebServiceExtensions.cs new file mode 100644 index 0000000..3c070b4 --- /dev/null +++ b/5-Aquiis.Professional/Extensions/WebServiceExtensions.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore; +using Aquiis.Core.Interfaces; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Application; +using Aquiis.Application.Services; +using Aquiis.Professional.Data; +using Aquiis.Professional.Entities; +using Aquiis.Professional.Services; + +namespace Aquiis.Professional.Extensions; + +/// +/// Extension methods for configuring Web-specific services for Professional. +/// +public static class WebServiceExtensions +{ + /// + /// Adds all Web-specific infrastructure services including database and identity. + /// + /// The service collection. + /// The application configuration. + /// The service collection for chaining. + public static IServiceCollection AddWebServices( + this IServiceCollection services, + IConfiguration configuration) + { + // Register path service + services.AddScoped(); + + // Get connection string from configuration + var connectionString = configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + + // Register Application layer (includes Infrastructure internally) + services.AddApplication(connectionString); + + // Register Identity database context (Professional-specific) + services.AddDbContext(options => + options.UseSqlite(connectionString)); + + // Register DatabaseService now that both contexts are available + services.AddScoped(sp => + new DatabaseService( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>())); + + services.AddDatabaseDeveloperPageExceptionFilter(); + + // Configure Identity with Web-specific settings + services.AddIdentity(options => { + // For web app, require confirmed email + options.SignIn.RequireConfirmedAccount = true; + options.Password.RequireDigit = true; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = true; + options.Password.RequireLowercase = true; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // Configure cookie authentication for Web + services.ConfigureApplicationCookie(options => + { + options.LoginPath = "/Account/Login"; + options.LogoutPath = "/Account/Logout"; + options.AccessDeniedPath = "/Account/AccessDenied"; + }); + + return services; + } +} diff --git a/5-Aquiis.Professional/Features/Administration/Application/Pages/DailyReport.razor b/5-Aquiis.Professional/Features/Administration/Application/Pages/DailyReport.razor new file mode 100644 index 0000000..5db5067 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Application/Pages/DailyReport.razor @@ -0,0 +1,148 @@ +@page "/administration/application/dailyreport" + +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Application/Pages/InitializeSchemaVersion.razor b/5-Aquiis.Professional/Features/Administration/Application/Pages/InitializeSchemaVersion.razor new file mode 100644 index 0000000..a30925b --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Application/Pages/InitializeSchemaVersion.razor @@ -0,0 +1,64 @@ +@page "/administration/application/initialize-schema" +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Application/Pages/ManageDatabase.razor b/5-Aquiis.Professional/Features/Administration/Application/Pages/ManageDatabase.razor new file mode 100644 index 0000000..862dd19 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Application/Pages/ManageDatabase.razor @@ -0,0 +1,778 @@ +@page "/administration/application/database" +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Dashboard.razor similarity index 100% rename from Aquiis.Professional/Features/Administration/Dashboard.razor rename to 5-Aquiis.Professional/Features/Administration/Dashboard.razor diff --git a/5-Aquiis.Professional/Features/Administration/Organizations/Pages/CreateOrganization.razor b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/CreateOrganization.razor new file mode 100644 index 0000000..b5785f4 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/CreateOrganization.razor @@ -0,0 +1,197 @@ +@page "/administration/organizations/create" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Organizations/Pages/EditOrganization.razor b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/EditOrganization.razor new file mode 100644 index 0000000..2d42d5e --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/EditOrganization.razor @@ -0,0 +1,296 @@ +@page "/administration/organizations/{Id:guid}/edit" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Organizations/Pages/ManageUsers.razor b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/ManageUsers.razor new file mode 100644 index 0000000..64a1833 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/ManageUsers.razor @@ -0,0 +1,454 @@ +@page "/administration/organizations/{Id:guid}/users" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Organizations/Pages/Organizations.razor b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/Organizations.razor new file mode 100644 index 0000000..d1381f8 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/Organizations.razor @@ -0,0 +1,214 @@ +@page "/administration/organizations" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Components.Shared +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Organizations/Pages/ViewOrganization.razor b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/ViewOrganization.razor new file mode 100644 index 0000000..f479787 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Organizations/Pages/ViewOrganization.razor @@ -0,0 +1,344 @@ +@page "/administration/organizations/{Id:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Settings/Pages/CalendarSettings.razor b/5-Aquiis.Professional/Features/Administration/Settings/Pages/CalendarSettings.razor new file mode 100644 index 0000000..ec6e761 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Settings/Pages/CalendarSettings.razor @@ -0,0 +1,378 @@ +@page "/administration/settings/calendar" +@using Aquiis.Core.Entities +@using CalendarSettingsEntity = Aquiis.Core.Entities.CalendarSettings +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.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/5-Aquiis.Professional/Features/Administration/Settings/Pages/EmailSettings.razor b/5-Aquiis.Professional/Features/Administration/Settings/Pages/EmailSettings.razor new file mode 100644 index 0000000..f389e74 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Settings/Pages/EmailSettings.razor @@ -0,0 +1,453 @@ +@page "/administration/settings/email" +@using Aquiis.Application.Services +@using SocketIOClient.Messages +@using System.ComponentModel.DataAnnotations +@inject EmailSettingsService EmailSettingsService +@inject EmailService 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 EmailStats? 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.GetEmailStatsAsync(); + } + } + + 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/5-Aquiis.Professional/Features/Administration/Settings/Pages/LateFeeSettings.razor b/5-Aquiis.Professional/Features/Administration/Settings/Pages/LateFeeSettings.razor new file mode 100644 index 0000000..bae29c4 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Settings/Pages/LateFeeSettings.razor @@ -0,0 +1,439 @@ +@page "/administration/settings/latefees" +@using Aquiis.Core.Constants +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Settings/Pages/OrganizationSettings.razor b/5-Aquiis.Professional/Features/Administration/Settings/Pages/OrganizationSettings.razor new file mode 100644 index 0000000..ab20d86 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Settings/Pages/OrganizationSettings.razor @@ -0,0 +1,566 @@ +@page "/administration/settings/organization" + +@using Aquiis.Core.Entities +@using OrganizationSettingsEntity = Aquiis.Core.Entities.OrganizationSettings +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Settings/Pages/SMSSettings.razor b/5-Aquiis.Professional/Features/Administration/Settings/Pages/SMSSettings.razor new file mode 100644 index 0000000..a4d3a9f --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Settings/Pages/SMSSettings.razor @@ -0,0 +1,419 @@ +@page "/administration/settings/sms" + +@using Aquiis.Application.Services +@using SocketIOClient.Messages +@using System.ComponentModel.DataAnnotations +@using Aquiis.Core.Entities +@inject SMSSettingsService SMSSettingsService +@inject SMSService SMSService + +@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 SMSStats? 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 GetSMSStatsAsync is implemented + if (settings.IsSMSEnabled) + { + stats = await SMSService.GetSMSStatsAsync(); + } + } + + 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/5-Aquiis.Professional/Features/Administration/Settings/Pages/ServiceSettings.razor b/5-Aquiis.Professional/Features/Administration/Settings/Pages/ServiceSettings.razor new file mode 100644 index 0000000..3d4cea7 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Settings/Pages/ServiceSettings.razor @@ -0,0 +1,464 @@ +@page "/administration/settings/services" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Infrastructure.Data +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Users/Manage.razor b/5-Aquiis.Professional/Features/Administration/Users/Manage.razor new file mode 100644 index 0000000..1d08af6 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Users/Manage.razor @@ -0,0 +1,612 @@ +@page "/administration/users/manage" + +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Components.Shared +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Users/Pages/Create.razor b/5-Aquiis.Professional/Features/Administration/Users/Pages/Create.razor new file mode 100644 index 0000000..431e93d --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Users/Pages/Create.razor @@ -0,0 +1,311 @@ +@page "/Administration/Users/Create" + +@using Aquiis.Professional.Shared.Components.Account +@using Aquiis.Core.Constants +@using Aquiis.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/5-Aquiis.Professional/Features/Administration/Users/View.razor b/5-Aquiis.Professional/Features/Administration/Users/View.razor new file mode 100644 index 0000000..6f2f765 --- /dev/null +++ b/5-Aquiis.Professional/Features/Administration/Users/View.razor @@ -0,0 +1,508 @@ +@page "/administration/users/view/{UserId}" + +@using Aquiis.Professional.Shared.Components.Shared +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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; } = default!; + + protected override async Task OnInitializedAsync() + { + //Input = Input ?? new InputModel(); + 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/5-Aquiis.Professional/Features/Calendar/Calendar.razor b/5-Aquiis.Professional/Features/Calendar/Calendar.razor new file mode 100644 index 0000000..49bc13b --- /dev/null +++ b/5-Aquiis.Professional/Features/Calendar/Calendar.razor @@ -0,0 +1,1670 @@ +@page "/Calendar" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Utilities +@using Aquiis.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/{request.Id}"); + } + + private void ShowPropertyDetail(Property property) + { + // Navigate to property detail page + Navigation.NavigateTo($"/propertymanagement/properties/{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("/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/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor b/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor new file mode 100644 index 0000000..7eda510 --- /dev/null +++ b/5-Aquiis.Professional/Features/Calendar/CalendarListView.razor @@ -0,0 +1,877 @@ +@page "/Calendar/ListView" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Utilities +@using Aquiis.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("/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/5-Aquiis.Professional/Features/Notifications/Pages/Index.razor b/5-Aquiis.Professional/Features/Notifications/Pages/Index.razor new file mode 100644 index 0000000..8eeeec1 --- /dev/null +++ b/5-Aquiis.Professional/Features/Notifications/Pages/Index.razor @@ -0,0 +1,8 @@ +@page "/notifications" +@using Aquiis.Professional.Shared.Services +@attribute [OrganizationAuthorize] +@rendermode InteractiveServer + +Notification Center + + diff --git a/5-Aquiis.Professional/Features/Notifications/Pages/Preferences.razor b/5-Aquiis.Professional/Features/Notifications/Pages/Preferences.razor new file mode 100644 index 0000000..bb58200 --- /dev/null +++ b/5-Aquiis.Professional/Features/Notifications/Pages/Preferences.razor @@ -0,0 +1,9 @@ +@page "/notifications/preferences" +@using Aquiis.Professional.Shared.Services +@inject ToastService ToastService +@attribute [OrganizationAuthorize] +@rendermode InteractiveServer + +Notification Preferences - Aquiis + + diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Create.razor new file mode 100644 index 0000000..66752ca --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Create.razor @@ -0,0 +1,616 @@ +@page "/propertymanagement/prospects/{ProspectId:guid}/submit-application" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Validation +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Application.Services.Workflows +@using Aquiis.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.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/5-Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Index.razor new file mode 100644 index 0000000..921a7e0 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Index.razor @@ -0,0 +1,250 @@ +@page "/propertymanagement/applications" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Applications/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Applications/Pages/View.razor new file mode 100644 index 0000000..32dbf6d --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Applications/Pages/View.razor @@ -0,0 +1,1177 @@ +@page "/propertymanagement/applications/{ApplicationId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Application.Services.Workflows +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/ChecklistTemplates/Edit.razor b/5-Aquiis.Professional/Features/PropertyManagement/ChecklistTemplates/Edit.razor new file mode 100644 index 0000000..204061d --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/ChecklistTemplates/Edit.razor @@ -0,0 +1,344 @@ +@page "/propertymanagement/checklists/templates/{TemplateId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/ChecklistTemplates/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/ChecklistTemplates/Index.razor new file mode 100644 index 0000000..a052d2e --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/ChecklistTemplates/Index.razor @@ -0,0 +1,349 @@ +@page "/propertymanagement/checklists/templates" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Complete.razor b/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Complete.razor new file mode 100644 index 0000000..76c5085 --- /dev/null +++ b/5-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.Core.Entities +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.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/{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/{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/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Create.razor new file mode 100644 index 0000000..0361df6 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Create.razor @@ -0,0 +1,352 @@ +@page "/propertymanagement/checklists/create" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Index.razor new file mode 100644 index 0000000..004b123 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Index.razor @@ -0,0 +1,176 @@ +@page "/propertymanagement/checklists" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor b/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor new file mode 100644 index 0000000..2c5e8b9 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor @@ -0,0 +1,368 @@ +@page "/propertymanagement/checklists/mychecklists" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/View.razor new file mode 100644 index 0000000..3b93cfc --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/View.razor @@ -0,0 +1,588 @@ +@page "/propertymanagement/checklists/{ChecklistId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Core.Constants +@using Aquiis.Application.Services +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Documents/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Documents/Pages/Index.razor new file mode 100644 index 0000000..9ae49a2 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Documents/Pages/Index.razor @@ -0,0 +1,586 @@ +@page "/propertymanagement/documents" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.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/{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/5-Aquiis.Professional/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor b/5-Aquiis.Professional/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor new file mode 100644 index 0000000..fd8bb1a --- /dev/null +++ b/5-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.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/5-Aquiis.Professional/Features/PropertyManagement/Documents/Pages/_Imports.razor b/5-Aquiis.Professional/Features/PropertyManagement/Documents/Pages/_Imports.razor new file mode 100644 index 0000000..cab6793 --- /dev/null +++ b/5-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.Infrastructure.Data +@using Aquiis.Core.Entities +@using System.ComponentModel.DataAnnotations diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor b/5-Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor rename to 5-Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionChecklistItem.razor diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Inspections/InspectionSectionView.razor b/5-Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionSectionView.razor similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/Inspections/InspectionSectionView.razor rename to 5-Aquiis.Professional/Features/PropertyManagement/Inspections/InspectionSectionView.razor diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Create.razor new file mode 100644 index 0000000..2a623c6 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Create.razor @@ -0,0 +1,552 @@ +@page "/propertymanagement/inspections/create" +@page "/propertymanagement/inspections/create/{PropertyId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Validation +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/{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/5-Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Schedule.razor b/5-Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Schedule.razor new file mode 100644 index 0000000..e63c83a --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Schedule.razor @@ -0,0 +1,323 @@ +@page "/propertymanagement/inspections/schedule" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.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/{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/5-Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/View.razor new file mode 100644 index 0000000..11afba5 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/View.razor @@ -0,0 +1,426 @@ +@page "/propertymanagement/inspections/{InspectionId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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.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/{inspection.PropertyId}"); + } + else + { + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + } +} diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Create.razor new file mode 100644 index 0000000..1608f35 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Create.razor @@ -0,0 +1,317 @@ +@page "/propertymanagement/invoices/create" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Edit.razor b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Edit.razor new file mode 100644 index 0000000..fc3cdc6 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Edit.razor @@ -0,0 +1,396 @@ +@page "/propertymanagement/invoices/{Id:guid}/edit" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.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/{Id}"); + } + + private void ViewLease() + { + if (invoice?.LeaseId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{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/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Index.razor new file mode 100644 index 0000000..d2d680e --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Index.razor @@ -0,0 +1,592 @@ +@page "/propertymanagement/invoices" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.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/{id}"); + } + + private void EditInvoice(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/invoices/{id}/edit"); + } + + 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/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/View.razor new file mode 100644 index 0000000..e7fdfea --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/View.razor @@ -0,0 +1,408 @@ +@page "/propertymanagement/invoices/{Id:guid}" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.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/{Id}/edit"); + } + + private void RecordPayment() + { + NavigationManager.NavigateTo($"/propertymanagement/payments/create?invoiceId={Id}"); + } + + private void ViewLease() + { + if (invoice?.LeaseId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{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.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/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/Accept.razor b/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/Accept.razor new file mode 100644 index 0000000..a8694ea --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/Accept.razor @@ -0,0 +1,637 @@ +@page "/propertymanagement/leases/{LeaseId:guid}/accept" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/Create.razor new file mode 100644 index 0000000..566b95a --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/Create.razor @@ -0,0 +1,449 @@ +@page "/propertymanagement/applications/{ApplicationId:guid}/generate-lease-offer" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Application.Services.Workflows +@using Aquiis.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.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/{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/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/Index.razor new file mode 100644 index 0000000..474a5e7 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/Index.razor @@ -0,0 +1,165 @@ +@page "/propertymanagement/leaseoffers" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/{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/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/View.razor new file mode 100644 index 0000000..454930a --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/View.razor @@ -0,0 +1,574 @@ +@page "/propertymanagement/leaseoffers/{Id:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Application.Services.Workflows +@using Aquiis.Infrastructure.Data +@using Aquiis.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/{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/{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/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Create.razor new file mode 100644 index 0000000..3e6b16b --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Create.razor @@ -0,0 +1,383 @@ +@page "/propertymanagement/leases/create" + +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Edit.razor b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Edit.razor new file mode 100644 index 0000000..131c14a --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Edit.razor @@ -0,0 +1,357 @@ +@page "/propertymanagement/leases/{Id:guid}/edit" + +@using Aquiis.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/{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/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Index.razor new file mode 100644 index 0000000..f2a8cfa --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Index.razor @@ -0,0 +1,837 @@ +@page "/propertymanagement/leases" + +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.EntityFrameworkCore +@using Aquiis.Infrastructure.Data +@using Aquiis.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/{id}"); + } + + private void EditLease(Guid id) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{id}/edit"); + } + + 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/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/View.razor new file mode 100644 index 0000000..308a59d --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/View.razor @@ -0,0 +1,1263 @@ +@page "/propertymanagement/leases/{Id:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.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.Application.Services +@using Aquiis.Application.Services.Workflows +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/{Id}/edit"); + } + + 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/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor new file mode 100644 index 0000000..6e8c922 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor @@ -0,0 +1,354 @@ +@page "/propertymanagement/maintenance/create/{PropertyId:int?}" +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor new file mode 100644 index 0000000..81a664a --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor @@ -0,0 +1,306 @@ +@page "/propertymanagement/maintenance/{Id:guid}/edit" +@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/{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/{Id}"); + } +} diff --git a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor new file mode 100644 index 0000000..c0f61d9 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Index.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/{requestId}"); + } + + private void ViewProperty(Guid propertyId) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/{propertyId}"); + } +} diff --git a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor new file mode 100644 index 0000000..c60a539 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor @@ -0,0 +1,309 @@ +@page "/propertymanagement/maintenance/{Id:guid}" + +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/{Id}/edit"); + } + + private void ViewProperty() + { + if (maintenanceRequest?.PropertyId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/{maintenanceRequest.PropertyId}"); + } + } + + private void GoBack() + { + NavigationManager.NavigateTo("/propertymanagement/maintenance"); + } +} diff --git a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor new file mode 100644 index 0000000..c24e5c5 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor @@ -0,0 +1,4 @@ +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Microsoft.AspNetCore.Authorization diff --git a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/CreatePayment.razor b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Create.razor similarity index 100% rename from Aquiis.Professional/Features/PropertyManagement/Payments/Pages/CreatePayment.razor rename to 5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Create.razor diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Edit.razor b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Edit.razor new file mode 100644 index 0000000..f951cfd --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Edit.razor @@ -0,0 +1,278 @@ +@page "/propertymanagement/payments/{PaymentId:guid}/edit" +@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/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Index.razor new file mode 100644 index 0000000..2eb80b5 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Index.razor @@ -0,0 +1,492 @@ +@page "/propertymanagement/payments" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.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/{id}"); + } + + private void EditPayment(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/payments/{id}/edit"); + } + + 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/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/View.razor new file mode 100644 index 0000000..76fd559 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/View.razor @@ -0,0 +1,417 @@ +@page "/propertymanagement/payments/{PaymentId:guid}" + +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.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/{PaymentId}/edit"); + } + + 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.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/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/_Imports.razor b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/_Imports.razor new file mode 100644 index 0000000..09581e0 --- /dev/null +++ b/5-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.Infrastructure.Data +@using Aquiis.Core.Entities diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Create.razor new file mode 100644 index 0000000..ad962d9 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Create.razor @@ -0,0 +1,260 @@ +@page "/propertymanagement/properties/create" +@using Aquiis.Core.Constants +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Edit.razor b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Edit.razor new file mode 100644 index 0000000..0556bda --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Edit.razor @@ -0,0 +1,399 @@ +@page "/propertymanagement/properties/{PropertyId:guid}/edit" + +@using System.ComponentModel.DataAnnotations +@using Aquiis.Core.Entities +@using Aquiis.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/{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/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Index.razor new file mode 100644 index 0000000..894ad5b --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Index.razor @@ -0,0 +1,558 @@ +@page "/propertymanagement/properties" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.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/{propertyId}"); + } + + private void EditProperty(Guid propertyId) + { + Navigation.NavigateTo($"/propertymanagement/properties/{propertyId}/edit"); + } + + 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/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor new file mode 100644 index 0000000..99d3de8 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor @@ -0,0 +1,626 @@ +@page "/propertymanagement/properties/{PropertyId:guid}" +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Core.Constants +@using Aquiis.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/{PropertyId}/edit"); + } + + private void CreateLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/create/?propertyid={PropertyId}"); + } + + private void ViewLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{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/{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/5-Aquiis.Professional/Features/PropertyManagement/Prospects/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Prospects/Index.razor new file mode 100644 index 0000000..aadecb9 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Prospects/Index.razor @@ -0,0 +1,476 @@ +@page "/PropertyManagement/ProspectiveTenants" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Constants +@using Aquiis.Core.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/5-Aquiis.Professional/Features/PropertyManagement/Prospects/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Prospects/View.razor new file mode 100644 index 0000000..8193f8b --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Prospects/View.razor @@ -0,0 +1,814 @@ +@page "/PropertyManagement/ProspectiveTenants/{ProspectId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Constants +@using Aquiis.Core.Utilities +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authorization +@using System.Runtime.Serialization + +@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}"); + } + } + + 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/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor new file mode 100644 index 0000000..9bf5858 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor @@ -0,0 +1,240 @@ +@page "/reports/income-statement" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor new file mode 100644 index 0000000..1d44f5d --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor @@ -0,0 +1,259 @@ +@page "/reports/property-performance" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/RentRollReport.razor b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/RentRollReport.razor new file mode 100644 index 0000000..4a2c774 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/RentRollReport.razor @@ -0,0 +1,243 @@ +@page "/reports/rentroll" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/Reports.razor b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/Reports.razor new file mode 100644 index 0000000..3cf646c --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/Reports.razor @@ -0,0 +1,278 @@ +@page "/reports" +@using Microsoft.AspNetCore.Authorization +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/TaxReport.razor b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/TaxReport.razor new file mode 100644 index 0000000..c502dc9 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Reports/Pages/TaxReport.razor @@ -0,0 +1,287 @@ +@page "/reports/tax-report" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Components/SampleFeatureCompnentOne.razor b/5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Components/SampleFeatureCompnentOne.razor similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Components/SampleFeatureCompnentOne.razor rename to 5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Components/SampleFeatureCompnentOne.razor diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Create.razor similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Create.razor rename to 5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Create.razor diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Edit.razor b/5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Edit.razor similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Edit.razor rename to 5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Edit.razor diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Index.razor similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/Index.razor rename to 5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/Index.razor diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/View.razor similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/Pages/View.razor rename to 5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/Pages/View.razor diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs b/5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs rename to 5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureContext.cs diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs b/5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs rename to 5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureEntity.cs diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs b/5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs similarity index 100% rename from Aquiis.SimpleStart/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs rename to 5-Aquiis.Professional/Features/PropertyManagement/SampleFeature/SampleFeatureService.cs diff --git a/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor b/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor new file mode 100644 index 0000000..f090260 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor @@ -0,0 +1,337 @@ +@page "/property-management/security-deposits/calculate-dividends/{PoolId:guid}" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor b/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor new file mode 100644 index 0000000..47e0fe3 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor @@ -0,0 +1,345 @@ +@page "/property-management/security-deposits/investment-pools" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor b/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor new file mode 100644 index 0000000..057ed8b --- /dev/null +++ b/5-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.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor b/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor new file mode 100644 index 0000000..5e2c026 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor @@ -0,0 +1,401 @@ +@page "/property-management/security-deposits" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.Core.Constants +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor b/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor new file mode 100644 index 0000000..ab8e749 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor @@ -0,0 +1,419 @@ +@page "/property-management/security-deposits/investment-pool/{PoolId:guid}" +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor new file mode 100644 index 0000000..e7b4871 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor @@ -0,0 +1,217 @@ +@page "/propertymanagement/tenants/create" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Edit.razor b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Edit.razor new file mode 100644 index 0000000..211d029 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Edit.razor @@ -0,0 +1,339 @@ +@page "/propertymanagement/tenants/{Id:guid}/edit" +@using Aquiis.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/{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/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Index.razor new file mode 100644 index 0000000..1e6eb85 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Index.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/{id}"); + } + + private void EditTenant(Guid id) + { + Navigation.NavigateTo($"/propertymanagement/tenants/{id}/edit"); + } + + 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/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/View.razor new file mode 100644 index 0000000..c5e9367 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/View.razor @@ -0,0 +1,241 @@ +@page "/propertymanagement/tenants/{Id:guid}" +@using Aquiis.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/{Id}/edit"); + } + + 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/5-Aquiis.Professional/Features/PropertyManagement/Tours/Calendar.razor b/5-Aquiis.Professional/Features/PropertyManagement/Tours/Calendar.razor new file mode 100644 index 0000000..c96fae0 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tours/Calendar.razor @@ -0,0 +1,667 @@ +@page "/PropertyManagement/Tours/Calendar" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Tours/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Tours/Create.razor new file mode 100644 index 0000000..02441f5 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tours/Create.razor @@ -0,0 +1,326 @@ +@page "/PropertyManagement/Tours/Schedule/{ProspectId:guid}" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Features/PropertyManagement/Tours/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Tours/Index.razor new file mode 100644 index 0000000..2ef2076 --- /dev/null +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tours/Index.razor @@ -0,0 +1,397 @@ +@page "/PropertyManagement/Tours" + +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/{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/{checklistId}"); + } +} diff --git a/5-Aquiis.Professional/Features/_Imports.razor b/5-Aquiis.Professional/Features/_Imports.razor new file mode 100644 index 0000000..0c7307e --- /dev/null +++ b/5-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.Application.Services +@using Aquiis.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.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.Professional.Features.Administration diff --git a/Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-Bold.ttf b/5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Bold.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-Bold.ttf rename to 5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Bold.ttf diff --git a/Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf b/5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf rename to 5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans-BoldOblique.ttf diff --git a/Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf b/5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf rename to 5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans-ExtraLight.ttf diff --git a/Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-Oblique.ttf b/5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Oblique.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans-Oblique.ttf rename to 5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans-Oblique.ttf diff --git a/Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans.ttf b/5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSans.ttf rename to 5-Aquiis.Professional/Fonts/DejaVu/DejaVuSans.ttf diff --git a/Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf b/5-Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf rename to 5-Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Bold.ttf diff --git a/Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf b/5-Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf rename to 5-Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-BoldOblique.ttf diff --git a/Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf b/5-Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf rename to 5-Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed-Oblique.ttf diff --git a/Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed.ttf b/5-Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/DejaVu/DejaVuSansCondensed.ttf rename to 5-Aquiis.Professional/Fonts/DejaVu/DejaVuSansCondensed.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-Black.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-Black.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-Black.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-Black.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-BlackItalic.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-BlackItalic.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-BlackItalic.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-BlackItalic.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-Bold.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-Bold.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-Bold.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-Bold.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-BoldItalic.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-BoldItalic.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-BoldItalic.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-BoldItalic.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraBold.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBold.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraBold.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBold.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-ExtraBoldItalic.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraLight.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLight.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraLight.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLight.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraLightItalic.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLightItalic.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-ExtraLightItalic.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-ExtraLightItalic.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-Italic.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-Italic.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-Italic.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-Italic.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-Light.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-Light.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-Light.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-Light.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-LightItalic.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-LightItalic.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-LightItalic.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-LightItalic.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-Medium.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-Medium.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-Medium.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-Medium.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-MediumItalic.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-MediumItalic.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-MediumItalic.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-MediumItalic.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-Regular.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-Regular.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-Regular.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-Regular.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-SemiBold.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-SemiBold.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-SemiBold.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-SemiBold.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-SemiBoldItalic.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-SemiBoldItalic.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-SemiBoldItalic.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-SemiBoldItalic.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-Thin.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-Thin.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-Thin.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-Thin.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/Lato-ThinItalic.ttf b/5-Aquiis.Professional/Fonts/LatoFont/Lato-ThinItalic.ttf similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/Lato-ThinItalic.ttf rename to 5-Aquiis.Professional/Fonts/LatoFont/Lato-ThinItalic.ttf diff --git a/Aquiis.SimpleStart/Fonts/LatoFont/OFL.txt b/5-Aquiis.Professional/Fonts/LatoFont/OFL.txt similarity index 100% rename from Aquiis.SimpleStart/Fonts/LatoFont/OFL.txt rename to 5-Aquiis.Professional/Fonts/LatoFont/OFL.txt diff --git a/5-Aquiis.Professional/GlobalUsings.cs b/5-Aquiis.Professional/GlobalUsings.cs new file mode 100644 index 0000000..ca183d9 --- /dev/null +++ b/5-Aquiis.Professional/GlobalUsings.cs @@ -0,0 +1,3 @@ +// Global using directives +global using Aquiis.Professional.Entities; +global using ApplicationDbContext = Aquiis.Infrastructure.Data.ApplicationDbContext; diff --git a/5-Aquiis.Professional/Program.cs b/5-Aquiis.Professional/Program.cs new file mode 100644 index 0000000..87f3afe --- /dev/null +++ b/5-Aquiis.Professional/Program.cs @@ -0,0 +1,584 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Aquiis.Core.Constants; +using Aquiis.Core.Interfaces; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Professional.Shared.Services; +using Aquiis.Professional.Shared.Authorization; +using Aquiis.Professional.Extensions; +using Aquiis.Application.Services; +using Aquiis.Application.Services.Workflows; +using Aquiis.Professional.Data; +using Aquiis.Professional.Entities; +using ElectronNET.API; +using Microsoft.Extensions.Options; +using Aquiis.Application.Services.PdfGenerators; +using Aquiis.Professional.Shared.Components.Account; + + +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(); + +// Add antiforgery services with options for Blazor +builder.Services.AddAntiforgery(options => +{ + options.HeaderName = "X-CSRF-TOKEN"; + // Allow cookies over HTTP for Electron/Development + if (HybridSupport.IsElectronActive || builder.Environment.IsDevelopment()) + { + options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + } +}); + + + //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(); + +// Add platform-specific infrastructure services (Database, Identity, Path services) +if (HybridSupport.IsElectronActive) +{ + builder.Services.AddElectronServices(builder.Configuration); +} +else +{ + builder.Services.AddWebServices(builder.Configuration); +} + +// 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 events (cookie lifetime already configured in extension methods) +builder.Services.ConfigureApplicationCookie(options => +{ + 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(); // Concrete class for components that need it +builder.Services.AddScoped(sp => sp.GetRequiredService()); // Interface alias +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(); +// SendGridEmailService and TwilioSMSService registered in extension methods + +// Workflow services +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()) +{ + // Get services + var dbService = scope.ServiceProvider.GetRequiredService(); + var identityContext = 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); + + // Check pending migrations for both contexts + var businessPendingCount = await dbService.GetPendingMigrationsCountAsync(); + var identityPendingCount = await dbService.GetIdentityPendingMigrationsCountAsync(); + + if (businessPendingCount > 0 || identityPendingCount > 0) + { + var totalCount = businessPendingCount + identityPendingCount; + app.Logger.LogInformation("Found {Count} pending migrations ({BusinessCount} business, {IdentityCount} identity)", + totalCount, businessPendingCount, identityPendingCount); + + // 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 using DatabaseService + await dbService.InitializeAsync(); + + 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); + + // Apply migrations using DatabaseService + await dbService.InitializeAsync(); + + 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"); + + // 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 for both contexts + var businessPendingCount = await dbService.GetPendingMigrationsCountAsync(); + var identityPendingCount = await dbService.GetIdentityPendingMigrationsCountAsync(); + + var isNewDatabase = businessPendingCount == 0 && identityPendingCount == 0; + + if (businessPendingCount > 0 || identityPendingCount > 0) + { + var totalCount = businessPendingCount + identityPendingCount; + app.Logger.LogInformation("Found {Count} pending migrations ({BusinessCount} business, {IdentityCount} identity)", + totalCount, businessPendingCount, identityPendingCount); + + // Create backup before migration + var backupPath = await backupService.CreatePreMigrationBackupAsync(); + if (backupPath != null) + { + app.Logger.LogInformation("Database backed up to {BackupPath}", backupPath); + } + } + + // Apply migrations to both contexts + if (identityPendingCount > 0 || businessPendingCount > 0) + { + app.Logger.LogInformation("Applying migrations ({Identity} identity, {Business} business)", + identityPendingCount, businessPendingCount); + await dbService.InitializeAsync(); + } + + 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.UseAuthentication(); +app.UseAuthorization(); +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/5-Aquiis.Professional/Properties/launchSettings.json similarity index 100% rename from Aquiis.Professional/Properties/launchSettings.json rename to 5-Aquiis.Professional/Properties/launchSettings.json diff --git a/Aquiis.Professional/README.md b/5-Aquiis.Professional/README.md similarity index 100% rename from Aquiis.Professional/README.md rename to 5-Aquiis.Professional/README.md diff --git a/5-Aquiis.Professional/Services/ElectronPathService.cs b/5-Aquiis.Professional/Services/ElectronPathService.cs new file mode 100644 index 0000000..4478074 --- /dev/null +++ b/5-Aquiis.Professional/Services/ElectronPathService.cs @@ -0,0 +1,76 @@ +using ElectronNET.API; +using ElectronNET.API.Entities; +using Microsoft.Extensions.Configuration; +using Aquiis.Core.Interfaces; + +namespace Aquiis.Professional.Services; + +/// +/// Electron-specific implementation of path service. +/// Manages file paths and connection strings for Electron desktop applications. +/// +public class ElectronPathService : IPathService +{ + private readonly IConfiguration _configuration; + + public ElectronPathService(IConfiguration configuration) + { + _configuration = configuration; + } + + /// + public bool IsActive => HybridSupport.IsElectronActive; + + /// + public async Task GetConnectionStringAsync(object configuration) + { + var dbPath = await GetDatabasePathAsync(); + return $"DataSource={dbPath};Cache=Shared"; + } + + /// + public async Task GetDatabasePathAsync() + { + var dbFileName = _configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; + + if (HybridSupport.IsElectronActive) + { + var userDataPath = await GetUserDataPathAsync(); + 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 + { + // Fallback to local path if not in Electron mode + var dataDir = Path.Combine(Directory.GetCurrentDirectory(), "Data"); + if (!Directory.Exists(dataDir)) + { + Directory.CreateDirectory(dataDir); + } + return Path.Combine(dataDir, dbFileName); + } + } + + /// + public async Task GetUserDataPathAsync() + { + if (HybridSupport.IsElectronActive) + { + return await Electron.App.GetPathAsync(PathName.UserData); + } + else + { + // Fallback for non-Electron mode + return Path.Combine(Directory.GetCurrentDirectory(), "Data"); + } + } + +} diff --git a/5-Aquiis.Professional/Services/WebPathService.cs b/5-Aquiis.Professional/Services/WebPathService.cs new file mode 100644 index 0000000..98f8c55 --- /dev/null +++ b/5-Aquiis.Professional/Services/WebPathService.cs @@ -0,0 +1,50 @@ +using Aquiis.Core.Interfaces; +using Microsoft.Extensions.Configuration; + +namespace Aquiis.Professional.Services; + +/// +/// Path service for web/server applications. +/// Uses standard server file system paths. +/// +public class WebPathService : IPathService +{ + private readonly IConfiguration _configuration; + + public WebPathService(IConfiguration configuration) + { + _configuration = configuration; + } + + public bool IsActive => true; + + public async Task GetConnectionStringAsync(object configuration) + { + if (configuration is IConfiguration config) + { + return await Task.Run(() => config.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.")); + } + throw new ArgumentException("Configuration must be IConfiguration", nameof(configuration)); + } + + public async Task GetDatabasePathAsync() + { + var connectionString = await GetConnectionStringAsync(_configuration); + // Extract Data Source from connection string + var dataSource = connectionString.Split(';') + .FirstOrDefault(s => s.Trim().StartsWith("Data Source=", StringComparison.OrdinalIgnoreCase)); + + if (dataSource != null) + { + return dataSource.Split('=')[1].Trim(); + } + + return "aquiis.db"; // Default + } + + public async Task GetUserDataPathAsync() + { + return await Task.Run(() => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); + } +} diff --git a/5-Aquiis.Professional/Shared/App.razor b/5-Aquiis.Professional/Shared/App.razor new file mode 100644 index 0000000..3e6deb4 --- /dev/null +++ b/5-Aquiis.Professional/Shared/App.razor @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Aquiis.Professional/Shared/Authorization/OrganizationAuthorizeAttribute.cs b/5-Aquiis.Professional/Shared/Authorization/OrganizationAuthorizeAttribute.cs similarity index 100% rename from Aquiis.Professional/Shared/Authorization/OrganizationAuthorizeAttribute.cs rename to 5-Aquiis.Professional/Shared/Authorization/OrganizationAuthorizeAttribute.cs diff --git a/Aquiis.Professional/Shared/Authorization/OrganizationPolicyProvider.cs b/5-Aquiis.Professional/Shared/Authorization/OrganizationPolicyProvider.cs similarity index 100% rename from Aquiis.Professional/Shared/Authorization/OrganizationPolicyProvider.cs rename to 5-Aquiis.Professional/Shared/Authorization/OrganizationPolicyProvider.cs diff --git a/5-Aquiis.Professional/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs b/5-Aquiis.Professional/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs new file mode 100644 index 0000000..9b69f71 --- /dev/null +++ b/5-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.Infrastructure.Data; +using Aquiis.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/5-Aquiis.Professional/Shared/Authorization/OrganizationRoleRequirement.cs similarity index 100% rename from Aquiis.Professional/Shared/Authorization/OrganizationRoleRequirement.cs rename to 5-Aquiis.Professional/Shared/Authorization/OrganizationRoleRequirement.cs diff --git a/Aquiis.Professional/Shared/Components/Account/AccountConstants.cs b/5-Aquiis.Professional/Shared/Components/Account/AccountConstants.cs similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/AccountConstants.cs rename to 5-Aquiis.Professional/Shared/Components/Account/AccountConstants.cs diff --git a/Aquiis.Professional/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs b/5-Aquiis.Professional/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs rename to 5-Aquiis.Professional/Shared/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs diff --git a/5-Aquiis.Professional/Shared/Components/Account/IdentityNoOpEmailSender.cs b/5-Aquiis.Professional/Shared/Components/Account/IdentityNoOpEmailSender.cs new file mode 100644 index 0000000..7ec79d8 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/IdentityNoOpEmailSender.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Aquiis.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/5-Aquiis.Professional/Shared/Components/Account/IdentityRedirectManager.cs similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/IdentityRedirectManager.cs rename to 5-Aquiis.Professional/Shared/Components/Account/IdentityRedirectManager.cs diff --git a/5-Aquiis.Professional/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs b/5-Aquiis.Professional/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs new file mode 100644 index 0000000..41062ee --- /dev/null +++ b/5-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.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/5-Aquiis.Professional/Shared/Components/Account/IdentityUserAccessor.cs b/5-Aquiis.Professional/Shared/Components/Account/IdentityUserAccessor.cs new file mode 100644 index 0000000..eca758b --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/IdentityUserAccessor.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Identity; +using Aquiis.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.SimpleStart/Shared/Components/Account/Pages/AccessDenied.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/AccessDenied.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/AccessDenied.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/AccessDenied.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Pages/ConfirmEmail.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmail.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/ConfirmEmail.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmail.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Pages/ConfirmEmailChange.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmailChange.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/ConfirmEmailChange.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/ConfirmEmailChange.razor diff --git a/5-Aquiis.Professional/Shared/Components/Account/Pages/ExternalLogin.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/ExternalLogin.razor new file mode 100644 index 0000000..2a5adf0 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/ExternalLogin.razor @@ -0,0 +1,206 @@ +@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; } = default!; + + [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() + { + Input = Input ?? new InputModel(); + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/ForgotPassword.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/ForgotPassword.razor new file mode 100644 index 0000000..1e127a6 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/ForgotPassword.razor @@ -0,0 +1,72 @@ +@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; } = default!; + + protected override Task OnParametersSetAsync() + { + Input = Input ?? new InputModel(); + return base.OnParametersSetAsync(); + } + 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.SimpleStart/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/ForgotPasswordConfirmation.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Pages/InvalidPasswordReset.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/InvalidPasswordReset.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/InvalidPasswordReset.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/InvalidPasswordReset.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Pages/InvalidUser.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/InvalidUser.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/InvalidUser.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/InvalidUser.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Lockout.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Lockout.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/Lockout.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/Lockout.razor diff --git a/5-Aquiis.Professional/Shared/Components/Account/Pages/Login.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Login.razor new file mode 100644 index 0000000..d335e19 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Login.razor @@ -0,0 +1,134 @@ +@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; } = default!; + + [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); + } + } + + protected override Task OnParametersSetAsync() + { + ReturnUrl ??= NavigationManager.BaseUri; + Input = Input ?? new InputModel(); + return base.OnParametersSetAsync(); + } + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/LoginWith2fa.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/LoginWith2fa.razor new file mode 100644 index 0000000..56e6de6 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/LoginWith2fa.razor @@ -0,0 +1,106 @@ +@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; } = default!; + + [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."); + } + + protected override Task OnParametersSetAsync() + { + Input = Input ?? new InputModel(); + return base.OnParametersSetAsync(); + } + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor new file mode 100644 index 0000000..dd7cf29 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor @@ -0,0 +1,90 @@ +@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; } = default!; + + [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."); + } + + protected override Task OnParametersSetAsync() + { + Input = Input ?? new InputModel(); + return base.OnParametersSetAsync(); + } + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/ChangePassword.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/ChangePassword.razor new file mode 100644 index 0000000..082cd71 --- /dev/null +++ b/5-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; } = default!; + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor new file mode 100644 index 0000000..0970813 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor @@ -0,0 +1,84 @@ +@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; } = default!; + + protected override async Task OnInitializedAsync() + { + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/Disable2fa.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/Disable2fa.razor new file mode 100644 index 0000000..ad461c8 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/Disable2fa.razor @@ -0,0 +1,61 @@ +@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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/Email.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/Email.razor new file mode 100644 index 0000000..434373d --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/Email.razor @@ -0,0 +1,121 @@ +@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; } = default!; + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor new file mode 100644 index 0000000..0ffa261 --- /dev/null +++ b/5-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; } = default!; + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/ExternalLogins.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/ExternalLogins.razor new file mode 100644 index 0000000..5f772dc --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/ExternalLogins.razor @@ -0,0 +1,137 @@ +@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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor new file mode 100644 index 0000000..093516e --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor @@ -0,0 +1,66 @@ +@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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/Index.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/Index.razor new file mode 100644 index 0000000..5fc0b84 --- /dev/null +++ b/5-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; } = default!; + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/PersonalData.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/PersonalData.razor new file mode 100644 index 0000000..5ca6b75 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/PersonalData.razor @@ -0,0 +1,33 @@ +@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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor new file mode 100644 index 0000000..73fad62 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor @@ -0,0 +1,50 @@ +@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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/SetPassword.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/SetPassword.razor new file mode 100644 index 0000000..a3ae060 --- /dev/null +++ b/5-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; } = default!; + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor new file mode 100644 index 0000000..0084aea --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor @@ -0,0 +1,99 @@ +@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.SimpleStart/Shared/Components/Account/Pages/Manage/_Imports.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/_Imports.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/_Imports.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/Manage/_Imports.razor diff --git a/5-Aquiis.Professional/Shared/Components/Account/Pages/Register.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Register.razor new file mode 100644 index 0000000..53ad596 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Register.razor @@ -0,0 +1,277 @@ +@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.Infrastructure.Data +@using Aquiis.Application.Services +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Microsoft.EntityFrameworkCore + +@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; } = default!; + + [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 users = await UserManager.Users + .Where(u => u.Id != ApplicationConstants.SystemUser.Id) + .ToListAsync(); + + var userCount = users.Count; + + _isFirstUser = userCount == 0; + _allowRegistration = _isFirstUser; // Only allow registration if this is the first user + } + + protected override Task OnParametersSetAsync() + { + ReturnUrl ??= NavigationManager.BaseUri; + Input = Input ?? new InputModel(); + return base.OnParametersSetAsync(); + } + + 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.SimpleStart/Shared/Components/Account/Pages/RegisterConfirmation.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/RegisterConfirmation.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor diff --git a/5-Aquiis.Professional/Shared/Components/Account/Pages/ResendEmailConfirmation.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/ResendEmailConfirmation.razor new file mode 100644 index 0000000..278b6c6 --- /dev/null +++ b/5-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; } = default!; + + 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/5-Aquiis.Professional/Shared/Components/Account/Pages/ResetPassword.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/ResetPassword.razor new file mode 100644 index 0000000..fc7c209 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/ResetPassword.razor @@ -0,0 +1,107 @@ +@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; } = default!; + + [SupplyParameterFromQuery] + private string? Code { get; set; } + + private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; + + protected override Task OnParametersSetAsync() + { + Input = Input ?? new InputModel(); + return base.OnParametersSetAsync(); + } + 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.SimpleStart/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Pages/ResetPasswordConfirmation.razor diff --git a/5-Aquiis.Professional/Shared/Components/Account/Pages/_Imports.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/_Imports.razor new file mode 100644 index 0000000..0bc513f --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/_Imports.razor @@ -0,0 +1 @@ +@using Aquiis.Professional.Shared.Components.Account.Shared diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Shared/ExternalLoginPicker.razor b/5-Aquiis.Professional/Shared/Components/Account/Shared/ExternalLoginPicker.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Shared/ExternalLoginPicker.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Shared/ExternalLoginPicker.razor diff --git a/Aquiis.Professional/Shared/Components/Account/Shared/ManageLayout.razor b/5-Aquiis.Professional/Shared/Components/Account/Shared/ManageLayout.razor similarity index 100% rename from Aquiis.Professional/Shared/Components/Account/Shared/ManageLayout.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Shared/ManageLayout.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Shared/ManageNavMenu.razor b/5-Aquiis.Professional/Shared/Components/Account/Shared/ManageNavMenu.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Shared/ManageNavMenu.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Shared/ManageNavMenu.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Shared/RedirectToLogin.razor b/5-Aquiis.Professional/Shared/Components/Account/Shared/RedirectToLogin.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Shared/RedirectToLogin.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Shared/RedirectToLogin.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Shared/ShowRecoveryCodes.razor b/5-Aquiis.Professional/Shared/Components/Account/Shared/ShowRecoveryCodes.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Shared/ShowRecoveryCodes.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Shared/ShowRecoveryCodes.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Account/Shared/StatusMessage.razor b/5-Aquiis.Professional/Shared/Components/Account/Shared/StatusMessage.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Account/Shared/StatusMessage.razor rename to 5-Aquiis.Professional/Shared/Components/Account/Shared/StatusMessage.razor diff --git a/5-Aquiis.Professional/Shared/Components/AuthorizedHeaderSection.razor b/5-Aquiis.Professional/Shared/Components/AuthorizedHeaderSection.razor new file mode 100644 index 0000000..6f7537e --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/AuthorizedHeaderSection.razor @@ -0,0 +1,7 @@ +@using Aquiis.Professional.Shared.Components +@rendermode InteractiveServer + +
+ + +
diff --git a/5-Aquiis.Professional/Shared/Components/LeaseRenewalWidget.razor b/5-Aquiis.Professional/Shared/Components/LeaseRenewalWidget.razor new file mode 100644 index 0000000..8ce5388 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/LeaseRenewalWidget.razor @@ -0,0 +1,180 @@ +@using Aquiis.Application.Services +@using Aquiis.Professional.Features.PropertyManagement +@using Aquiis.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/5-Aquiis.Professional/Shared/Components/NotesTimeline.razor b/5-Aquiis.Professional/Shared/Components/NotesTimeline.razor new file mode 100644 index 0000000..5bb0ed7 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/NotesTimeline.razor @@ -0,0 +1,246 @@ +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.Application.Services.PdfGenerators +@using Aquiis.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/5-Aquiis.Professional/Shared/Components/NotificationBellWrapper.razor b/5-Aquiis.Professional/Shared/Components/NotificationBellWrapper.razor new file mode 100644 index 0000000..158a290 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/NotificationBellWrapper.razor @@ -0,0 +1,4 @@ +@using Aquiis.UI.Shared.Features.Notifications +@using Aquiis.Professional.Shared.Services + + diff --git a/5-Aquiis.Professional/Shared/Components/OrganizationSwitcher.razor b/5-Aquiis.Professional/Shared/Components/OrganizationSwitcher.razor new file mode 100644 index 0000000..8405134 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/OrganizationSwitcher.razor @@ -0,0 +1,189 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Core.Constants +@inject OrganizationService OrganizationService +@inject UserContextService UserContext +@inject NavigationManager Navigation +@implements IDisposable + +@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.SimpleStart/Shared/Components/Pages/About.razor b/5-Aquiis.Professional/Shared/Components/Pages/About.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Pages/About.razor rename to 5-Aquiis.Professional/Shared/Components/Pages/About.razor diff --git a/Aquiis.SimpleStart/Shared/Components/Pages/Error.razor b/5-Aquiis.Professional/Shared/Components/Pages/Error.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/Pages/Error.razor rename to 5-Aquiis.Professional/Shared/Components/Pages/Error.razor diff --git a/5-Aquiis.Professional/Shared/Components/Pages/Home.razor b/5-Aquiis.Professional/Shared/Components/Pages/Home.razor new file mode 100644 index 0000000..cff5610 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Pages/Home.razor @@ -0,0 +1,479 @@ +@page "/" +@using Aquiis.Application.Services +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.EntityFrameworkCore +@using Aquiis.Infrastructure.Data +@using Aquiis.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("/calendar"); + } +} diff --git a/5-Aquiis.Professional/Shared/Components/SchemaValidationWarning.razor b/5-Aquiis.Professional/Shared/Components/SchemaValidationWarning.razor new file mode 100644 index 0000000..7bacee8 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/SchemaValidationWarning.razor @@ -0,0 +1,66 @@ +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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.SimpleStart/Shared/Components/SessionTimeoutModal.razor b/5-Aquiis.Professional/Shared/Components/SessionTimeoutModal.razor similarity index 100% rename from Aquiis.SimpleStart/Shared/Components/SessionTimeoutModal.razor rename to 5-Aquiis.Professional/Shared/Components/SessionTimeoutModal.razor diff --git a/5-Aquiis.Professional/Shared/Components/Shared/OrganizationAuthorizeView.razor b/5-Aquiis.Professional/Shared/Components/Shared/OrganizationAuthorizeView.razor new file mode 100644 index 0000000..f77915a --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/Shared/OrganizationAuthorizeView.razor @@ -0,0 +1,62 @@ +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Shared/Components/ToastContainer.razor b/5-Aquiis.Professional/Shared/Components/ToastContainer.razor new file mode 100644 index 0000000..06f55e2 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Components/ToastContainer.razor @@ -0,0 +1,164 @@ +@using Aquiis.Application.Services +@using Aquiis.Professional.Shared.Services +@using Aquiis.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/5-Aquiis.Professional/Shared/Layout/MainLayout.razor b/5-Aquiis.Professional/Shared/Layout/MainLayout.razor new file mode 100644 index 0000000..536ee80 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Layout/MainLayout.razor @@ -0,0 +1,45 @@ +@inherits LayoutComponentBase +@using Aquiis.Professional.Shared.Components +@using Aquiis.Professional.Shared.Services +@using Aquiis.UI.Shared.Components.Layout +@inject ThemeService ThemeService +@implements IDisposable + + + + + + + + About + + + + + + + + @Body + + + + + + + + + + + + +@code { + protected override void OnInitialized() + { + ThemeService.OnThemeChanged += StateHasChanged; + } + + public void Dispose() + { + ThemeService.OnThemeChanged -= StateHasChanged; + } +} diff --git a/Aquiis.SimpleStart/Shared/Layout/MainLayout.razor.css b/5-Aquiis.Professional/Shared/Layout/MainLayout.razor.css similarity index 100% rename from Aquiis.SimpleStart/Shared/Layout/MainLayout.razor.css rename to 5-Aquiis.Professional/Shared/Layout/MainLayout.razor.css diff --git a/Aquiis.Professional/Shared/Layout/NavMenu.razor b/5-Aquiis.Professional/Shared/Layout/NavMenu.razor similarity index 100% rename from Aquiis.Professional/Shared/Layout/NavMenu.razor rename to 5-Aquiis.Professional/Shared/Layout/NavMenu.razor diff --git a/Aquiis.SimpleStart/Shared/Layout/NavMenu.razor.css b/5-Aquiis.Professional/Shared/Layout/NavMenu.razor.css similarity index 100% rename from Aquiis.SimpleStart/Shared/Layout/NavMenu.razor.css rename to 5-Aquiis.Professional/Shared/Layout/NavMenu.razor.css diff --git a/Aquiis.Professional/Shared/Routes.razor b/5-Aquiis.Professional/Shared/Routes.razor similarity index 100% rename from Aquiis.Professional/Shared/Routes.razor rename to 5-Aquiis.Professional/Shared/Routes.razor diff --git a/5-Aquiis.Professional/Shared/Services/DatabaseBackupService.cs b/5-Aquiis.Professional/Shared/Services/DatabaseBackupService.cs new file mode 100644 index 0000000..7fcd96b --- /dev/null +++ b/5-Aquiis.Professional/Shared/Services/DatabaseBackupService.cs @@ -0,0 +1,415 @@ +using Aquiis.Infrastructure.Data; +using Aquiis.Core.Interfaces; +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 IPathService _pathService; + + public DatabaseBackupService( + ILogger logger, + ApplicationDbContext dbContext, + IConfiguration configuration, + IPathService pathService) + { + _logger = logger; + _dbContext = dbContext; + _configuration = configuration; + _pathService = pathService; + } + + /// + /// 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 _pathService.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/5-Aquiis.Professional/Shared/Services/EntityRouteHelper.cs b/5-Aquiis.Professional/Shared/Services/EntityRouteHelper.cs new file mode 100644 index 0000000..7cd55e6 --- /dev/null +++ b/5-Aquiis.Professional/Shared/Services/EntityRouteHelper.cs @@ -0,0 +1,130 @@ +using Aquiis.Core.Entities; + +namespace Aquiis.Professional.Shared.Services; + +/// +/// Provides centralized mapping between entity types and their navigation routes. +/// This ensures consistent URL generation across the application when navigating to entity details. +/// RESTful pattern: /resource/{id} for view, /resource/{id}/edit for edit, /resource for list +/// +public static class EntityRouteHelper +{ + private static readonly Dictionary RouteMap = new() + { + { "Lease", "/propertymanagement/leases" }, + { "Payment", "/propertymanagement/payments" }, + { "Invoice", "/propertymanagement/invoices" }, + { "Maintenance", "/propertymanagement/maintenance" }, + { "Application", "/propertymanagement/applications" }, + { "Property", "/propertymanagement/properties" }, + { "Tenant", "/propertymanagement/tenants" }, + { "Prospect", "/PropertyManagement/ProspectiveTenants" }, + { "Inspection", "/propertymanagement/inspections" }, + { "LeaseOffer", "/propertymanagement/leaseoffers" }, + { "Checklist", "/propertymanagement/checklists" }, + { "Organization", "/administration/organizations" } + }; + + /// + /// Gets the full navigation route for viewing an entity (RESTful: /resource/{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 "/"; + } + + /// + /// Gets the route for an entity action (RESTful: /resource/{id}/action) + /// + /// The type of entity + /// The unique identifier of the entity + /// The action (e.g., "edit", "delete", "approve") + /// The full route path, or "/" if not mapped + public static string GetEntityActionRoute(string? entityType, Guid entityId, string action) + { + if (string.IsNullOrWhiteSpace(entityType)) + { + return "/"; + } + + if (RouteMap.TryGetValue(entityType, out var route)) + { + return $"{route}/{entityId}/{action}"; + } + + return "/"; + } + + /// + /// Gets the list route for an entity type (RESTful: /resource) + /// + /// The type of entity + /// The list route path, or "/" if not mapped + public static string GetListRoute(string? entityType) + { + if (string.IsNullOrWhiteSpace(entityType)) + { + return "/"; + } + + if (RouteMap.TryGetValue(entityType, out var route)) + { + return route; + } + + return "/"; + } + + /// + /// Gets the create route for an entity type (RESTful: /resource/create) + /// + /// The type of entity + /// The create route path, or "/" if not mapped + public static string GetCreateRoute(string? entityType) + { + if (string.IsNullOrWhiteSpace(entityType)) + { + return "/"; + } + + if (RouteMap.TryGetValue(entityType, out var route)) + { + return $"{route}/create"; + } + + 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; + } +} \ No newline at end of file diff --git a/Aquiis.Professional/Shared/Services/SessionTimeoutService.cs b/5-Aquiis.Professional/Shared/Services/SessionTimeoutService.cs similarity index 100% rename from Aquiis.Professional/Shared/Services/SessionTimeoutService.cs rename to 5-Aquiis.Professional/Shared/Services/SessionTimeoutService.cs diff --git a/Aquiis.Professional/Shared/Services/ThemeService.cs b/5-Aquiis.Professional/Shared/Services/ThemeService.cs similarity index 100% rename from Aquiis.Professional/Shared/Services/ThemeService.cs rename to 5-Aquiis.Professional/Shared/Services/ThemeService.cs diff --git a/Aquiis.Professional/Shared/Services/ToastService.cs b/5-Aquiis.Professional/Shared/Services/ToastService.cs similarity index 100% rename from Aquiis.Professional/Shared/Services/ToastService.cs rename to 5-Aquiis.Professional/Shared/Services/ToastService.cs diff --git a/5-Aquiis.Professional/Shared/Services/UserContextService.cs b/5-Aquiis.Professional/Shared/Services/UserContextService.cs new file mode 100644 index 0000000..128095e --- /dev/null +++ b/5-Aquiis.Professional/Shared/Services/UserContextService.cs @@ -0,0 +1,311 @@ +using Aquiis.Professional.Entities; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Aquiis.Core.Entities; +using System.Security.Claims; +using Aquiis.Core.Constants; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Application.Services; + +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 : IUserContextService + { + 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). + /// Returns null if user is not authenticated or has no active organization. + /// Callers should handle null appropriately. + /// + public async Task GetActiveOrganizationIdAsync() + { + // Check if user is authenticated first + if (!await IsAuthenticatedAsync()) + { + return null; // Not authenticated - no organization + } + + await EnsureInitializedAsync(); + + // Return null if no active organization (e.g., fresh database, new user) + if (!_activeOrganizationId.HasValue || _activeOrganizationId == Guid.Empty) + { + return null; + } + + 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/5-Aquiis.Professional/Shared/_Imports.razor b/5-Aquiis.Professional/Shared/_Imports.razor new file mode 100644 index 0000000..d84320c --- /dev/null +++ b/5-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.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.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.UI.Shared.Features.Notifications +@using Aquiis.UI.Shared.Components.Common diff --git a/Aquiis.SimpleStart/appsettings.Development.json b/5-Aquiis.Professional/appsettings.Development.json similarity index 100% rename from Aquiis.SimpleStart/appsettings.Development.json rename to 5-Aquiis.Professional/appsettings.Development.json diff --git a/5-Aquiis.Professional/appsettings.json b/5-Aquiis.Professional/appsettings.json new file mode 100644 index 0000000..0258005 --- /dev/null +++ b/5-Aquiis.Professional/appsettings.json @@ -0,0 +1,49 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "DataSource=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/5-Aquiis.Professional/electron.manifest.json similarity index 100% rename from Aquiis.Professional/electron.manifest.json rename to 5-Aquiis.Professional/electron.manifest.json diff --git a/Aquiis.SimpleStart/libman.json b/5-Aquiis.Professional/libman.json similarity index 100% rename from Aquiis.SimpleStart/libman.json rename to 5-Aquiis.Professional/libman.json diff --git a/Aquiis.SimpleStart/wwwroot/app.css b/5-Aquiis.Professional/wwwroot/app.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/app.css rename to 5-Aquiis.Professional/wwwroot/app.css diff --git a/Aquiis.SimpleStart/wwwroot/assets/database-fill-gear.svg b/5-Aquiis.Professional/wwwroot/assets/database-fill-gear.svg similarity index 100% rename from Aquiis.SimpleStart/wwwroot/assets/database-fill-gear.svg rename to 5-Aquiis.Professional/wwwroot/assets/database-fill-gear.svg diff --git a/Aquiis.SimpleStart/wwwroot/assets/database.svg b/5-Aquiis.Professional/wwwroot/assets/database.svg similarity index 100% rename from Aquiis.SimpleStart/wwwroot/assets/database.svg rename to 5-Aquiis.Professional/wwwroot/assets/database.svg diff --git a/Aquiis.SimpleStart/wwwroot/assets/splash.png b/5-Aquiis.Professional/wwwroot/assets/splash.png similarity index 100% rename from Aquiis.SimpleStart/wwwroot/assets/splash.png rename to 5-Aquiis.Professional/wwwroot/assets/splash.png diff --git a/Aquiis.SimpleStart/wwwroot/assets/splash.svg b/5-Aquiis.Professional/wwwroot/assets/splash.svg similarity index 100% rename from Aquiis.SimpleStart/wwwroot/assets/splash.svg rename to 5-Aquiis.Professional/wwwroot/assets/splash.svg diff --git a/Aquiis.SimpleStart/wwwroot/favicon.png b/5-Aquiis.Professional/wwwroot/favicon.png similarity index 100% rename from Aquiis.SimpleStart/wwwroot/favicon.png rename to 5-Aquiis.Professional/wwwroot/favicon.png diff --git a/Aquiis.SimpleStart/wwwroot/js/fileDownload.js b/5-Aquiis.Professional/wwwroot/js/fileDownload.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/js/fileDownload.js rename to 5-Aquiis.Professional/wwwroot/js/fileDownload.js diff --git a/Aquiis.SimpleStart/wwwroot/js/sessionTimeout.js b/5-Aquiis.Professional/wwwroot/js/sessionTimeout.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/js/sessionTimeout.js rename to 5-Aquiis.Professional/wwwroot/js/sessionTimeout.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-grid.rtl.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-reboot.rtl.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap-utilities.rtl.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/css/bootstrap.rtl.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.esm.min.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.min.js b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.min.js similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.min.js rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.min.js diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.min.js.map b/5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.min.js.map similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/js/bootstrap.min.js.map rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/js/bootstrap.min.js.map diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_accordion.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_accordion.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_accordion.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_accordion.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_alert.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_alert.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_alert.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_alert.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_badge.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_badge.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_badge.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_badge.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_breadcrumb.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_breadcrumb.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_breadcrumb.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_breadcrumb.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_button-group.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_button-group.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_button-group.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_button-group.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_buttons.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_buttons.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_buttons.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_buttons.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_card.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_card.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_card.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_card.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_carousel.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_carousel.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_carousel.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_carousel.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_close.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_close.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_close.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_close.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_containers.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_containers.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_containers.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_containers.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_dropdown.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_dropdown.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_dropdown.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_dropdown.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_forms.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_forms.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_forms.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_forms.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_functions.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_functions.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_functions.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_functions.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_grid.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_grid.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_grid.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_grid.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_helpers.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_helpers.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_helpers.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_helpers.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_images.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_images.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_images.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_images.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_list-group.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_list-group.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_list-group.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_list-group.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_maps.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_maps.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_maps.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_maps.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_mixins.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_mixins.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_mixins.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_mixins.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_modal.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_modal.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_modal.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_modal.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_nav.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_nav.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_nav.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_nav.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_navbar.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_navbar.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_navbar.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_navbar.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_offcanvas.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_offcanvas.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_offcanvas.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_offcanvas.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_pagination.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_pagination.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_pagination.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_pagination.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_placeholders.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_placeholders.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_placeholders.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_placeholders.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_popover.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_popover.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_popover.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_popover.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_progress.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_progress.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_progress.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_progress.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_reboot.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_reboot.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_reboot.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_reboot.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_root.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_root.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_root.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_root.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_spinners.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_spinners.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_spinners.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_spinners.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_tables.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_tables.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_tables.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_tables.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_toasts.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_toasts.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_toasts.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_toasts.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_tooltip.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_tooltip.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_tooltip.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_tooltip.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_transitions.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_transitions.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_transitions.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_transitions.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_type.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_type.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_type.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_type.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_utilities.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_utilities.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_utilities.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_utilities.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_variables-dark.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_variables-dark.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_variables-dark.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_variables-dark.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_variables.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_variables.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/_variables.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/_variables.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-grid.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-grid.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-grid.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-grid.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-reboot.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-reboot.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-reboot.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-reboot.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-utilities.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-utilities.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap-utilities.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap-utilities.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/bootstrap.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/bootstrap.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_floating-labels.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_floating-labels.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_floating-labels.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_floating-labels.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-check.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-check.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-check.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-check.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-control.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-control.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-control.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-control.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-range.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-range.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-range.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-range.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-select.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-select.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-select.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-select.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-text.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-text.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_form-text.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_form-text.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_input-group.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_input-group.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_input-group.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_input-group.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_labels.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_labels.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_labels.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_labels.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_validation.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_validation.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/forms/_validation.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/forms/_validation.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_clearfix.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_clearfix.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_clearfix.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_clearfix.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_color-bg.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_color-bg.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_color-bg.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_color-bg.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_colored-links.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_colored-links.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_colored-links.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_colored-links.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_focus-ring.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_focus-ring.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_focus-ring.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_focus-ring.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_icon-link.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_icon-link.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_icon-link.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_icon-link.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_position.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_position.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_position.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_position.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_ratio.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_ratio.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_ratio.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_ratio.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_stacks.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_stacks.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_stacks.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_stacks.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_stretched-link.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_stretched-link.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_stretched-link.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_stretched-link.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_text-truncation.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_text-truncation.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_text-truncation.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_text-truncation.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_visually-hidden.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_visually-hidden.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_visually-hidden.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_visually-hidden.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_vr.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_vr.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/helpers/_vr.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/helpers/_vr.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_alert.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_alert.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_alert.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_alert.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_backdrop.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_backdrop.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_backdrop.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_backdrop.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_banner.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_banner.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_banner.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_banner.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_border-radius.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_border-radius.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_border-radius.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_border-radius.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_box-shadow.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_box-shadow.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_box-shadow.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_box-shadow.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_breakpoints.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_breakpoints.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_breakpoints.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_breakpoints.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_buttons.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_buttons.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_buttons.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_buttons.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_caret.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_caret.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_caret.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_caret.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_clearfix.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_clearfix.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_clearfix.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_clearfix.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_color-mode.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_color-mode.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_color-mode.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_color-mode.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_color-scheme.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_color-scheme.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_color-scheme.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_color-scheme.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_container.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_container.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_container.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_container.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_deprecate.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_deprecate.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_deprecate.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_deprecate.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_forms.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_forms.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_forms.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_forms.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_gradients.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_gradients.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_gradients.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_gradients.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_grid.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_grid.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_grid.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_grid.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_image.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_image.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_image.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_image.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_list-group.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_list-group.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_list-group.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_list-group.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_lists.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_lists.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_lists.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_lists.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_pagination.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_pagination.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_pagination.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_pagination.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_reset-text.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_reset-text.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_reset-text.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_reset-text.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_resize.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_resize.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_resize.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_resize.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_table-variants.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_table-variants.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_table-variants.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_table-variants.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_text-truncate.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_text-truncate.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_text-truncate.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_text-truncate.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_transition.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_transition.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_transition.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_transition.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_utilities.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_utilities.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_utilities.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_utilities.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_visually-hidden.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_visually-hidden.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/mixins/_visually-hidden.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/mixins/_visually-hidden.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/utilities/_api.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/utilities/_api.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/utilities/_api.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/utilities/_api.scss diff --git a/Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/vendor/_rfs.scss b/5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/vendor/_rfs.scss similarity index 100% rename from Aquiis.SimpleStart/wwwroot/lib/bootstrap/scss/vendor/_rfs.scss rename to 5-Aquiis.Professional/wwwroot/lib/bootstrap/scss/vendor/_rfs.scss diff --git a/6-Tests/Aquiis.Application.Tests/Aquiis.Application.Tests.csproj b/6-Tests/Aquiis.Application.Tests/Aquiis.Application.Tests.csproj new file mode 100644 index 0000000..bb9b362 --- /dev/null +++ b/6-Tests/Aquiis.Application.Tests/Aquiis.Application.Tests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/6-Tests/Aquiis.Application.Tests/Services/BaseServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/BaseServiceTests.cs new file mode 100644 index 0000000..a57a6e0 --- /dev/null +++ b/6-Tests/Aquiis.Application.Tests/Services/BaseServiceTests.cs @@ -0,0 +1,20 @@ +// TODO: Update BaseServiceTests after Clean Architecture refactoring +// BaseService pattern has been replaced with specific service classes +// These tests need to be rewritten to test the new service architecture + +/* +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Entities; +using Aquiis.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; + +namespace Aquiis.Application.Tests +{ + // Tests commented out pending refactoring +} +*/ diff --git a/Aquiis.SimpleStart.Tests/DocumentServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/DocumentServiceTests.cs similarity index 93% rename from Aquiis.SimpleStart.Tests/DocumentServiceTests.cs rename to 6-Tests/Aquiis.Application.Tests/Services/DocumentServiceTests.cs index 4a81a69..aa214d7 100644 --- a/Aquiis.SimpleStart.Tests/DocumentServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/DocumentServiceTests.cs @@ -1,25 +1,17 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Entities; +using Aquiis.Infrastructure.Data; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using Xunit; -using DocumentService = Aquiis.SimpleStart.Application.Services.DocumentService; +using DocumentService = Aquiis.Application.Services.DocumentService; -namespace Aquiis.SimpleStart.Tests +namespace Aquiis.Application.Tests { /// /// Comprehensive unit tests for DocumentService. @@ -28,8 +20,8 @@ namespace Aquiis.SimpleStart.Tests public class DocumentServiceTests : IDisposable { private readonly SqliteConnection _connection; - private readonly ApplicationDbContext _context; - private readonly UserContextService _userContext; + private readonly TestApplicationDbContext _context; + private readonly Mock _mockUserContext; private readonly Mock> _mockLogger; private readonly IOptions _mockSettings; private readonly DocumentService _service; @@ -49,34 +41,21 @@ public DocumentServiceTests() .UseSqlite(_connection) .Options; - _context = new ApplicationDbContext(options); + _context = new TestApplicationDbContext(options); _context.Database.EnsureCreated(); - // Mock AuthenticationStateProvider with claims - var claims = new ClaimsPrincipal(new ClaimsIdentity( - new[] { new Claim(ClaimTypes.NameIdentifier, _testUserId) }, - "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - // Mock UserManager - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, null, null, null, null, null, null, null, null); - - var appUser = new ApplicationUser - { - Id = _testUserId, - UserName = "testuser", - Email = "testuser@example.com", - ActiveOrganizationId = _testOrgId - }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())) - .ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - _userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); + // Mock IUserContextService + _mockUserContext = new Mock(); + _mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(_testUserId); + _mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); + _mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + _mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("testuser@example.com"); + _mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); // Create test user var user = new ApplicationUser @@ -159,7 +138,7 @@ public DocumentServiceTests() _service = new DocumentService( _context, _mockLogger.Object, - _userContext, + _mockUserContext.Object, _mockSettings); } diff --git a/Aquiis.SimpleStart.Tests/InvoiceServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/InvoiceServiceTests.cs similarity index 94% rename from Aquiis.SimpleStart.Tests/InvoiceServiceTests.cs rename to 6-Tests/Aquiis.Application.Tests/Services/InvoiceServiceTests.cs index 3b44833..7687ae4 100644 --- a/Aquiis.SimpleStart.Tests/InvoiceServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/InvoiceServiceTests.cs @@ -1,25 +1,17 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Entities; +using Aquiis.Infrastructure.Data; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using Xunit; -using InvoiceService = Aquiis.SimpleStart.Application.Services.InvoiceService; +using InvoiceService = Aquiis.Application.Services.InvoiceService; -namespace Aquiis.SimpleStart.Tests +namespace Aquiis.Application.Tests { /// /// Comprehensive unit tests for InvoiceService. @@ -28,8 +20,8 @@ namespace Aquiis.SimpleStart.Tests public class InvoiceServiceTests : IDisposable { private readonly SqliteConnection _connection; - private readonly ApplicationDbContext _context; - private readonly UserContextService _userContext; + private readonly TestApplicationDbContext _context; + private readonly Mock _mockUserContext; private readonly Mock> _mockLogger; private readonly IOptions _mockSettings; private readonly InvoiceService _service; @@ -49,34 +41,21 @@ public InvoiceServiceTests() .UseSqlite(_connection) .Options; - _context = new ApplicationDbContext(options); + _context = new TestApplicationDbContext(options); _context.Database.EnsureCreated(); - // Mock AuthenticationStateProvider with claims - var claims = new ClaimsPrincipal(new ClaimsIdentity( - new[] { new Claim(ClaimTypes.NameIdentifier, _testUserId) }, - "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - // Mock UserManager - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, null, null, null, null, null, null, null, null); - - var appUser = new ApplicationUser - { - Id = _testUserId, - UserName = "testuser", - Email = "testuser@example.com", - ActiveOrganizationId = _testOrgId - }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())) - .ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - _userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); + // Mock IUserContextService + _mockUserContext = new Mock(); + _mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(_testUserId); + _mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); + _mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + _mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("testuser@example.com"); + _mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); // Create test user var user = new ApplicationUser @@ -159,7 +138,7 @@ public InvoiceServiceTests() _service = new InvoiceService( _context, _mockLogger.Object, - _userContext, + _mockUserContext.Object, _mockSettings); } diff --git a/Aquiis.SimpleStart.Tests/LeaseServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/LeaseServiceTests.cs similarity index 94% rename from Aquiis.SimpleStart.Tests/LeaseServiceTests.cs rename to 6-Tests/Aquiis.Application.Tests/Services/LeaseServiceTests.cs index 09cefb2..1fd1e52 100644 --- a/Aquiis.SimpleStart.Tests/LeaseServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/LeaseServiceTests.cs @@ -1,25 +1,17 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Application.Services; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; +using Aquiis.Application.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Entities; +using Aquiis.Infrastructure.Data; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using Xunit; -namespace Aquiis.SimpleStart.Tests +namespace Aquiis.Application.Tests { /// /// Comprehensive unit tests for LeaseService. @@ -28,8 +20,8 @@ namespace Aquiis.SimpleStart.Tests public class LeaseServiceTests : IDisposable { private readonly SqliteConnection _connection; - private readonly ApplicationDbContext _context; - private readonly UserContextService _userContext; + private readonly TestApplicationDbContext _context; + private readonly Mock _mockUserContext; private readonly Mock> _mockLogger; private readonly IOptions _mockSettings; private readonly LeaseService _service; @@ -48,34 +40,21 @@ public LeaseServiceTests() .UseSqlite(_connection) .Options; - _context = new ApplicationDbContext(options); + _context = new TestApplicationDbContext(options); _context.Database.EnsureCreated(); - // Mock AuthenticationStateProvider with claims - var claims = new ClaimsPrincipal(new ClaimsIdentity( - new[] { new Claim(ClaimTypes.NameIdentifier, _testUserId) }, - "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - // Mock UserManager - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, null, null, null, null, null, null, null, null); - - var appUser = new ApplicationUser - { - Id = _testUserId, - UserName = "testuser", - Email = "testuser@example.com", - ActiveOrganizationId = _testOrgId - }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())) - .ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - _userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); + // Mock IUserContextService + _mockUserContext = new Mock(); + _mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(_testUserId); + _mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); + _mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + _mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("testuser@example.com"); + _mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); // Create test user var user = new ApplicationUser @@ -141,7 +120,7 @@ public LeaseServiceTests() _service = new LeaseService( _context, _mockLogger.Object, - _userContext, + _mockUserContext.Object, _mockSettings); } diff --git a/Aquiis.SimpleStart.Tests/LeaseWorkflowService.Tests.cs b/6-Tests/Aquiis.Application.Tests/Services/LeaseWorkflowService.Tests.cs similarity index 96% rename from Aquiis.SimpleStart.Tests/LeaseWorkflowService.Tests.cs rename to 6-Tests/Aquiis.Application.Tests/Services/LeaseWorkflowService.Tests.cs index a8f9181..13b4443 100644 --- a/Aquiis.SimpleStart.Tests/LeaseWorkflowService.Tests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/LeaseWorkflowService.Tests.cs @@ -1,23 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Application.Services.Workflows; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Core.Constants; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; +using Aquiis.Core.Entities; +using Aquiis.Core.Constants; +using Aquiis.Core.Interfaces.Services; using Microsoft.EntityFrameworkCore; using Moq; -using Xunit; // Alias for the workflow enum to avoid ambiguity with Core.Constants.LeaseStatus -using WorkflowLeaseStatus = Aquiis.SimpleStart.Application.Services.Workflows.LeaseStatus; +using WorkflowLeaseStatus = Aquiis.Application.Services.Workflows.LeaseStatus; +using Aquiis.Infrastructure.Data; +using Aquiis.SimpleStart.Entities; +using Aquiis.Application.Services.Workflows; -namespace Aquiis.SimpleStart.Tests; +namespace Aquiis.Application.Tests; /// /// Comprehensive tests for LeaseWorkflowService covering: @@ -38,30 +31,27 @@ private static async Task CreateTestContextAsync() { var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); connection.Open(); - var options = new DbContextOptionsBuilder() + var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; var testUserId = "test-user-id"; var orgId = Guid.NewGuid(); - var claims = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, testUserId) }, "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, - null!, null!, null!, null!, null!, null!, null!, null!); - - var appUser = new ApplicationUser { Id = testUserId, ActiveOrganizationId = orgId }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())).ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - var userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); - - var context = new SimpleStart.Infrastructure.Data.ApplicationDbContext(options); + // Mock IUserContextService + var mockUserContext = new Mock(); + mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(testUserId); + mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(orgId); + mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("t@t.com"); + mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(orgId); + + TestApplicationDbContext context = new TestApplicationDbContext(options); await context.Database.EnsureCreatedAsync(); var appUserEntity = new ApplicationUser { Id = testUserId, UserName = "testuser", Email = "t@t.com", ActiveOrganizationId = orgId }; @@ -71,8 +61,8 @@ private static async Task CreateTestContextAsync() context.Organizations.Add(org); await context.SaveChangesAsync(); - var noteService = new Application.Services.NoteService(context, userContext); - var workflowService = new LeaseWorkflowService(context, userContext, noteService); + var noteService = new Application.Services.NoteService(context, mockUserContext.Object); + var workflowService = new LeaseWorkflowService(context, mockUserContext.Object, noteService); return new TestContext { @@ -176,7 +166,7 @@ private static async Task CreateSecurityDepositAsync( private class TestContext : IAsyncDisposable { public required Microsoft.Data.Sqlite.SqliteConnection Connection { get; init; } - public required Aquiis.SimpleStart.Infrastructure.Data.ApplicationDbContext Context { get; init; } + public required ApplicationDbContext Context { get; init; } public required LeaseWorkflowService WorkflowService { get; init; } public required string UserId { get; init; } public required Guid OrgId { get; init; } diff --git a/Aquiis.SimpleStart.Tests/MaintenanceServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/MaintenanceServiceTests.cs similarity index 96% rename from Aquiis.SimpleStart.Tests/MaintenanceServiceTests.cs rename to 6-Tests/Aquiis.Application.Tests/Services/MaintenanceServiceTests.cs index 150722b..6490fc0 100644 --- a/Aquiis.SimpleStart.Tests/MaintenanceServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/MaintenanceServiceTests.cs @@ -1,37 +1,28 @@ -using Aquiis.SimpleStart.Application.Services; -using Aquiis.SimpleStart.Application.Services.Workflows; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Components.Authorization; +using Aquiis.Application.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Entities; +using Aquiis.Infrastructure.Data; using Microsoft.AspNetCore.Identity; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Xunit; -namespace Aquiis.SimpleStart.Tests +namespace Aquiis.Application.Tests { public class MaintenanceServiceTests : IDisposable { - private readonly ApplicationDbContext _context; + private readonly TestApplicationDbContext _context; private readonly MaintenanceService _service; private readonly Mock> _mockUserManager; private readonly Mock> _mockLogger; private readonly Mock _mockCalendarEventService; - private readonly UserContextService _userContext; + private readonly Mock _mockUserContext; private readonly IOptions _mockSettings; private readonly SqliteConnection _connection; private readonly ApplicationUser _testUser; @@ -62,7 +53,7 @@ public MaintenanceServiceTests() .UseSqlite(_connection) .Options; - _context = new ApplicationDbContext(options); + _context = new TestApplicationDbContext(options); _context.Database.EnsureCreated(); // Setup test data @@ -144,22 +135,18 @@ public MaintenanceServiceTests() _mockUserManager.Setup(x => x.FindByIdAsync(It.IsAny())) .ReturnsAsync(_testUser); - var mockAuthStateProvider = new Mock(); - var claims = new List - { - new Claim(ClaimTypes.NameIdentifier, _testUser.Id), - new Claim("OrganizationId", _testOrg.Id.ToString()) - }; - var identity = new ClaimsIdentity(claims, "TestAuth"); - var claimsPrincipal = new ClaimsPrincipal(identity); - mockAuthStateProvider.Setup(x => x.GetAuthenticationStateAsync()) - .ReturnsAsync(new Microsoft.AspNetCore.Components.Authorization.AuthenticationState(claimsPrincipal)); - - var serviceProvider = new Mock(); - _userContext = new UserContextService( - mockAuthStateProvider.Object, - _mockUserManager.Object, - serviceProvider.Object); + // Mock IUserContextService + _mockUserContext = new Mock(); + _mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(_testUserId); + _mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); + _mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser@test.com"); + _mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("testuser@test.com"); + _mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); _mockLogger = new Mock>(); @@ -173,7 +160,7 @@ public MaintenanceServiceTests() _service = new MaintenanceService( _context, _mockLogger.Object, - _userContext, + _mockUserContext.Object, _mockSettings, _mockCalendarEventService.Object); } diff --git a/Aquiis.SimpleStart.Tests/Infrastructure/Services/NotificationServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/NotificationServiceTests.cs similarity index 92% rename from Aquiis.SimpleStart.Tests/Infrastructure/Services/NotificationServiceTests.cs rename to 6-Tests/Aquiis.Application.Tests/Services/NotificationServiceTests.cs index 248329f..65cd6ea 100644 --- a/Aquiis.SimpleStart.Tests/Infrastructure/Services/NotificationServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/NotificationServiceTests.cs @@ -1,17 +1,9 @@ - -using System; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Application.Services; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; +using Aquiis.Application.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Entities; +using Aquiis.Infrastructure.Data; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; @@ -19,16 +11,16 @@ using Moq; using Xunit; -namespace Aquiis.SimpleStart.Tests.Infrastructure.Services +namespace Aquiis.Application.Tests { public class NotificationServiceTests : IDisposable { private readonly SqliteConnection _connection; - private readonly ApplicationDbContext _context; + private readonly TestApplicationDbContext _context; private readonly NotificationService _service; private readonly Mock _mockEmailService; private readonly Mock _mockSMSService; - private readonly UserContextService _userContext; + private readonly Mock _mockUserContext; private readonly Guid _testOrgId; private readonly string _testUserId; @@ -42,7 +34,7 @@ public NotificationServiceTests() .UseSqlite(_connection) .Options; - _context = new ApplicationDbContext(options); + _context = new TestApplicationDbContext(options); _context.Database.EnsureCreated(); // Setup test organization and user @@ -71,24 +63,18 @@ public NotificationServiceTests() _context.Organizations.Add(testOrg); _context.SaveChanges(); - // Setup UserContextService with mocks - var claims = new ClaimsPrincipal(new ClaimsIdentity(new[] - { - new Claim(ClaimTypes.NameIdentifier, _testUserId) - }, "TestAuth")); - - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, null, null, null, null, null, null, null, null); - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())) - .ReturnsAsync(testUser); - - var mockServiceProvider = new Mock(); - _userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, mockServiceProvider.Object); + // Mock IUserContextService + _mockUserContext = new Mock(); + _mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(_testUserId); + _mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); + _mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + _mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("test@example.com"); + _mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); // Setup mock email and SMS services _mockEmailService = new Mock(); @@ -104,7 +90,7 @@ public NotificationServiceTests() // Create NotificationService _service = new NotificationService( _context, - _userContext, + _mockUserContext.Object, _mockEmailService.Object, _mockSMSService.Object, settings, diff --git a/Aquiis.SimpleStart.Tests/PaymentServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/PaymentServiceTests.cs similarity index 94% rename from Aquiis.SimpleStart.Tests/PaymentServiceTests.cs rename to 6-Tests/Aquiis.Application.Tests/Services/PaymentServiceTests.cs index 57ed152..f198d64 100644 --- a/Aquiis.SimpleStart.Tests/PaymentServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/PaymentServiceTests.cs @@ -1,25 +1,17 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Entities; +using Aquiis.Infrastructure.Data; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using Xunit; -using PaymentService = Aquiis.SimpleStart.Application.Services.PaymentService; +using PaymentService = Aquiis.Application.Services.PaymentService; -namespace Aquiis.SimpleStart.Tests +namespace Aquiis.Application.Tests { /// /// Comprehensive unit tests for PaymentService. @@ -28,8 +20,8 @@ namespace Aquiis.SimpleStart.Tests public class PaymentServiceTests : IDisposable { private readonly SqliteConnection _connection; - private readonly ApplicationDbContext _context; - private readonly UserContextService _userContext; + private readonly TestApplicationDbContext _context; + private readonly Mock _mockUserContext; private readonly Mock> _mockLogger; private readonly IOptions _mockSettings; private readonly PaymentService _service; @@ -50,34 +42,21 @@ public PaymentServiceTests() .UseSqlite(_connection) .Options; - _context = new ApplicationDbContext(options); + _context = new TestApplicationDbContext(options); _context.Database.EnsureCreated(); - // Mock AuthenticationStateProvider with claims - var claims = new ClaimsPrincipal(new ClaimsIdentity( - new[] { new Claim(ClaimTypes.NameIdentifier, _testUserId) }, - "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - // Mock UserManager - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, null, null, null, null, null, null, null, null); - - var appUser = new ApplicationUser - { - Id = _testUserId, - UserName = "testuser", - Email = "testuser@example.com", - ActiveOrganizationId = _testOrgId - }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())) - .ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - _userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); + // Mock IUserContextService + _mockUserContext = new Mock(); + _mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(_testUserId); + _mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); + _mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + _mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("testuser@example.com"); + _mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); // Create test user var user = new ApplicationUser @@ -178,7 +157,7 @@ public PaymentServiceTests() _service = new PaymentService( _context, _mockLogger.Object, - _userContext, + _mockUserContext.Object, _mockSettings); } diff --git a/Aquiis.SimpleStart.Tests/PropertyServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/PropertyServiceTests.cs similarity index 89% rename from Aquiis.SimpleStart.Tests/PropertyServiceTests.cs rename to 6-Tests/Aquiis.Application.Tests/Services/PropertyServiceTests.cs index 80a9ac9..4b8701f 100644 --- a/Aquiis.SimpleStart.Tests/PropertyServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/PropertyServiceTests.cs @@ -1,31 +1,24 @@ -using System; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Application.Services; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; +using Aquiis.Application.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Entities; +using Aquiis.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using Xunit; -namespace Aquiis.SimpleStart.Tests +namespace Aquiis.Application.Tests { /// /// Unit tests for PropertyService business logic and property-specific operations. /// public class PropertyServiceTests : IDisposable { - private readonly ApplicationDbContext _context; - private readonly UserContextService _userContext; + private readonly TestApplicationDbContext _context; + private readonly Mock _mockUserContext; private readonly PropertyService _service; private readonly string _testUserId; private readonly Guid _testOrgId; @@ -41,38 +34,25 @@ public PropertyServiceTests() .UseSqlite(_connection) .Options; - _context = new ApplicationDbContext(options); + _context = new TestApplicationDbContext(options); _context.Database.EnsureCreated(); // Setup test user and organization _testUserId = "test-user-123"; _testOrgId = Guid.NewGuid(); - // Mock AuthenticationStateProvider - var claims = new ClaimsPrincipal(new ClaimsIdentity( - new[] { new Claim(ClaimTypes.NameIdentifier, _testUserId) }, - "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - // Mock UserManager - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, null, null, null, null, null, null, null, null); - - var appUser = new ApplicationUser - { - Id = _testUserId, - UserName = "testuser", - Email = "test@example.com", - ActiveOrganizationId = _testOrgId - }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())) - .ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - _userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); + // Mock IUserContextService + _mockUserContext = new Mock(); + _mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(_testUserId); + _mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); + _mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + _mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("test@example.com"); + _mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); // Seed test data var user = new ApplicationUser @@ -101,13 +81,28 @@ public PropertyServiceTests() SoftDeleteEnabled = true }); + // Mock loggers var mockLogger = new Mock>(); - // Create real CalendarEventService for testing - var mockCalendarSettings = new Mock(_context, _userContext); - var calendarService = new CalendarEventService(_context, mockCalendarSettings.Object, _userContext); + // Create real CalendarSettingsService and CalendarEventService for testing + var calendarSettingsService = new CalendarSettingsService(_context, _mockUserContext.Object); + var calendarService = new CalendarEventService(_context, calendarSettingsService, _mockUserContext.Object); + + // Create real NotificationService with mocked email/SMS dependencies + var mockNotificationLogger = new Mock>(); + var mockEmailService = new Mock(); + var mockSMSService = new Mock(); - _service = new PropertyService(_context, mockLogger.Object, _userContext, mockSettings, calendarService); + var notificationService = new NotificationService( + _context, + _mockUserContext.Object, + mockEmailService.Object, + mockSMSService.Object, + mockSettings, + mockNotificationLogger.Object + ); + + _service = new PropertyService(_context, mockLogger.Object, _mockUserContext.Object, mockSettings, calendarService, notificationService); } public void Dispose() @@ -138,6 +133,17 @@ public async Task CreateAsync_SetsNextRoutineInspectionDate() // Assert Assert.NotNull(result.NextRoutineInspectionDueDate); Assert.Equal(expectedDate, result.NextRoutineInspectionDueDate!.Value.Date); + + // Verify notification was created + var notification = await _context.Notifications + .FirstOrDefaultAsync(n => n.RelatedEntityId == result.Id); + + Assert.NotNull(notification); + Assert.Equal("Routine Inspection Scheduled", notification.Title); + Assert.Contains(result.Address, notification.Message); + Assert.Equal(_testUserId, notification.CreatedBy); + Assert.Equal(_testOrgId, notification.OrganizationId); + Assert.False(notification.IsRead); } #endregion diff --git a/Aquiis.SimpleStart.Tests/TenantServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Services/TenantServiceTests.cs similarity index 92% rename from Aquiis.SimpleStart.Tests/TenantServiceTests.cs rename to 6-Tests/Aquiis.Application.Tests/Services/TenantServiceTests.cs index 1c9293b..37ae66b 100644 --- a/Aquiis.SimpleStart.Tests/TenantServiceTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Services/TenantServiceTests.cs @@ -1,26 +1,17 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Application.Services; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; +using Aquiis.Application.Services; +using Aquiis.Core.Constants; +using Aquiis.Core.Entities; +using Aquiis.Core.Interfaces.Services; +using Aquiis.SimpleStart.Entities; +using Aquiis.Infrastructure.Data; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using Xunit; -namespace Aquiis.SimpleStart.Tests +namespace Aquiis.Application.Tests { /// /// Comprehensive unit tests for TenantService. @@ -29,8 +20,8 @@ namespace Aquiis.SimpleStart.Tests public class TenantServiceTests : IDisposable { private readonly SqliteConnection _connection; - private readonly ApplicationDbContext _context; - private readonly UserContextService _userContext; + private readonly TestApplicationDbContext _context; + private readonly Mock _mockUserContext; private readonly Mock> _mockLogger; private readonly IOptions _mockSettings; private readonly TenantService _service; @@ -47,34 +38,21 @@ public TenantServiceTests() .UseSqlite(_connection) .Options; - _context = new ApplicationDbContext(options); + _context = new TestApplicationDbContext(options); _context.Database.EnsureCreated(); - // Mock AuthenticationStateProvider with claims - var claims = new ClaimsPrincipal(new ClaimsIdentity( - new[] { new Claim(ClaimTypes.NameIdentifier, _testUserId) }, - "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - // Mock UserManager - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, null, null, null, null, null, null, null, null); - - var appUser = new ApplicationUser - { - Id = _testUserId, - UserName = "testuser", - Email = "testuser@example.com", - ActiveOrganizationId = _testOrgId - }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())) - .ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - _userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); + // Mock IUserContextService + _mockUserContext = new Mock(); + _mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(_testUserId); + _mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); + _mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + _mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("testuser@example.com"); + _mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(_testOrgId); // Create test user (required for Organization.OwnerId foreign key) var user = new ApplicationUser @@ -110,7 +88,7 @@ public TenantServiceTests() _service = new TenantService( _context, _mockLogger.Object, - _userContext, + _mockUserContext.Object, _mockSettings); } diff --git a/6-Tests/Aquiis.Application.Tests/TestApplicationDbContext.cs b/6-Tests/Aquiis.Application.Tests/TestApplicationDbContext.cs new file mode 100644 index 0000000..404f53c --- /dev/null +++ b/6-Tests/Aquiis.Application.Tests/TestApplicationDbContext.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Aquiis.Infrastructure.Data; + +namespace Aquiis.Application.Tests; + +/// +/// Minimal ApplicationUser entity for testing purposes only. +/// This is a simplified version used only in unit tests. +/// +public class ApplicationUser : IdentityUser +{ + public Guid ActiveOrganizationId { get; set; } = Guid.Empty; +} + +/// +/// Test-specific DbContext that extends ApplicationDbContext with Identity support. +/// This allows tests to work with ApplicationUser entities in an in-memory database. +/// +public class TestApplicationDbContext : ApplicationDbContext +{ + public TestApplicationDbContext(DbContextOptions options) + : base((DbContextOptions)options) + { + } + + // Expose ApplicationUser DbSet for test scenarios + public DbSet Users { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure ApplicationUser entity for tests + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Id).ValueGeneratedOnAdd(); + entity.HasIndex(e => e.UserName).IsUnique(); + entity.HasIndex(e => e.Email); + }); + } +} diff --git a/Aquiis.SimpleStart.Tests/ApplicationWorkflowService.EdgeCaseTests.cs b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.EdgeCaseTests.cs similarity index 95% rename from Aquiis.SimpleStart.Tests/ApplicationWorkflowService.EdgeCaseTests.cs rename to 6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.EdgeCaseTests.cs index b8755ff..305e211 100644 --- a/Aquiis.SimpleStart.Tests/ApplicationWorkflowService.EdgeCaseTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.EdgeCaseTests.cs @@ -1,21 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Application.Services.Workflows; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Core.Constants; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; +using Aquiis.Application.Services.Workflows; +using Aquiis.Core.Entities; +using Aquiis.Core.Constants; +using Aquiis.Core.Interfaces.Services; +using Aquiis.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Moq; -using Xunit; - -namespace Aquiis.SimpleStart.Tests; +using Aquiis.SimpleStart.Entities; +namespace Aquiis.Application.Tests; /// /// Edge case tests for ApplicationWorkflowService covering: /// - Denial flow and property rollback @@ -33,30 +25,27 @@ private static async Task CreateTestContextAsync() { var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); connection.Open(); - var options = new DbContextOptionsBuilder() + var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; var testUserId = "test-user-id"; var orgId = Guid.NewGuid(); - var claims = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, testUserId) }, "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, - null!, null!, null!, null!, null!, null!, null!, null!); - - var appUser = new ApplicationUser { Id = testUserId, ActiveOrganizationId = orgId }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())).ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - var userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); - - var context = new SimpleStart.Infrastructure.Data.ApplicationDbContext(options); + // Mock IUserContextService + var mockUserContext = new Mock(); + mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(testUserId); + mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(orgId); + mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("t@t.com"); + mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(orgId); + + TestApplicationDbContext context = new TestApplicationDbContext(options); await context.Database.EnsureCreatedAsync(); var appUserEntity = new ApplicationUser { Id = testUserId, UserName = "testuser", Email = "t@t.com", ActiveOrganizationId = orgId }; @@ -66,8 +55,8 @@ private static async Task CreateTestContextAsync() context.Organizations.Add(org); await context.SaveChangesAsync(); - var noteService = new Application.Services.NoteService(context, userContext); - var workflowService = new ApplicationWorkflowService(context, userContext, noteService); + var noteService = new Application.Services.NoteService(context, mockUserContext.Object); + var workflowService = new ApplicationWorkflowService(context, mockUserContext.Object, noteService); return new TestContext { @@ -146,7 +135,7 @@ private static async Task CreateTestContextAsync() private class TestContext : IAsyncDisposable { public required Microsoft.Data.Sqlite.SqliteConnection Connection { get; init; } - public required Aquiis.SimpleStart.Infrastructure.Data.ApplicationDbContext Context { get; init; } + public required ApplicationDbContext Context { get; init; } public required ApplicationWorkflowService WorkflowService { get; init; } public required string UserId { get; init; } public required Guid OrgId { get; init; } diff --git a/Aquiis.SimpleStart.Tests/ApplicationWorkflowService.LeaseLifecycleTests.cs b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.LeaseLifecycleTests.cs similarity index 76% rename from Aquiis.SimpleStart.Tests/ApplicationWorkflowService.LeaseLifecycleTests.cs rename to 6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.LeaseLifecycleTests.cs index 6d569f0..1408b64 100644 --- a/Aquiis.SimpleStart.Tests/ApplicationWorkflowService.LeaseLifecycleTests.cs +++ b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowService.LeaseLifecycleTests.cs @@ -1,19 +1,13 @@ -using System.Security.Claims; -using System.Threading.Tasks; -using System; -using Aquiis.SimpleStart.Application.Services.Workflows; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Core.Constants; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; +using Aquiis.Core.Entities; +using Aquiis.Core.Constants; +using Aquiis.Core.Interfaces.Services; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Xunit; +using Aquiis.Infrastructure.Data; +using Aquiis.SimpleStart.Entities; +using Aquiis.Application.Services.Workflows; -namespace Aquiis.SimpleStart.Tests; +namespace Aquiis.Application.Tests; public class ApplicationWorkflowServiceLeaseLifecycleTests { @@ -23,33 +17,28 @@ public async Task GenerateAndAcceptLeaseOffer_CreatesLeaseAndTenant_UpdatesPrope // Arrange - setup SQLite in-memory var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); connection.Open(); - var options = new DbContextOptionsBuilder() + var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; var testUserId = "test-user-id"; var orgId = Guid.NewGuid(); - // Mock AuthenticationStateProvider - var claims = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, testUserId) }, "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, - null, null, null, null, null, null, null, null); - - var appUser = new ApplicationUser { Id = testUserId, ActiveOrganizationId = orgId }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())).ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - - var userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); + // Mock IUserContextService + var mockUserContext = new Mock(); + mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(testUserId); + mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(orgId); + mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("t@t.com"); + mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(orgId); // Create DbContext and seed data - await using var context = new SimpleStart.Infrastructure.Data.ApplicationDbContext(options); + await using TestApplicationDbContext context = new TestApplicationDbContext(options); await context.Database.EnsureCreatedAsync(); var appUserEntity = new ApplicationUser { Id = testUserId, UserName = "testuser", Email = "t@t.com", ActiveOrganizationId = orgId }; @@ -64,8 +53,8 @@ public async Task GenerateAndAcceptLeaseOffer_CreatesLeaseAndTenant_UpdatesPrope context.Properties.Add(property); await context.SaveChangesAsync(); - var noteService = new Application.Services.NoteService(context, userContext); - var workflowService = new ApplicationWorkflowService(context, userContext, noteService); + var noteService = new Application.Services.NoteService(context, mockUserContext.Object); + var workflowService = new ApplicationWorkflowService(context, mockUserContext.Object, noteService); // Submit application var submissionModel = new ApplicationSubmissionModel diff --git a/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowServiceTests.cs b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowServiceTests.cs new file mode 100644 index 0000000..ea543a3 --- /dev/null +++ b/6-Tests/Aquiis.Application.Tests/Workflows/ApplicationWorkflowServiceTests.cs @@ -0,0 +1,108 @@ +using Aquiis.Core.Entities; +using Aquiis.Core.Constants; +using Aquiis.Core.Interfaces.Services; +using Microsoft.EntityFrameworkCore; +using Moq; +using Aquiis.Infrastructure.Data; +using Aquiis.SimpleStart.Entities; +using Aquiis.Application.Services.Workflows; + +namespace Aquiis.Application.Tests; + +public class ApplicationWorkflowServiceTests +{ + [Fact] + public async Task GetApplicationWorkflowStateAsync_ReturnsExpectedState() + { + // Arrange + // Use SQLite in-memory to support transactions used by workflow base class + var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); + connection.Open(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + // Create test user and org + var testUserId = "test-user-id"; + var orgId = Guid.NewGuid(); + + // Mock IUserContextService + var mockUserContext = new Mock(); + mockUserContext.Setup(x => x.GetUserIdAsync()) + .ReturnsAsync(testUserId); + mockUserContext.Setup(x => x.GetActiveOrganizationIdAsync()) + .ReturnsAsync(orgId); + mockUserContext.Setup(x => x.GetUserNameAsync()) + .ReturnsAsync("testuser"); + mockUserContext.Setup(x => x.GetUserEmailAsync()) + .ReturnsAsync("t@t.com"); + mockUserContext.Setup(x => x.GetOrganizationIdAsync()) + .ReturnsAsync(orgId); + + // Create DbContext and seed prospect/property + await using TestApplicationDbContext context = new TestApplicationDbContext(options); + // Ensure schema is created for SQLite in-memory + await context.Database.EnsureCreatedAsync(); + var appUserEntity = new ApplicationUser { Id = testUserId, UserName = "testuser", Email = "t@t.com", ActiveOrganizationId = orgId }; + context.Users.Add(appUserEntity); + + var org = new Organization { Id = orgId, Name = "TestOrg", OwnerId = testUserId, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; + context.Organizations.Add(org); + + var prospect = new ProspectiveTenant { Id = Guid.NewGuid(), OrganizationId = orgId, FirstName = "Test", LastName = "User", Email = "t@t.com", Phone = "123", Status = "Lead", CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; + var property = new Property { Id = Guid.NewGuid(), OrganizationId = orgId, Address = "123 Main", City = "X", State = "ST", ZipCode = "00000", Status = ApplicationConstants.PropertyStatuses.Available, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; + context.ProspectiveTenants.Add(prospect); + context.Properties.Add(property); + await context.SaveChangesAsync(); + + // Create NoteService (not used heavily in this test) + var noteService = new Application.Services.NoteService(context, mockUserContext.Object); + + var workflowService = new ApplicationWorkflowService(context, mockUserContext.Object, noteService); + + // Act - submit application then initiate screening + var submissionModel = new ApplicationSubmissionModel + { + ApplicationFee = 25m, + ApplicationFeePaid = true, + ApplicationFeePaymentMethod = "Card", + CurrentAddress = "Addr", + CurrentCity = "C", + CurrentState = "ST", + CurrentZipCode = "00000", + CurrentRent = 1000m, + LandlordName = "L", + LandlordPhone = "P", + EmployerName = "E", + JobTitle = "J", + MonthlyIncome = 2000m, + EmploymentLengthMonths = 12, + Reference1Name = "R1", + Reference1Phone = "111", + Reference1Relationship = "Friend" + }; + + var submitResult = await workflowService.SubmitApplicationAsync(prospect.Id, property.Id, submissionModel); + Assert.True(submitResult.Success, string.Join(";", submitResult.Errors)); + + var application = submitResult.Data!; + + var screeningResult = await workflowService.InitiateScreeningAsync(application.Id, true, true); + Assert.True(screeningResult.Success, string.Join(";", screeningResult.Errors)); + + // Get aggregated workflow state + var state = await workflowService.GetApplicationWorkflowStateAsync(application.Id); + + // Assert + Assert.NotNull(state.Application); + Assert.NotEqual(Guid.Empty, state.Application.Id); + Assert.NotNull(state.Prospect); + Assert.NotEqual(Guid.Empty, state.Prospect.Id); + Assert.NotNull(state.Property); + Assert.NotEqual(Guid.Empty, state.Property.Id); + Assert.NotNull(state.Screening); + Assert.NotEqual(Guid.Empty, state.Screening.Id); + Assert.NotEmpty(state.AuditHistory); + Assert.All(state.AuditHistory, item => Assert.NotEqual(Guid.Empty, item.Id)); + + } +} diff --git a/6-Tests/Aquiis.Core.Tests/Aquiis.Core.Tests.csproj b/6-Tests/Aquiis.Core.Tests/Aquiis.Core.Tests.csproj new file mode 100644 index 0000000..95bdbc3 --- /dev/null +++ b/6-Tests/Aquiis.Core.Tests/Aquiis.Core.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + diff --git a/6-Tests/Aquiis.Core.Tests/UnitTest1.cs b/6-Tests/Aquiis.Core.Tests/UnitTest1.cs new file mode 100644 index 0000000..b237e97 --- /dev/null +++ b/6-Tests/Aquiis.Core.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Aquiis.Core.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/Aquiis.SimpleStart.Tests/Core/Validation/GuidValidationAttributeTests.cs b/6-Tests/Aquiis.Core.Tests/Validation/GuidValidationAttributeTests.cs similarity index 97% rename from Aquiis.SimpleStart.Tests/Core/Validation/GuidValidationAttributeTests.cs rename to 6-Tests/Aquiis.Core.Tests/Validation/GuidValidationAttributeTests.cs index 0086347..6f957f0 100644 --- a/Aquiis.SimpleStart.Tests/Core/Validation/GuidValidationAttributeTests.cs +++ b/6-Tests/Aquiis.Core.Tests/Validation/GuidValidationAttributeTests.cs @@ -1,9 +1,7 @@ -using Aquiis.SimpleStart.Core.Validation; -using System; +using Aquiis.Core.Validation; using System.ComponentModel.DataAnnotations; -using Xunit; -namespace Aquiis.SimpleStart.Tests.Core.Validation; +namespace Aquiis.Core.Tests.Validation; public class RequiredGuidAttributeTests { @@ -17,7 +15,7 @@ public void RequiredGuid_GuidEmpty_ReturnsFalse() // Act var result = attribute.IsValid(value); - // Assert + // Assert Assert.False(result); } diff --git a/6-Tests/Aquiis.Infrastructure.Tests/Aquiis.Infrastructure.Tests.csproj b/6-Tests/Aquiis.Infrastructure.Tests/Aquiis.Infrastructure.Tests.csproj new file mode 100644 index 0000000..20f1dda --- /dev/null +++ b/6-Tests/Aquiis.Infrastructure.Tests/Aquiis.Infrastructure.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/Aquiis.SimpleStart.UI.Tests/.runsettings b/6-Tests/Aquiis.UI.Professional.Tests/.runsettings similarity index 100% rename from Aquiis.SimpleStart.UI.Tests/.runsettings rename to 6-Tests/Aquiis.UI.Professional.Tests/.runsettings diff --git a/6-Tests/Aquiis.UI.Professional.Tests/Aquiis.UI.Professional.Tests.csproj b/6-Tests/Aquiis.UI.Professional.Tests/Aquiis.UI.Professional.Tests.csproj new file mode 100644 index 0000000..445c947 --- /dev/null +++ b/6-Tests/Aquiis.UI.Professional.Tests/Aquiis.UI.Professional.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs b/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs new file mode 100644 index 0000000..4a2912b --- /dev/null +++ b/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs @@ -0,0 +1,406 @@ +using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; + +namespace Aquiis.UI.Professional.Tests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] + +public class NewSetupUITests : PageTest +{ + + private const string BaseUrl = "http://localhost:5105"; + private const int KeepBrowserOpenSeconds = 30; // Set to 0 to close immediately + + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions + { + IgnoreHTTPSErrors = true, + BaseURL = BaseUrl, + RecordVideoDir = Path.Combine(Directory.GetCurrentDirectory(), "test-videos"), + RecordVideoSize = new RecordVideoSize { Width = 1280, Height = 720 } + }; + } + + [Test, Order(1)] + public async Task CreateNewAccount() + { + await Page.GotoAsync("http://localhost:5105/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Create Account" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).FillAsync("Aquiis"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Confirm Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Register" }).ClickAsync(); + + await Page.WaitForSelectorAsync("h1:has-text('Register confirmation')"); + + // await Page.GetByRole(AriaRole.Heading, new() { Name = "Register confirmation" }).ClickAsync(); + await Page.GetByRole(AriaRole.Link, new() { Name = "Click here to confirm your account" }).ClickAsync(); + + await Page.WaitForSelectorAsync("h1:has-text('Confirm email')"); + + // await Page.GetByText("Thank you for confirming your").ClickAsync(); + await Page.GetByRole(AriaRole.Link, new() { Name = "Home" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + + await Page.WaitForSelectorAsync("h1:has-text('Log in')"); + + // await Page.GetByRole(AriaRole.Heading, new() { Name = "Log in", Exact = true }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForSelectorAsync("text=Dashboard"); + + await Page.GetByRole(AriaRole.Heading, new() { Name = "Property Management Dashboard", Exact= true }).ClickAsync(); + + + } + + [Test, Order(2)] + public async Task AddProperty() + { + await Page.GotoAsync("http://localhost:5105/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + // Wait for login to complete + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + await Page.WaitForSelectorAsync("h1:has-text('Property Management Dashboard')"); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Properties" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Add Property" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).FillAsync("369 Crescent Drive"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Apt 2B, Unit" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).FillAsync("New Orleans"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).PressAsync("Tab"); + await Page.Locator("select[name=\"propertyModel.State\"]").SelectOptionAsync(new[] { "LA" }); + await Page.Locator("select[name=\"propertyModel.State\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("70119"); + await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.GetByPlaceholder("0.00").ClickAsync(); + await Page.GetByPlaceholder("0.00").FillAsync("1800"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").FillAsync("2500"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); + + // Verify property was created successfully + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + await Expect(Page.GetByText("369 Crescent Drive").First).ToBeVisibleAsync(); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Add Property" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).FillAsync("354 Maple Avenue"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Apt 2B, Unit" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).FillAsync("Los Angeles"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).PressAsync("Tab"); + await Page.Locator("select[name=\"propertyModel.State\"]").SelectOptionAsync(new[] { "CA" }); + await Page.Locator("select[name=\"propertyModel.State\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("90210"); + await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.GetByPlaceholder("0.00").ClickAsync(); + await Page.GetByPlaceholder("0.00").FillAsync("4900"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").FillAsync("3200"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); + + // Verify property was created successfully + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + await Expect(Page.GetByText("354 Maple Avenue").First).ToBeVisibleAsync(); + } + + [Test, Order(3)] + public async Task AddProspect() + { + await Page.GotoAsync("http://localhost:5105/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Add New Prospect" }).ClickAsync(); + + await Page.Locator("input[name=\"newProspect.FirstName\"]").ClickAsync(); + await Page.Locator("input[name=\"newProspect.FirstName\"]").FillAsync("Mya"); + await Page.Locator("input[name=\"newProspect.FirstName\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.LastName\"]").ClickAsync(); + await Page.Locator("input[name=\"newProspect.LastName\"]").FillAsync("Smith"); + await Page.Locator("input[name=\"newProspect.LastName\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.Email\"]").FillAsync("mya@gmail.com"); + await Page.Locator("input[name=\"newProspect.Email\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.Phone\"]").FillAsync("504-234-3600"); + await Page.Locator("input[name=\"newProspect.Phone\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.DateOfBirth\"]").FillAsync("1993-09-29"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Driver's License #" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Driver's License #" }).FillAsync("12345678"); + await Page.Locator("select[name=\"newProspect.IdentificationState\"]").SelectOptionAsync(new[] { "LA" }); + await Page.Locator("select[name=\"newProspect.Source\"]").SelectOptionAsync(new[] { "Zillow" }); + await Page.Locator("select[name=\"newProspect.InterestedPropertyId\"]").SelectOptionAsync(new[] { "354 Maple Avenue" }); + await Page.Locator("input[name=\"newProspect.DesiredMoveInDate\"]").FillAsync("2026-01-01"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Save Prospect" }).ClickAsync(); + + // Verify property was created successfully + await Page.WaitForSelectorAsync("h1:has-text('Prospective Tenants')"); + await Expect(Page.GetByText("Mya Smith").First).ToBeVisibleAsync(); + } + + [Test, Order(4)] + public async Task ScheduleAndCompleteTour() + { + await Page.GotoAsync("http://localhost:5105/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + await Page.GetByTitle("Schedule Tour").ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Schedule Tour" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Complete Tour", Exact = true }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Continue Editing" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).First.ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(1).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(2).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(3).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(4).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(5).ClickAsync(); + + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).First.ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).First.FillAsync("1800"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., $" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., $" }).FillAsync("1800"); + + await Page.Locator("div:nth-child(10) > .card-header > .btn").ClickAsync(); + await Page.Locator("div:nth-child(11) > .card-header > .btn").ClickAsync(); + + await Page.GetByText("Interested", new() { Exact = true }).ClickAsync(); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Save Progress" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Mark as Complete" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Generate PDF" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + var page1 = await Page.RunAndWaitForPopupAsync(async () => + { + await Page.GetByRole(AriaRole.Button, new() { Name = " View PDF" }).ClickAsync(); + }); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + + [Test, Order(5)] + public async Task SubmitApplication() + { + await Page.GotoAsync("http://localhost:5105/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Apply" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).FillAsync("123 Main Street"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Los Angeles" }).FillAsync("Los Angeles"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Los Angeles" }).PressAsync("Tab"); + await Page.Locator("select[name=\"applicationModel.CurrentState\"]").SelectOptionAsync(new[] { "CA" }); + await Page.Locator("select[name=\"applicationModel.CurrentState\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "90210" }).FillAsync("90210"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "90210" }).PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.CurrentRent\"]").FillAsync("1500"); + await Page.Locator("input[name=\"applicationModel.CurrentRent\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "John Smith" }).FillAsync("John Smith"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "John Smith" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "(555) 123-" }).FillAsync("555-123-4567"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "(555) 123-" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("CapsLock"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).FillAsync("ABC"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("CapsLock"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).FillAsync("ABC Company"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Software Engineer" }).FillAsync("Software Engineer"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Software Engineer" }).PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.MonthlyIncome\"]").FillAsync("9600"); + await Page.Locator("input[name=\"applicationModel.MonthlyIncome\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.EmploymentLengthMonths\"]").FillAsync("15"); + await Page.Locator("input[name=\"applicationModel.EmploymentLengthMonths\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.Reference1Name\"]").FillAsync("Richard"); + await Page.Locator("input[name=\"applicationModel.Reference1Name\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.Reference1Phone\"]").FillAsync("Zachary"); + await Page.Locator("input[name=\"applicationModel.Reference1Phone\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Friend, Coworker, etc." }).FillAsync("Spouse"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Submit Application" }).ClickAsync(); + + // Verify property was created successfully + await Expect(Page.GetByText("Application submitted successfully")).ToBeVisibleAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.WaitForSelectorAsync("h1:has-text('Prospective Tenants')"); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + + [Test, Order(6)] + public async Task ApproveApplication() + { + await Page.GotoAsync("http://localhost:5105/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByTitle("View Details").ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " View Application" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Collect Application Fee" }).ClickAsync(); + await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new[] { "Online Payment" }); + await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Payment" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Initiate Screening" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Pass" }).First.ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Pass" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Pass" }).Nth(1).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Pass" }).ClickAsync(); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Approve Application" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + + [Test] + public async Task GenerateLeaseOfferAndConvertToLease() + { + await Page.GotoAsync("http://localhost:5105/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByTitle("View Details").ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " View Application" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Generate Lease Offer" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Generate Lease Offer" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Accept Offer (Convert to Lease" }).ClickAsync(); + + await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new[] { "Online Payment" }); + await Page.GetByRole(AriaRole.Button, new() { Name = " Accept & Create Lease" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + +} \ No newline at end of file diff --git a/Aquiis.SimpleStart.UI.Tests/README.md b/6-Tests/Aquiis.UI.Professional.Tests/README.md similarity index 100% rename from Aquiis.SimpleStart.UI.Tests/README.md rename to 6-Tests/Aquiis.UI.Professional.Tests/README.md diff --git a/Aquiis.SimpleStart.UI.Tests/run-tests-debug.sh b/6-Tests/Aquiis.UI.Professional.Tests/run-tests-debug.sh similarity index 100% rename from Aquiis.SimpleStart.UI.Tests/run-tests-debug.sh rename to 6-Tests/Aquiis.UI.Professional.Tests/run-tests-debug.sh diff --git a/Aquiis.SimpleStart.UI.Tests/run-tests.sh b/6-Tests/Aquiis.UI.Professional.Tests/run-tests.sh similarity index 100% rename from Aquiis.SimpleStart.UI.Tests/run-tests.sh rename to 6-Tests/Aquiis.UI.Professional.Tests/run-tests.sh diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Aquiis.UI.Shared.Tests.csproj b/6-Tests/Aquiis.UI.Shared.Tests/Aquiis.UI.Shared.Tests.csproj new file mode 100644 index 0000000..cd30393 --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/Aquiis.UI.Shared.Tests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + $(NoWarn);CS0618;CS0619 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/CardTests.cs b/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/CardTests.cs new file mode 100644 index 0000000..b4a3f57 --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/CardTests.cs @@ -0,0 +1,132 @@ +using Aquiis.UI.Shared.Components.Common; +using Bunit; +using FluentAssertions; +using Microsoft.AspNetCore.Components; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Components.Common; + +public class CardTests : TestContext +{ + [Fact] + public void Card_Renders_With_Title() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Title, "Test Card") + ); + + // Assert + cut.Markup.Should().Contain("Test Card"); + cut.Markup.Should().Contain("card-header"); + } + + [Fact] + public void Card_Renders_Body_Content() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.ChildContent, (RenderFragment)(builder => builder.AddContent(0, "Body Content"))) + ); + + // Assert + cut.Markup.Should().Contain("Body Content"); + cut.Markup.Should().Contain("card-body"); + } + + [Fact] + public void Card_Renders_Header_Content() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.HeaderContent, (RenderFragment)(builder => builder.AddContent(0, "Custom Header"))) + ); + + // Assert + cut.Markup.Should().Contain("Custom Header"); + cut.Markup.Should().Contain("card-header"); + } + + [Fact] + public void Card_Renders_Footer_Content() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.FooterContent, (RenderFragment)(builder => builder.AddContent(0, "Footer Content"))) + ); + + // Assert + cut.Markup.Should().Contain("Footer Content"); + cut.Markup.Should().Contain("card-footer"); + } + + [Fact] + public void Card_DoesNotRender_Header_When_No_Title_Or_HeaderContent() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.ChildContent, (RenderFragment)(builder => builder.AddContent(0, "Just Body"))) + ); + + // Assert + cut.Markup.Should().NotContain("card-header"); + } + + [Fact] + public void Card_DoesNotRender_Footer_When_No_FooterContent() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.ChildContent, (RenderFragment)(builder => builder.AddContent(0, "Just Body"))) + ); + + // Assert + cut.Markup.Should().NotContain("card-footer"); + } + + [Fact] + public void Card_Prefers_HeaderContent_Over_Title() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Title, "Title Text") + .Add(p => p.HeaderContent, (RenderFragment)(builder => builder.AddContent(0, "Custom Header"))) + ); + + // Assert + cut.Markup.Should().Contain("Custom Header"); + cut.Markup.Should().NotContain("Title Text"); + } + + [Fact] + public void Card_Renders_With_Custom_CssClass() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.CssClass, "custom-card-class") + .Add(p => p.ChildContent, (RenderFragment)(builder => builder.AddContent(0, "Content"))) + ); + + // Assert + cut.Markup.Should().Contain("custom-card-class"); + } + + [Fact] + public void Card_Renders_All_Sections_Together() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Title, "Card Title") + .Add(p => p.ChildContent, (RenderFragment)(builder => builder.AddContent(0, "Body"))) + .Add(p => p.FooterContent, (RenderFragment)(builder => builder.AddContent(0, "Footer"))) + ); + + // Assert + cut.Markup.Should().Contain("Card Title"); + cut.Markup.Should().Contain("Body"); + cut.Markup.Should().Contain("Footer"); + cut.Markup.Should().Contain("card-header"); + cut.Markup.Should().Contain("card-body"); + cut.Markup.Should().Contain("card-footer"); + } +} diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/DataTableTests.cs b/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/DataTableTests.cs new file mode 100644 index 0000000..abcad34 --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/DataTableTests.cs @@ -0,0 +1,172 @@ +using Aquiis.UI.Shared.Components.Common; +using Bunit; +using FluentAssertions; +using Microsoft.AspNetCore.Components; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Components.Common; + +public class DataTableTests : TestContext +{ + private class TestItem + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } + + [Fact] + public void DataTable_Renders_With_Empty_Items() + { + // Arrange & Act + var cut = Render>(parameters => parameters + .Add(p => p.Items, new List()) + ); + + // Assert + cut.Markup.Should().Contain("No data available"); + } + + [Fact] + public void DataTable_Renders_With_Null_Items() + { + // Arrange & Act + var cut = Render>(parameters => parameters + .Add(p => p.Items, null) + ); + + // Assert + cut.Markup.Should().Contain("No data available"); + } + + [Fact] + public void DataTable_Renders_Header_Template() + { + // Arrange + var items = new List + { + new TestItem { Id = 1, Name = "Test" } + }; + + // Act + var cut = Render>(parameters => parameters + .Add(p => p.Items, items) + .Add(p => p.HeaderTemplate, (RenderFragment)(builder => + { + builder.OpenElement(0, "th"); + builder.AddContent(1, "ID"); + builder.CloseElement(); + builder.OpenElement(2, "th"); + builder.AddContent(3, "Name"); + builder.CloseElement(); + })) + ); + + // Assert + cut.Markup.Should().Contain("ID"); + cut.Markup.Should().Contain("Name"); + cut.Markup.Should().Contain(""); + } + + [Fact] + public void DataTable_Renders_Row_Template() + { + // Arrange + var items = new List + { + new TestItem { Id = 1, Name = "Item 1", Description = "Description 1" }, + new TestItem { Id = 2, Name = "Item 2", Description = "Description 2" } + }; + + // Act + var cut = Render>(parameters => parameters + .Add(p => p.Items, items) + .Add(p => p.RowTemplate, (RenderFragment)(item => builder => + { + builder.OpenElement(0, "td"); + builder.AddContent(1, item.Id); + builder.CloseElement(); + builder.OpenElement(2, "td"); + builder.AddContent(3, item.Name); + builder.CloseElement(); + })) + ); + + // Assert + cut.Markup.Should().Contain("Item 1"); + cut.Markup.Should().Contain("Item 2"); + cut.Markup.Should().Contain(""); + } + + [Fact] + public void DataTable_Renders_Multiple_Rows() + { + // Arrange + var items = new List + { + new TestItem { Id = 1, Name = "First" }, + new TestItem { Id = 2, Name = "Second" }, + new TestItem { Id = 3, Name = "Third" } + }; + + // Act + var cut = Render>(parameters => parameters + .Add(p => p.Items, items) + .Add(p => p.RowTemplate, (RenderFragment)(item => builder => + { + builder.OpenElement(0, "td"); + builder.AddContent(1, item.Name); + builder.CloseElement(); + })) + ); + + // Assert + var tbody = cut.Find("tbody"); + var rows = tbody.QuerySelectorAll("tr"); + rows.Length.Should().Be(3); + cut.Markup.Should().Contain("First"); + cut.Markup.Should().Contain("Second"); + cut.Markup.Should().Contain("Third"); + } + + [Fact] + public void DataTable_Applies_Custom_CssClass() + { + // Arrange & Act + var cut = Render>(parameters => parameters + .Add(p => p.Items, new List()) + .Add(p => p.TableCssClass, "custom-table-class") + ); + + // Assert + cut.Markup.Should().Contain("custom-table-class"); + } + + [Fact] + public void DataTable_Renders_With_Custom_EmptyMessage() + { + // Arrange & Act + var cut = Render>(parameters => parameters + .Add(p => p.Items, new List()) + .Add(p => p.EmptyMessage, "Custom empty message") + ); + + // Assert + cut.Markup.Should().Contain("Custom empty message"); + cut.Markup.Should().NotContain("No data available"); + } + + [Fact] + public void DataTable_Has_Bootstrap_Table_Classes() + { + // Arrange & Act + var cut = Render>(parameters => parameters + .Add(p => p.Items, new List()) + ); + + // Assert + cut.Markup.Should().Contain("table"); + cut.Markup.Should().Contain("table-striped"); + cut.Markup.Should().Contain("table-hover"); + } +} diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/FormFieldTests.cs b/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/FormFieldTests.cs new file mode 100644 index 0000000..7814460 --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/FormFieldTests.cs @@ -0,0 +1,134 @@ +using Aquiis.UI.Shared.Components.Common; +using Bunit; +using FluentAssertions; +using Microsoft.AspNetCore.Components; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Components.Common; + +public class FormFieldTests : TestContext +{ + [Fact] + public void FormField_Renders_Label() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Test Label") + ); + + // Assert + cut.Markup.Should().Contain("Test Label"); + cut.Markup.Should().Contain("form-label"); + } + + [Fact] + public void FormField_Renders_ChildContent() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Test Field") + .AddChildContent("") + ); + + // Assert + cut.Markup.Should().Contain("input"); + cut.Markup.Should().Contain("form-control"); + } + + [Fact] + public void FormField_Shows_Required_Indicator_When_Required_Is_True() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Required Field") + .Add(p => p.Required, true) + ); + + // Assert + cut.Markup.Should().Contain("*"); + cut.Markup.Should().Contain("text-danger"); + } + + [Fact] + public void FormField_Does_Not_Show_Required_Indicator_When_Required_Is_False() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Optional Field") + .Add(p => p.Required, false) + ); + + // Assert + cut.Markup.Should().Contain("Optional Field"); + cut.Markup.Should().NotContain("text-danger"); + } + + [Fact] + public void FormField_Renders_HelpText() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Test Field") + .Add(p => p.HelpText, "This is help text") + ); + + // Assert + cut.Markup.Should().Contain("This is help text"); + cut.Markup.Should().Contain("form-text"); + } + + [Fact] + public void FormField_Does_Not_Render_HelpText_When_Null() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Test Field") + ); + + // Assert + cut.Markup.Should().NotContain("form-text"); + } + + [Fact] + public void FormField_Has_Form_Group_Class() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Test Field") + ); + + // Assert + cut.Markup.Should().Contain("mb-3"); + } + + [Fact] + public void FormField_Applies_Custom_CssClass() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Test Field") + .Add(p => p.CssClass, "custom-field") + ); + + // Assert + cut.Markup.Should().Contain("custom-field"); + } + + [Fact] + public void FormField_Renders_Complete_Structure() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.Label, "Complete Field") + .Add(p => p.Required, true) + .Add(p => p.HelpText, "Help text") + .AddChildContent("") + ); + + // Assert + cut.Markup.Should().Contain("Complete Field"); + cut.Markup.Should().Contain("*"); + cut.Markup.Should().Contain("Help text"); + cut.Markup.Should().Contain("input"); + } +} diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/ModalTests.cs b/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/ModalTests.cs new file mode 100644 index 0000000..c6f9b29 --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/Components/Common/ModalTests.cs @@ -0,0 +1,156 @@ +using Aquiis.UI.Shared.Components.Common; +using Bunit; +using FluentAssertions; +using Microsoft.AspNetCore.Components; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Components.Common; + +public class ModalTests : TestContext +{ + [Fact] + public void Modal_Renders_When_IsVisible_Is_True() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Title, "Test Modal") + .Add(p => p.ChildContent, (RenderFragment)(builder => builder.AddContent(0, "Test Content"))) + ); + + // Assert + cut.Markup.Should().Contain("Test Modal"); + cut.Markup.Should().Contain("Test Content"); + cut.Markup.Should().Contain("modal fade show d-block"); + } + + [Fact] + public void Modal_DoesNotRender_When_IsOpen_Is_False() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, false) + .Add(p => p.Title, "Test Modal") + ); + + // Assert + cut.Markup.Should().BeNullOrWhiteSpace(); + } + + [Fact] + public void Modal_Renders_With_Small_Size() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Title, "Small Modal") + .Add(p => p.Size, ModalSize.Small) + ); + + // Assert + cut.Markup.Should().Contain("modal-sm"); + } + + [Fact] + public void Modal_Renders_With_Large_Size() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Title, "Large Modal") + .Add(p => p.Size, ModalSize.Large) + ); + + // Assert + cut.Markup.Should().Contain("modal-lg"); + } + + [Fact] + public void Modal_Renders_With_ExtraLarge_Size() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Title, "Extra Large Modal") + .Add(p => p.Size, ModalSize.ExtraLarge) + ); + + // Assert + cut.Markup.Should().Contain("modal-xl"); + } + + [Fact] + public void Modal_Renders_Centered() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Title, "Centered Modal") + .Add(p => p.Position, ModalPosition.Centered) + ); + + // Assert + cut.Markup.Should().Contain("modal-dialog-centered"); + } + + [Fact] + public void Modal_Renders_With_Backdrop() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Title, "Modal with Backdrop") + .Add(p => p.CloseOnBackdropClick, true) + ); + + // Assert + cut.Markup.Should().Contain("rgba(0,0,0,0.5)"); + } + + [Fact] + public void Modal_Calls_OnClose_When_Close_Button_Clicked() + { + // Arrange + var closeCalled = false; + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Title, "Test Modal") + .Add(p => p.OnClose, EventCallback.Factory.Create(this, () => closeCalled = true)) + ); + + // Act + var closeButton = cut.Find(".btn-close"); + closeButton.Click(); + + // Assert + closeCalled.Should().BeTrue(); + } + + [Fact] + public void Modal_Renders_Footer_Content() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Title, "Modal with Footer") + .Add(p => p.FooterContent, (RenderFragment)(builder => builder.AddContent(0, "Footer Content"))) + ); + + // Assert + cut.Markup.Should().Contain("Footer Content"); + cut.Markup.Should().Contain("modal-footer"); + } + + [Fact] + public void Modal_DoesNotRender_Footer_When_No_Content() + { + // Arrange & Act + var cut = Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Title, "Modal without Footer") + ); + + // Assert + cut.Markup.Should().NotContain("modal-footer"); + } +} diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Components/Layout/SharedMainLayoutTests.cs b/6-Tests/Aquiis.UI.Shared.Tests/Components/Layout/SharedMainLayoutTests.cs new file mode 100644 index 0000000..0b32488 --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/Components/Layout/SharedMainLayoutTests.cs @@ -0,0 +1,163 @@ +using Aquiis.UI.Shared.Components.Layout; +using Bunit; +using FluentAssertions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; +using System.Security.Claims; +using System.Threading.Tasks; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Components.Layout; + +/// +/// Tests for SharedMainLayout component. +/// Note: SharedMainLayout uses AuthorizeView, so these tests add test authorization context. +/// +public class SharedMainLayoutTests : TestContext +{ + public SharedMainLayoutTests() + { + // Add a test authorization state provider + var authState = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); + var authStateProvider = new TestAuthStateProvider(authState); + Services.AddSingleton(authStateProvider); + Services.AddSingleton(new TestAuthorizationService()); + Services.AddSingleton(new TestAuthorizationPolicyProvider()); + } + + private class TestAuthStateProvider : AuthenticationStateProvider + { + private readonly Task _authState; + public TestAuthStateProvider(Task authState) => _authState = authState; + public override Task GetAuthenticationStateAsync() => _authState; + } + + private class TestAuthorizationService : IAuthorizationService + { + public Task AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable requirements) + => Task.FromResult(AuthorizationResult.Success()); + public Task AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName) + => Task.FromResult(AuthorizationResult.Success()); + } + + private class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider + { + public Task GetDefaultPolicyAsync() + => Task.FromResult(new AuthorizationPolicy(new[] { new TestRequirement() }, new string[0])); + public Task GetFallbackPolicyAsync() => Task.FromResult(null); + public Task GetPolicyAsync(string policyName) => Task.FromResult(null); + + private class TestRequirement : IAuthorizationRequirement { } + } + + // Helper method to render SharedMainLayout with cascading authentication state + private IRenderedComponent RenderLayoutWithAuth(Action> parameters) + { + return Render(cascadingParams => + { + cascadingParams.AddChildContent(parameters); + }).FindComponent(); + } + + [Fact] + public void SharedMainLayout_Renders_ChildContent() + { + // Arrange & Act + var cut = RenderLayoutWithAuth(parameters => parameters + .Add(p => p.ChildContent, (RenderFragment)(builder => builder.AddContent(0, "Main content"))) + ); + + // Assert + cut.Markup.Should().Contain("Main content"); + cut.Markup.Should().Contain("content px-4"); + } + + [Fact] + public void SharedMainLayout_Renders_SidebarContent() + { + // Arrange & Act + var cut = RenderLayoutWithAuth(parameters => parameters + .Add(p => p.SidebarContent, (RenderFragment)(builder => builder.AddContent(0, "Sidebar content"))) + ); + + // Assert + cut.Markup.Should().Contain("Sidebar content"); + cut.Markup.Should().Contain("sidebar"); + } + + [Fact] + public void SharedMainLayout_Renders_AuthorizedHeaderContent() + { + // Arrange & Act + var cut = RenderLayoutWithAuth(parameters => parameters + .Add(p => p.AuthorizedHeaderContent, (RenderFragment)(builder => builder.AddContent(0, "Authorized Header"))) + ); + + // Assert + cut.Markup.Should().Contain("top-row px-4"); + } + + [Fact] + public void SharedMainLayout_Renders_NotAuthorizedContent() + { + // Arrange & Act + var cut = RenderLayoutWithAuth(parameters => parameters + .Add(p => p.NotAuthorizedContent, (RenderFragment)(builder => builder.AddContent(0, "Not Authorized"))) + ); + + // Assert + cut.Markup.Should().Contain("top-row px-4"); + } + + [Fact] + public void SharedMainLayout_Renders_FooterContent() + { + // Arrange & Act + var cut = RenderLayoutWithAuth(parameters => parameters + .Add(p => p.FooterContent, (RenderFragment)(builder => builder.AddContent(0, "Footer content"))) + ); + + // Assert + cut.Markup.Should().Contain("Footer content"); + } + + [Fact] + public void SharedMainLayout_Has_Theme_Attribute() + { + // Arrange & Act + var cut = RenderLayoutWithAuth(parameters => parameters + .Add(p => p.Theme, "dark") + ); + + // Assert + cut.Markup.Should().Contain("data-theme=\"dark\""); + } + + [Fact] + public void SharedMainLayout_Has_Error_UI() + { + // Arrange & Act + var cut = RenderLayoutWithAuth(parameters => parameters + .Add(p => p.ChildContent, (RenderFragment)(builder => builder.AddContent(0, "Content"))) + ); + + // Assert + cut.Markup.Should().Contain("blazor-error-ui"); + cut.Markup.Should().Contain("An unhandled error has occurred."); + } + + [Fact] + public void SharedMainLayout_Minimal_Configuration() + { + // Arrange & Act + var cut = RenderLayoutWithAuth(parameters => parameters + .Add(p => p.ChildContent, (RenderFragment)(builder => builder.AddContent(0, "Minimal"))) + ); + + // Assert + cut.Markup.Should().Contain("Minimal"); + cut.Markup.Should().Contain("page"); + } +} diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Features/Notifications/NotificationBellTests.cs b/6-Tests/Aquiis.UI.Shared.Tests/Features/Notifications/NotificationBellTests.cs new file mode 100644 index 0000000..e0b364a --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/Features/Notifications/NotificationBellTests.cs @@ -0,0 +1,33 @@ +using Aquiis.UI.Shared.Features.Notifications; +using Bunit; +using FluentAssertions; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Features.Notifications; + +/// +/// Basic structure tests for NotificationBell component. +/// Full functional testing requires NotificationService implementation. +/// +public class NotificationBellTests : TestContext +{ + [Fact] + public void NotificationBell_Component_Exists_And_Can_Be_Instantiated() + { + // Arrange & Act + // Note: This will fail if required services aren't mocked + // For now, we're just verifying the component exists and can be imported + + // Assert + typeof(NotificationBell).Should().NotBeNull(); + typeof(NotificationBell).Name.Should().Be("NotificationBell"); + } + + // Additional tests to be added when NotificationService is implemented: + // - Test notification bell renders with unread count + // - Test notification bell badge updates + // - Test dropdown opens/closes + // - Test marking notifications as read + // - Test navigation to notification center + // - Test GetEntityRoute parameter functionality +} diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Features/Notifications/NotificationCenterTests.cs b/6-Tests/Aquiis.UI.Shared.Tests/Features/Notifications/NotificationCenterTests.cs new file mode 100644 index 0000000..63ec400 --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/Features/Notifications/NotificationCenterTests.cs @@ -0,0 +1,35 @@ +using Aquiis.UI.Shared.Features.Notifications; +using Bunit; +using FluentAssertions; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Features.Notifications; + +/// +/// Basic structure tests for NotificationCenter component. +/// Full functional testing requires NotificationService implementation. +/// +public class NotificationCenterTests : TestContext +{ + [Fact] + public void NotificationCenter_Component_Exists_And_Can_Be_Instantiated() + { + // Arrange & Act + // Note: This will fail if required services aren't mocked + // For now, we're just verifying the component exists and can be imported + + // Assert + typeof(NotificationCenter).Should().NotBeNull(); + typeof(NotificationCenter).Name.Should().Be("NotificationCenter"); + } + + // Additional tests to be added when NotificationService is implemented: + // - Test notification list renders + // - Test filtering by read/unread status + // - Test pagination + // - Test mark all as read functionality + // - Test notification item click handling + // - Test empty state rendering + // - Test loading state + // - Test GetEntityRoute parameter functionality +} diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Features/Notifications/NotificationPreferencesTests.cs b/6-Tests/Aquiis.UI.Shared.Tests/Features/Notifications/NotificationPreferencesTests.cs new file mode 100644 index 0000000..c729f26 --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/Features/Notifications/NotificationPreferencesTests.cs @@ -0,0 +1,34 @@ +using Aquiis.UI.Shared.Features.Notifications; +using Bunit; +using FluentAssertions; +using Xunit; + +namespace Aquiis.UI.Shared.Tests.Features.Notifications; + +/// +/// Basic structure tests for NotificationPreferences component. +/// Full functional testing requires NotificationService and UserContextService implementation. +/// +public class NotificationPreferencesTests : TestContext +{ + [Fact] + public void NotificationPreferences_Component_Exists_And_Can_Be_Instantiated() + { + // Arrange & Act + // Note: This will fail if required services aren't mocked + // For now, we're just verifying the component exists and can be imported + + // Assert + typeof(NotificationPreferences).Should().NotBeNull(); + typeof(NotificationPreferences).Name.Should().Be("NotificationPreferences"); + } + + // Additional tests to be added when services are implemented: + // - Test preferences form renders + // - Test notification type toggles + // - Test preference saving + // - Test preference loading from UserContext + // - Test form validation + // - Test success/error messages + // - Test cancel button functionality +} diff --git a/6-Tests/Aquiis.UI.Shared.Tests/GlobalUsings.cs b/6-Tests/Aquiis.UI.Shared.Tests/GlobalUsings.cs new file mode 100644 index 0000000..604551f --- /dev/null +++ b/6-Tests/Aquiis.UI.Shared.Tests/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using Xunit; +global using Bunit; +global using FluentAssertions; +global using Microsoft.AspNetCore.Components; + +// Suppress bUnit migration warnings since we're using TestContext and RenderComponent +// as appropriate for bUnit 2.4.2 +#pragma warning disable CS0618 diff --git a/6-Tests/Aquiis.UI.SimpleStart.Tests/.runsettings b/6-Tests/Aquiis.UI.SimpleStart.Tests/.runsettings new file mode 100644 index 0000000..e0d5bb7 --- /dev/null +++ b/6-Tests/Aquiis.UI.SimpleStart.Tests/.runsettings @@ -0,0 +1,20 @@ + + + + chromium + + false + 10000 + + + true + http://localhost:5197 + + + + + Development + 1 + + + diff --git a/6-Tests/Aquiis.UI.SimpleStart.Tests/Aquiis.UI.SimpleStart.Tests.csproj b/6-Tests/Aquiis.UI.SimpleStart.Tests/Aquiis.UI.SimpleStart.Tests.csproj new file mode 100644 index 0000000..b16a620 --- /dev/null +++ b/6-Tests/Aquiis.UI.SimpleStart.Tests/Aquiis.UI.SimpleStart.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs b/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs new file mode 100644 index 0000000..527cfad --- /dev/null +++ b/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs @@ -0,0 +1,406 @@ +using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; + +namespace Aquiis.UI.SimpleStart.Tests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] + +public class NewSetupUITests : PageTest +{ + + private const string BaseUrl = "http://localhost:5197"; + private const int KeepBrowserOpenSeconds = 30; // Set to 0 to close immediately + + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions + { + IgnoreHTTPSErrors = true, + BaseURL = BaseUrl, + RecordVideoDir = Path.Combine(Directory.GetCurrentDirectory(), "test-videos"), + RecordVideoSize = new RecordVideoSize { Width = 1280, Height = 720 } + }; + } + + [Test, Order(1)] + public async Task CreateNewAccount() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Create Account" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).FillAsync("Aquiis"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Confirm Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Register" }).ClickAsync(); + + await Page.WaitForSelectorAsync("h1:has-text('Register confirmation')"); + + // await Page.GetByRole(AriaRole.Heading, new() { Name = "Register confirmation" }).ClickAsync(); + await Page.GetByRole(AriaRole.Link, new() { Name = "Click here to confirm your account" }).ClickAsync(); + + await Page.WaitForSelectorAsync("h1:has-text('Confirm email')"); + + // await Page.GetByText("Thank you for confirming your").ClickAsync(); + await Page.GetByRole(AriaRole.Link, new() { Name = "Home" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + + await Page.WaitForSelectorAsync("h1:has-text('Log in')"); + + // await Page.GetByRole(AriaRole.Heading, new() { Name = "Log in", Exact = true }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForSelectorAsync("text=Dashboard"); + + await Page.GetByRole(AriaRole.Heading, new() { Name = "Property Management Dashboard", Exact= true }).ClickAsync(); + + + } + + [Test, Order(2)] + public async Task AddProperty() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + // Wait for login to complete + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + await Page.WaitForSelectorAsync("h1:has-text('Property Management Dashboard')"); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Properties" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Add Property" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).FillAsync("369 Crescent Drive"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Apt 2B, Unit" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).FillAsync("New Orleans"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).PressAsync("Tab"); + await Page.Locator("select[name=\"propertyModel.State\"]").SelectOptionAsync(new[] { "LA" }); + await Page.Locator("select[name=\"propertyModel.State\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("70119"); + await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.GetByPlaceholder("0.00").ClickAsync(); + await Page.GetByPlaceholder("0.00").FillAsync("1800"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").FillAsync("2500"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); + + // Verify property was created successfully + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + await Expect(Page.GetByText("369 Crescent Drive").First).ToBeVisibleAsync(); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Add Property" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).FillAsync("354 Maple Avenue"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Apt 2B, Unit" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).FillAsync("Los Angeles"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).PressAsync("Tab"); + await Page.Locator("select[name=\"propertyModel.State\"]").SelectOptionAsync(new[] { "CA" }); + await Page.Locator("select[name=\"propertyModel.State\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("90210"); + await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.GetByPlaceholder("0.00").ClickAsync(); + await Page.GetByPlaceholder("0.00").FillAsync("4900"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").FillAsync("3200"); + await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); + + // Verify property was created successfully + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + await Expect(Page.GetByText("354 Maple Avenue").First).ToBeVisibleAsync(); + } + + [Test, Order(3)] + public async Task AddProspect() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Add New Prospect" }).ClickAsync(); + + await Page.Locator("input[name=\"newProspect.FirstName\"]").ClickAsync(); + await Page.Locator("input[name=\"newProspect.FirstName\"]").FillAsync("Mya"); + await Page.Locator("input[name=\"newProspect.FirstName\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.LastName\"]").ClickAsync(); + await Page.Locator("input[name=\"newProspect.LastName\"]").FillAsync("Smith"); + await Page.Locator("input[name=\"newProspect.LastName\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.Email\"]").FillAsync("mya@gmail.com"); + await Page.Locator("input[name=\"newProspect.Email\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.Phone\"]").FillAsync("504-234-3600"); + await Page.Locator("input[name=\"newProspect.Phone\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"newProspect.DateOfBirth\"]").FillAsync("1993-09-29"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Driver's License #" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Driver's License #" }).FillAsync("12345678"); + await Page.Locator("select[name=\"newProspect.IdentificationState\"]").SelectOptionAsync(new[] { "LA" }); + await Page.Locator("select[name=\"newProspect.Source\"]").SelectOptionAsync(new[] { "Zillow" }); + await Page.Locator("select[name=\"newProspect.InterestedPropertyId\"]").SelectOptionAsync(new[] { "354 Maple Avenue" }); + await Page.Locator("input[name=\"newProspect.DesiredMoveInDate\"]").FillAsync("2026-01-01"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Save Prospect" }).ClickAsync(); + + // Verify property was created successfully + await Page.WaitForSelectorAsync("h1:has-text('Prospective Tenants')"); + await Expect(Page.GetByText("Mya Smith").First).ToBeVisibleAsync(); + } + + [Test, Order(4)] + public async Task ScheduleAndCompleteTour() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + await Page.GetByTitle("Schedule Tour").ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Schedule Tour" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Complete Tour", Exact = true }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Continue Editing" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).First.ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(1).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(2).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(3).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(4).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(5).ClickAsync(); + + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).First.ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).First.FillAsync("1800"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., $" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., $" }).FillAsync("1800"); + + await Page.Locator("div:nth-child(10) > .card-header > .btn").ClickAsync(); + await Page.Locator("div:nth-child(11) > .card-header > .btn").ClickAsync(); + + await Page.GetByText("Interested", new() { Exact = true }).ClickAsync(); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Save Progress" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Mark as Complete" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Generate PDF" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + var page1 = await Page.RunAndWaitForPopupAsync(async () => + { + await Page.GetByRole(AriaRole.Button, new() { Name = " View PDF" }).ClickAsync(); + }); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + + [Test, Order(5)] + public async Task SubmitApplication() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Apply" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).FillAsync("123 Main Street"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Los Angeles" }).FillAsync("Los Angeles"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Los Angeles" }).PressAsync("Tab"); + await Page.Locator("select[name=\"applicationModel.CurrentState\"]").SelectOptionAsync(new[] { "CA" }); + await Page.Locator("select[name=\"applicationModel.CurrentState\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "90210" }).FillAsync("90210"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "90210" }).PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.CurrentRent\"]").FillAsync("1500"); + await Page.Locator("input[name=\"applicationModel.CurrentRent\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "John Smith" }).FillAsync("John Smith"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "John Smith" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "(555) 123-" }).FillAsync("555-123-4567"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "(555) 123-" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("CapsLock"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).FillAsync("ABC"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("CapsLock"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).FillAsync("ABC Company"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Software Engineer" }).FillAsync("Software Engineer"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Software Engineer" }).PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.MonthlyIncome\"]").FillAsync("9600"); + await Page.Locator("input[name=\"applicationModel.MonthlyIncome\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.EmploymentLengthMonths\"]").FillAsync("15"); + await Page.Locator("input[name=\"applicationModel.EmploymentLengthMonths\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.Reference1Name\"]").FillAsync("Richard"); + await Page.Locator("input[name=\"applicationModel.Reference1Name\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"applicationModel.Reference1Phone\"]").FillAsync("Zachary"); + await Page.Locator("input[name=\"applicationModel.Reference1Phone\"]").PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Friend, Coworker, etc." }).FillAsync("Spouse"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Submit Application" }).ClickAsync(); + + // Verify property was created successfully + await Expect(Page.GetByText("Application submitted successfully")).ToBeVisibleAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.WaitForSelectorAsync("h1:has-text('Prospective Tenants')"); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + + [Test, Order(6)] + public async Task ApproveApplication() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByTitle("View Details").ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " View Application" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Collect Application Fee" }).ClickAsync(); + await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new[] { "Online Payment" }); + await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Payment" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Initiate Screening" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Pass" }).First.ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Pass" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Pass" }).Nth(1).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Pass" }).ClickAsync(); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Approve Application" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + + [Test] + public async Task GenerateLeaseOfferAndConvertToLease() + { + await Page.GotoAsync("http://localhost:5197/"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByTitle("View Details").ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " View Application" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Generate Lease Offer" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Generate Lease Offer" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = " Accept Offer (Convert to Lease" }).ClickAsync(); + + await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new[] { "Online Payment" }); + await Page.GetByRole(AriaRole.Button, new() { Name = " Accept & Create Lease" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); + + await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + + await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); + } + +} \ No newline at end of file diff --git a/6-Tests/Aquiis.UI.SimpleStart.Tests/README.md b/6-Tests/Aquiis.UI.SimpleStart.Tests/README.md new file mode 100644 index 0000000..8c741b6 --- /dev/null +++ b/6-Tests/Aquiis.UI.SimpleStart.Tests/README.md @@ -0,0 +1,234 @@ +# Aquiis E2E Tests with Playwright + +End-to-end UI testing for Aquiis property management system using Microsoft Playwright. + +## What's Tested + +These tests automate the **Phase 5.5 Multi-Organization Management** testing scenarios from `PROPERTY-TENANT-LIFECYCLE-ROADMAP.md`: + +1. **Scenario 1:** Owner (Carlos) - Full access across all organizations +2. **Scenario 2:** Administrator (Alice) - Manage single organization +3. **Scenario 3:** PropertyManager (Bob) - View/edit assigned properties +4. **Scenario 4:** User (Lisa) - Read-only access +5. **Scenario 5:** Cross-organization data isolation +6. **Scenario 6:** Owner organization switching + +## Prerequisites + +**Your application must be running** before executing tests: + +```bash +# Start the application (from /Aquiis.SimpleStart) +dotnet watch + +# Or use the watch task +``` + +The tests expect the app at: `https://localhost:5001` + +## Running Tests + +### All Tests + +```bash +cd Aquiis.Tests +dotnet test +``` + +### Specific Test + +```bash +dotnet test --filter "FullyQualifiedName~Scenario4_User_HasReadOnlyAccess" +``` + +### With Browser UI (Headed Mode) + +By default, tests run headless. To see the browser: + +```bash +# Set environment variable +export HEADED=1 +dotnet test + +# Or in PowerShell +$env:HEADED=1 +dotnet test +``` + +### Specific Browser + +```bash +# Chromium (default) +dotnet test + +# Firefox +export BROWSER=firefox +dotnet test + +# WebKit (Safari engine) +export BROWSER=webkit +dotnet test +``` + +## Test User Accounts + +Tests use these accounts (must exist in your database): + +| User | Email | Password | Role | Organization | +| --------------- | ----------------- | -------- | --------------- | ------------- | +| Owner | owner1@aquiis.com | Today123 | Owner | Multiple orgs | +| Administrator | jc@example.com | Today123 | Administrator | Aquiis | +| PropertyManager | jh@example.com | Today123 | PropertyManager | Aquiis | +| User | mya@example.com | Today123 | User | Aquiis | + +**Ensure these users exist before running tests!** + +**Organizations in test database:** + +- Aquiis +- Aquiis - Colorado + +## Debugging Failed Tests + +### Screenshots on Failure + +Playwright automatically captures screenshots when tests fail: + +``` +Aquiis.Tests/bin/Debug/net9.0/playwright-screenshots/ +``` + +### Trace Viewer + +Enable trace recording for detailed debugging: + +```csharp +// In PageTest, add: +[SetUp] +public async Task Setup() +{ + await Context.Tracing.StartAsync(new() + { + Screenshots = true, + Snapshots = true + }); +} + +[TearDown] +public async Task Teardown() +{ + await Context.Tracing.StopAsync(new() + { + Path = "trace.zip" + }); +} +``` + +Then view traces: + +```bash +pwsh bin/Debug/net9.0/playwright.ps1 show-trace trace.zip +``` + +### Slow Motion + +Run tests in slow motion to watch interactions: + +```csharp +// Modify test to launch browser with slow motion +await Page.Context.Browser.NewPageAsync(new() +{ + SlowMo = 1000 // 1 second delay between actions +}); +``` + +## CI/CD Integration + +Tests are designed for CI/CD pipelines: + +```yaml +# GitHub Actions example +- name: Install Playwright Browsers + run: pwsh Aquiis.Tests/bin/Debug/net9.0/playwright.ps1 install --with-deps + +- name: Run E2E Tests + run: dotnet test Aquiis.Tests +``` + +## Updating Tests + +When adding new features: + +1. Add test methods to `PropertyManagementTests.cs` +2. Follow naming convention: `Scenario{N}_{Description}` +3. Use descriptive assertions with custom messages +4. Keep tests isolated (no shared state between tests) + +## Playwright Best Practices + +### Use Role-Based Selectors + +```csharp +// Good +await Page.GetByRole(AriaRole.Button, new() { Name = "Add Property" }).ClickAsync(); + +// Avoid +await Page.ClickAsync(".btn-primary"); +``` + +### Wait for Elements + +```csharp +// Good - explicit wait +await Page.WaitForSelectorAsync("h1:has-text('Properties')"); + +// Avoid - arbitrary delays +await Task.Delay(2000); +``` + +### Assertions + +```csharp +// Good - Playwright assertions (auto-wait) +await Expect(Page.GetByText("Dashboard")).ToBeVisibleAsync(); + +// Avoid - NUnit assertions (no waiting) +Assert.That(await Page.Locator("text=Dashboard").IsVisibleAsync(), Is.True); +``` + +## Troubleshooting + +### Tests can't connect to app + +- Verify app is running: `curl https://localhost:5001` +- Check `BaseUrl` in tests matches your app URL +- Ensure SSL certificate is trusted + +### Login failures + +- Verify test users exist in database +- Check passwords match (case-sensitive!) +- Inspect login page selectors (may have changed) + +### Element not found + +- Use `await Page.PauseAsync()` to debug +- Inspect selector with browser dev tools +- Check if element is in shadow DOM or iframe + +### Browsers not installed + +```bash +pwsh bin/Debug/net9.0/playwright.ps1 install +``` + +## Resources + +- [Playwright .NET Documentation](https://playwright.dev/dotnet/) +- [NUnit Playwright Integration](https://playwright.dev/dotnet/docs/test-runners) +- [Selectors Guide](https://playwright.dev/dotnet/docs/selectors) +- [Assertions](https://playwright.dev/dotnet/docs/test-assertions) + +--- + +**Note:** These are E2E tests - they test the full stack (UI → API → Database). Keep them focused on critical user workflows to avoid slow, brittle test suites. diff --git a/6-Tests/Aquiis.UI.SimpleStart.Tests/run-tests-debug.sh b/6-Tests/Aquiis.UI.SimpleStart.Tests/run-tests-debug.sh new file mode 100755 index 0000000..9c02abe --- /dev/null +++ b/6-Tests/Aquiis.UI.SimpleStart.Tests/run-tests-debug.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Run Playwright tests in debug mode with visible browser and inspector + +export PWDEBUG=1 +export DISPLAY=:0 + +dotnet test "$@" diff --git a/6-Tests/Aquiis.UI.SimpleStart.Tests/run-tests.sh b/6-Tests/Aquiis.UI.SimpleStart.Tests/run-tests.sh new file mode 100755 index 0000000..8150b07 --- /dev/null +++ b/6-Tests/Aquiis.UI.SimpleStart.Tests/run-tests.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Run Playwright tests normally (headless mode, no inspector) + +unset PWDEBUG +export DISPLAY=:0 + +dotnet test "$@" diff --git a/Aquiis.Professional/Application/Services/ApplicationService.cs b/Aquiis.Professional/Application/Services/ApplicationService.cs deleted file mode 100644 index 2e07407..0000000 --- a/Aquiis.Professional/Application/Services/ApplicationService.cs +++ /dev/null @@ -1,111 +0,0 @@ -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 deleted file mode 100644 index 2199084..0000000 --- a/Aquiis.Professional/Application/Services/CalendarEventService.cs +++ /dev/null @@ -1,260 +0,0 @@ -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 deleted file mode 100644 index 117a626..0000000 --- a/Aquiis.Professional/Application/Services/CalendarSettingsService.cs +++ /dev/null @@ -1,137 +0,0 @@ -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 deleted file mode 100644 index a1e005d..0000000 --- a/Aquiis.Professional/Application/Services/ChecklistService.cs +++ /dev/null @@ -1,654 +0,0 @@ -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 deleted file mode 100644 index d6f993a..0000000 --- a/Aquiis.Professional/Application/Services/DocumentService.cs +++ /dev/null @@ -1,432 +0,0 @@ -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 deleted file mode 100644 index cb3dc47..0000000 --- a/Aquiis.Professional/Application/Services/EmailSettingsService.cs +++ /dev/null @@ -1,164 +0,0 @@ -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 deleted file mode 100644 index 70ad8bd..0000000 --- a/Aquiis.Professional/Application/Services/FinancialReportService.cs +++ /dev/null @@ -1,287 +0,0 @@ -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 deleted file mode 100644 index 2fc8783..0000000 --- a/Aquiis.Professional/Application/Services/InspectionService.cs +++ /dev/null @@ -1,276 +0,0 @@ -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 deleted file mode 100644 index 89f9cd9..0000000 --- a/Aquiis.Professional/Application/Services/InvoiceService.cs +++ /dev/null @@ -1,465 +0,0 @@ -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 deleted file mode 100644 index 559bc57..0000000 --- a/Aquiis.Professional/Application/Services/LeaseOfferService.cs +++ /dev/null @@ -1,294 +0,0 @@ -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 deleted file mode 100644 index 9ed0f7b..0000000 --- a/Aquiis.Professional/Application/Services/LeaseService.cs +++ /dev/null @@ -1,492 +0,0 @@ -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 deleted file mode 100644 index 2adf3fd..0000000 --- a/Aquiis.Professional/Application/Services/MaintenanceService.cs +++ /dev/null @@ -1,492 +0,0 @@ -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 deleted file mode 100644 index 2556bf0..0000000 --- a/Aquiis.Professional/Application/Services/NoteService.cs +++ /dev/null @@ -1,105 +0,0 @@ -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 deleted file mode 100644 index f079dda..0000000 --- a/Aquiis.Professional/Application/Services/NotificationService.cs +++ /dev/null @@ -1,252 +0,0 @@ - -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 deleted file mode 100644 index a4120ff..0000000 --- a/Aquiis.Professional/Application/Services/OrganizationService.cs +++ /dev/null @@ -1,495 +0,0 @@ -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 deleted file mode 100644 index 6fe5289..0000000 --- a/Aquiis.Professional/Application/Services/PaymentService.cs +++ /dev/null @@ -1,410 +0,0 @@ -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 deleted file mode 100644 index 1d21717..0000000 --- a/Aquiis.Professional/Application/Services/PdfGenerators/ChecklistPdfGenerator.cs +++ /dev/null @@ -1,248 +0,0 @@ -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 deleted file mode 100644 index 286f0f4..0000000 --- a/Aquiis.Professional/Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs +++ /dev/null @@ -1,453 +0,0 @@ -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 deleted file mode 100644 index 8254a8d..0000000 --- a/Aquiis.Professional/Application/Services/PdfGenerators/InspectionPdfGenerator.cs +++ /dev/null @@ -1,362 +0,0 @@ -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 deleted file mode 100644 index 4c6188a..0000000 --- a/Aquiis.Professional/Application/Services/PdfGenerators/InvoicePdfGenerator.cs +++ /dev/null @@ -1,244 +0,0 @@ -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 deleted file mode 100644 index ba6ff8e..0000000 --- a/Aquiis.Professional/Application/Services/PdfGenerators/LeasePdfGenerator.cs +++ /dev/null @@ -1,262 +0,0 @@ -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 deleted file mode 100644 index 064dd10..0000000 --- a/Aquiis.Professional/Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs +++ /dev/null @@ -1,238 +0,0 @@ -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 deleted file mode 100644 index 0c72be0..0000000 --- a/Aquiis.Professional/Application/Services/PdfGenerators/PaymentPdfGenerator.cs +++ /dev/null @@ -1,256 +0,0 @@ -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 deleted file mode 100644 index baab68b..0000000 --- a/Aquiis.Professional/Application/Services/PropertyManagementService.cs +++ /dev/null @@ -1,2677 +0,0 @@ -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 deleted file mode 100644 index c9132ad..0000000 --- a/Aquiis.Professional/Application/Services/PropertyService.cs +++ /dev/null @@ -1,382 +0,0 @@ -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 deleted file mode 100644 index 5a6e93f..0000000 --- a/Aquiis.Professional/Application/Services/ProspectiveTenantService.cs +++ /dev/null @@ -1,218 +0,0 @@ -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 deleted file mode 100644 index 4c54208..0000000 --- a/Aquiis.Professional/Application/Services/RentalApplicationService.cs +++ /dev/null @@ -1,273 +0,0 @@ -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 deleted file mode 100644 index 9bf37a4..0000000 --- a/Aquiis.Professional/Application/Services/SMSSettingsService.cs +++ /dev/null @@ -1,127 +0,0 @@ -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 deleted file mode 100644 index 8eb883d..0000000 --- a/Aquiis.Professional/Application/Services/ScheduledTaskService.cs +++ /dev/null @@ -1,802 +0,0 @@ -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 deleted file mode 100644 index 1863189..0000000 --- a/Aquiis.Professional/Application/Services/SchemaValidationService.cs +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index 807fa35..0000000 --- a/Aquiis.Professional/Application/Services/ScreeningService.cs +++ /dev/null @@ -1,237 +0,0 @@ -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 deleted file mode 100644 index 1cd6b55..0000000 --- a/Aquiis.Professional/Application/Services/SecurityDepositService.cs +++ /dev/null @@ -1,741 +0,0 @@ -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 deleted file mode 100644 index 4ceecf1..0000000 --- a/Aquiis.Professional/Application/Services/TenantConversionService.cs +++ /dev/null @@ -1,129 +0,0 @@ -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 deleted file mode 100644 index f9fd612..0000000 --- a/Aquiis.Professional/Application/Services/TenantService.cs +++ /dev/null @@ -1,418 +0,0 @@ -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 deleted file mode 100644 index 89ef4a7..0000000 --- a/Aquiis.Professional/Application/Services/TourService.cs +++ /dev/null @@ -1,490 +0,0 @@ -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 deleted file mode 100644 index e138cc3..0000000 --- a/Aquiis.Professional/Application/Services/Workflows/AccountWorkflowService.cs +++ /dev/null @@ -1,40 +0,0 @@ - -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 deleted file mode 100644 index aecc275..0000000 --- a/Aquiis.Professional/Application/Services/Workflows/ApplicationWorkflowService.cs +++ /dev/null @@ -1,1277 +0,0 @@ -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 deleted file mode 100644 index bdd47df..0000000 --- a/Aquiis.Professional/Application/Services/Workflows/BaseWorkflowService.cs +++ /dev/null @@ -1,208 +0,0 @@ -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 deleted file mode 100644 index e8370ae..0000000 --- a/Aquiis.Professional/Application/Services/Workflows/IWorkflowState.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 3871d49..0000000 --- a/Aquiis.Professional/Application/Services/Workflows/LeaseWorkflowService.cs +++ /dev/null @@ -1,848 +0,0 @@ -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 deleted file mode 100644 index 716789d..0000000 --- a/Aquiis.Professional/Application/Services/Workflows/WorkflowAuditLog.cs +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index 8b90ddb..0000000 --- a/Aquiis.Professional/Application/Services/Workflows/WorkflowResult.cs +++ /dev/null @@ -1,78 +0,0 @@ -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/Components/App.razor b/Aquiis.Professional/Components/App.razor deleted file mode 100644 index d18299a..0000000 --- a/Aquiis.Professional/Components/App.razor +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/Aquiis.Professional/Components/Layout/MainLayout.razor b/Aquiis.Professional/Components/Layout/MainLayout.razor deleted file mode 100644 index 9e253ba..0000000 --- a/Aquiis.Professional/Components/Layout/MainLayout.razor +++ /dev/null @@ -1,23 +0,0 @@ -@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 deleted file mode 100644 index f29f3c3..0000000 --- a/Aquiis.Professional/Components/Layout/MainLayout.razor.css +++ /dev/null @@ -1,98 +0,0 @@ -.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 deleted file mode 100644 index ac6cc16..0000000 --- a/Aquiis.Professional/Components/Layout/NavMenu.razor +++ /dev/null @@ -1,92 +0,0 @@ -@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 deleted file mode 100644 index b0ed49f..0000000 --- a/Aquiis.Professional/Components/Layout/NavMenu.razor.css +++ /dev/null @@ -1,125 +0,0 @@ -.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 deleted file mode 100644 index 680bc36..0000000 --- a/Aquiis.Professional/Components/_Imports.razor +++ /dev/null @@ -1,11 +0,0 @@ -@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 deleted file mode 100644 index 992fe0b..0000000 --- a/Aquiis.Professional/Core/Constants/ApplicationConstants.cs +++ /dev/null @@ -1,789 +0,0 @@ -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 deleted file mode 100644 index 20d7e6d..0000000 --- a/Aquiis.Professional/Core/Constants/ApplicationSettings.cs +++ /dev/null @@ -1,108 +0,0 @@ -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 deleted file mode 100644 index b7bb70d..0000000 --- a/Aquiis.Professional/Core/Constants/EntityTypeNames.cs +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index ef3035f..0000000 --- a/Aquiis.Professional/Core/Constants/NotificationConstants.cs +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index 39ca49c..0000000 --- a/Aquiis.Professional/Core/Entities/ApplicationScreening.cs +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index 35a30e3..0000000 --- a/Aquiis.Professional/Core/Entities/BaseModel.cs +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index a8f72fd..0000000 --- a/Aquiis.Professional/Core/Entities/CalendarEvent.cs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index b17193a..0000000 --- a/Aquiis.Professional/Core/Entities/CalendarEventTypes.cs +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index cf71c8d..0000000 --- a/Aquiis.Professional/Core/Entities/CalendarSettings.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index e79eeca..0000000 --- a/Aquiis.Professional/Core/Entities/Checklist.cs +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index 6cf99cb..0000000 --- a/Aquiis.Professional/Core/Entities/ChecklistItem.cs +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index f1d1dea..0000000 --- a/Aquiis.Professional/Core/Entities/ChecklistTemplate.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 1d3916f..0000000 --- a/Aquiis.Professional/Core/Entities/ChecklistTemplateItem.cs +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 47b109d..0000000 --- a/Aquiis.Professional/Core/Entities/Document.cs +++ /dev/null @@ -1,84 +0,0 @@ -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 deleted file mode 100644 index c4c5698..0000000 --- a/Aquiis.Professional/Core/Entities/ISchedulableEntity.cs +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index 93ef50c..0000000 --- a/Aquiis.Professional/Core/Entities/IncomeStatement.cs +++ /dev/null @@ -1,107 +0,0 @@ -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 deleted file mode 100644 index bb3727e..0000000 --- a/Aquiis.Professional/Core/Entities/Inspection.cs +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index 9cdc4df..0000000 --- a/Aquiis.Professional/Core/Entities/Invoice.cs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index d354e32..0000000 --- a/Aquiis.Professional/Core/Entities/Lease.cs +++ /dev/null @@ -1,113 +0,0 @@ -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 deleted file mode 100644 index dfb8e04..0000000 --- a/Aquiis.Professional/Core/Entities/LeaseOffer.cs +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index 2d7a81b..0000000 --- a/Aquiis.Professional/Core/Entities/MaintenanceRequest.cs +++ /dev/null @@ -1,145 +0,0 @@ -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 deleted file mode 100644 index 181f442..0000000 --- a/Aquiis.Professional/Core/Entities/Note.cs +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 3f81476..0000000 --- a/Aquiis.Professional/Core/Entities/Notification.cs +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index a9cbe51..0000000 --- a/Aquiis.Professional/Core/Entities/NotificationPreferences.cs +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index a7e8c7c..0000000 --- a/Aquiis.Professional/Core/Entities/OperationResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 34116a5..0000000 --- a/Aquiis.Professional/Core/Entities/Organization.cs +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index f8ce643..0000000 --- a/Aquiis.Professional/Core/Entities/OrganizationEmailSettings.cs +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index cdd674b..0000000 --- a/Aquiis.Professional/Core/Entities/OrganizationSMSSettings.cs +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 4a108d1..0000000 --- a/Aquiis.Professional/Core/Entities/OrganizationSettings.cs +++ /dev/null @@ -1,130 +0,0 @@ -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 deleted file mode 100644 index 2f1e6c5..0000000 --- a/Aquiis.Professional/Core/Entities/Payment.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index fd1bbd7..0000000 --- a/Aquiis.Professional/Core/Entities/Property.cs +++ /dev/null @@ -1,188 +0,0 @@ -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 deleted file mode 100644 index 22a1939..0000000 --- a/Aquiis.Professional/Core/Entities/ProspectiveTenant.cs +++ /dev/null @@ -1,87 +0,0 @@ -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 deleted file mode 100644 index 54c5578..0000000 --- a/Aquiis.Professional/Core/Entities/RentalApplication.cs +++ /dev/null @@ -1,161 +0,0 @@ -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 deleted file mode 100644 index 6028057..0000000 --- a/Aquiis.Professional/Core/Entities/SchemaVersion.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 8f701e0..0000000 --- a/Aquiis.Professional/Core/Entities/SecurityDeposit.cs +++ /dev/null @@ -1,100 +0,0 @@ -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 deleted file mode 100644 index 4d41278..0000000 --- a/Aquiis.Professional/Core/Entities/SecurityDepositDividend.cs +++ /dev/null @@ -1,114 +0,0 @@ -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 deleted file mode 100644 index 8d321a5..0000000 --- a/Aquiis.Professional/Core/Entities/SecurityDepositInvestmentPool.cs +++ /dev/null @@ -1,108 +0,0 @@ -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 deleted file mode 100644 index 46d8591..0000000 --- a/Aquiis.Professional/Core/Entities/Tenant.cs +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 5daecdb..0000000 --- a/Aquiis.Professional/Core/Entities/Tour.cs +++ /dev/null @@ -1,75 +0,0 @@ -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 deleted file mode 100644 index 6d841e7..0000000 --- a/Aquiis.Professional/Core/Entities/UserOrganization.cs +++ /dev/null @@ -1,67 +0,0 @@ - - -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 deleted file mode 100644 index c3ce3ac..0000000 --- a/Aquiis.Professional/Core/Interfaces/IAuditable.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 5092b34..0000000 --- a/Aquiis.Professional/Core/Interfaces/ICalendarEventService.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index bf36103..0000000 --- a/Aquiis.Professional/Core/Interfaces/Services/IEmailService.cs +++ /dev/null @@ -1,9 +0,0 @@ - -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 deleted file mode 100644 index 4719046..0000000 --- a/Aquiis.Professional/Core/Interfaces/Services/ISMSService.cs +++ /dev/null @@ -1,7 +0,0 @@ - -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 deleted file mode 100644 index bbcdfe3..0000000 --- a/Aquiis.Professional/Core/Services/BaseService.cs +++ /dev/null @@ -1,394 +0,0 @@ -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 deleted file mode 100644 index 0327c42..0000000 --- a/Aquiis.Professional/Core/Validation/OptionalGuidAttribute.cs +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index 27d99f8..0000000 --- a/Aquiis.Professional/Core/Validation/RequiredGuidAttribute.cs +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 9f271ec..0000000 --- a/Aquiis.Professional/Data/ApplicationDbContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index b4f3e05..0000000 --- a/Aquiis.Professional/Data/ApplicationUser.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 9d741c0..0000000 --- a/Aquiis.Professional/Features/Administration/Application/Pages/DailyReport.razor +++ /dev/null @@ -1,148 +0,0 @@ -@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 deleted file mode 100644 index aa93eef..0000000 --- a/Aquiis.Professional/Features/Administration/Application/Pages/InitializeSchemaVersion.razor +++ /dev/null @@ -1,64 +0,0 @@ -@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 deleted file mode 100644 index 1aa0db5..0000000 --- a/Aquiis.Professional/Features/Administration/Application/Pages/ManageDatabase.razor +++ /dev/null @@ -1,777 +0,0 @@ -@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/Organizations/Pages/CreateOrganization.razor b/Aquiis.Professional/Features/Administration/Organizations/Pages/CreateOrganization.razor deleted file mode 100644 index 75c5bb7..0000000 --- a/Aquiis.Professional/Features/Administration/Organizations/Pages/CreateOrganization.razor +++ /dev/null @@ -1,197 +0,0 @@ -@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 deleted file mode 100644 index 2114660..0000000 --- a/Aquiis.Professional/Features/Administration/Organizations/Pages/EditOrganization.razor +++ /dev/null @@ -1,296 +0,0 @@ -@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 deleted file mode 100644 index d8dfb33..0000000 --- a/Aquiis.Professional/Features/Administration/Organizations/Pages/ManageUsers.razor +++ /dev/null @@ -1,454 +0,0 @@ -@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 deleted file mode 100644 index 5e3659e..0000000 --- a/Aquiis.Professional/Features/Administration/Organizations/Pages/Organizations.razor +++ /dev/null @@ -1,214 +0,0 @@ -@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 deleted file mode 100644 index e75e0b9..0000000 --- a/Aquiis.Professional/Features/Administration/Organizations/Pages/ViewOrganization.razor +++ /dev/null @@ -1,344 +0,0 @@ -@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 deleted file mode 100644 index e950749..0000000 --- a/Aquiis.Professional/Features/Administration/Settings/Pages/CalendarSettings.razor +++ /dev/null @@ -1,378 +0,0 @@ -@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 deleted file mode 100644 index 722e812..0000000 --- a/Aquiis.Professional/Features/Administration/Settings/Pages/EmailSettings.razor +++ /dev/null @@ -1,454 +0,0 @@ -@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 deleted file mode 100644 index fe2f1e5..0000000 --- a/Aquiis.Professional/Features/Administration/Settings/Pages/LateFeeSettings.razor +++ /dev/null @@ -1,439 +0,0 @@ -@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 deleted file mode 100644 index 3a22d90..0000000 --- a/Aquiis.Professional/Features/Administration/Settings/Pages/OrganizationSettings.razor +++ /dev/null @@ -1,566 +0,0 @@ -@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 deleted file mode 100644 index 5eb4858..0000000 --- a/Aquiis.Professional/Features/Administration/Settings/Pages/SMSSettings.razor +++ /dev/null @@ -1,418 +0,0 @@ -@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 deleted file mode 100644 index 4aff432..0000000 --- a/Aquiis.Professional/Features/Administration/Settings/Pages/ServiceSettings.razor +++ /dev/null @@ -1,464 +0,0 @@ -@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 deleted file mode 100644 index d712bd3..0000000 --- a/Aquiis.Professional/Features/Administration/Users/Manage.razor +++ /dev/null @@ -1,612 +0,0 @@ -@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 deleted file mode 100644 index d8bed9d..0000000 --- a/Aquiis.Professional/Features/Administration/Users/Pages/Create.razor +++ /dev/null @@ -1,311 +0,0 @@ -@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 deleted file mode 100644 index 3d6e005..0000000 --- a/Aquiis.Professional/Features/Administration/Users/View.razor +++ /dev/null @@ -1,507 +0,0 @@ -@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 deleted file mode 100644 index 4d6c8f3..0000000 --- a/Aquiis.Professional/Features/Notifications/Pages/NotificationCenter.razor +++ /dev/null @@ -1,653 +0,0 @@ -@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 deleted file mode 100644 index d32d593..0000000 --- a/Aquiis.Professional/Features/Notifications/Pages/NotificationPreferences.razor +++ /dev/null @@ -1,404 +0,0 @@ -@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 deleted file mode 100644 index 920bc6d..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Applications.razor +++ /dev/null @@ -1,250 +0,0 @@ -@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 deleted file mode 100644 index ae18d26..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor +++ /dev/null @@ -1,449 +0,0 @@ -@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 deleted file mode 100644 index fb8204a..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor +++ /dev/null @@ -1,475 +0,0 @@ -@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 deleted file mode 100644 index dbf4046..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor +++ /dev/null @@ -1,1177 +0,0 @@ -@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 deleted file mode 100644 index e8a2111..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor +++ /dev/null @@ -1,326 +0,0 @@ -@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 deleted file mode 100644 index 415bbd2..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor +++ /dev/null @@ -1,616 +0,0 @@ -@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 deleted file mode 100644 index 435ead4..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/Tours.razor +++ /dev/null @@ -1,397 +0,0 @@ -@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 deleted file mode 100644 index 6091981..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor +++ /dev/null @@ -1,667 +0,0 @@ -@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 deleted file mode 100644 index c790f05..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor +++ /dev/null @@ -1,813 +0,0 @@ -@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 deleted file mode 100644 index 043e396..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Calendar.razor +++ /dev/null @@ -1,1670 +0,0 @@ -@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 deleted file mode 100644 index e216eaf..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/CalendarListView.razor +++ /dev/null @@ -1,877 +0,0 @@ -@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 deleted file mode 100644 index 55b52bd..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Checklists.razor +++ /dev/null @@ -1,176 +0,0 @@ -@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 deleted file mode 100644 index c1af2ad..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Complete.razor +++ /dev/null @@ -1,713 +0,0 @@ -@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 deleted file mode 100644 index 69a32ce..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Create.razor +++ /dev/null @@ -1,352 +0,0 @@ -@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 deleted file mode 100644 index eac1562..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/EditTemplate.razor +++ /dev/null @@ -1,344 +0,0 @@ -@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 deleted file mode 100644 index da78b81..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor +++ /dev/null @@ -1,368 +0,0 @@ -@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 deleted file mode 100644 index 61d450e..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/Templates.razor +++ /dev/null @@ -1,349 +0,0 @@ -@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 deleted file mode 100644 index 39ce200..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Checklists/Pages/View.razor +++ /dev/null @@ -1,588 +0,0 @@ -@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 deleted file mode 100644 index a4b23f0..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/Documents.razor +++ /dev/null @@ -1,586 +0,0 @@ -@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 deleted file mode 100644 index 4445501..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor +++ /dev/null @@ -1,347 +0,0 @@ -@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 deleted file mode 100644 index aaa76e6..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Documents/Pages/_Imports.razor +++ /dev/null @@ -1,13 +0,0 @@ -@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/Pages/Create.razor b/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Create.razor deleted file mode 100644 index bd19949..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Create.razor +++ /dev/null @@ -1,552 +0,0 @@ -@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 deleted file mode 100644 index 233f84e..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/Schedule.razor +++ /dev/null @@ -1,323 +0,0 @@ -@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 deleted file mode 100644 index cd50715..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Inspections/Pages/View.razor +++ /dev/null @@ -1,426 +0,0 @@ -@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 deleted file mode 100644 index 9d54ce3..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/CreateInvoice.razor +++ /dev/null @@ -1,317 +0,0 @@ -@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 deleted file mode 100644 index 485f8cb..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/EditInvoice.razor +++ /dev/null @@ -1,396 +0,0 @@ -@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 deleted file mode 100644 index 3fcf0b1..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Invoices.razor +++ /dev/null @@ -1,592 +0,0 @@ -@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 deleted file mode 100644 index 3f42340..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/ViewInvoice.razor +++ /dev/null @@ -1,408 +0,0 @@ -@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 deleted file mode 100644 index c3e68f3..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/LeaseOffers.razor +++ /dev/null @@ -1,165 +0,0 @@ -@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 deleted file mode 100644 index bcbef55..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor +++ /dev/null @@ -1,574 +0,0 @@ -@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 deleted file mode 100644 index 748639d..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/AcceptLease.razor +++ /dev/null @@ -1,637 +0,0 @@ -@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 deleted file mode 100644 index bc1796e..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/CreateLease.razor +++ /dev/null @@ -1,383 +0,0 @@ -@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 deleted file mode 100644 index ef47db0..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/EditLease.razor +++ /dev/null @@ -1,357 +0,0 @@ -@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 deleted file mode 100644 index 4ed018a..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Leases.razor +++ /dev/null @@ -1,837 +0,0 @@ -@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 deleted file mode 100644 index a3b205a..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Leases/Pages/ViewLease.razor +++ /dev/null @@ -1,1263 +0,0 @@ -@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 deleted file mode 100644 index b8533c1..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor +++ /dev/null @@ -1,354 +0,0 @@ -@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 deleted file mode 100644 index 545ed1b..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/EditMaintenanceRequest.razor +++ /dev/null @@ -1,306 +0,0 @@ -@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 deleted file mode 100644 index bd605ee..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/MaintenanceRequests.razor +++ /dev/null @@ -1,350 +0,0 @@ -@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 deleted file mode 100644 index 43edf24..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/ViewMaintenanceRequest.razor +++ /dev/null @@ -1,309 +0,0 @@ -@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 deleted file mode 100644 index 5871bf1..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor +++ /dev/null @@ -1,4 +0,0 @@ -@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/EditPayment.razor b/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/EditPayment.razor deleted file mode 100644 index eb492c0..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/EditPayment.razor +++ /dev/null @@ -1,278 +0,0 @@ -@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 deleted file mode 100644 index 00f21fb..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Payments.razor +++ /dev/null @@ -1,492 +0,0 @@ -@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 deleted file mode 100644 index d1ca627..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/ViewPayment.razor +++ /dev/null @@ -1,417 +0,0 @@ -@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 deleted file mode 100644 index f940988..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Payments/Pages/_Imports.razor +++ /dev/null @@ -1,12 +0,0 @@ -@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 deleted file mode 100644 index d1a54b8..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Create.razor +++ /dev/null @@ -1,260 +0,0 @@ -@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 deleted file mode 100644 index 4ef965c..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Edit.razor +++ /dev/null @@ -1,399 +0,0 @@ -@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 deleted file mode 100644 index 3747e97..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Index.razor +++ /dev/null @@ -1,558 +0,0 @@ -@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 deleted file mode 100644 index 0d994bf..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor +++ /dev/null @@ -1,626 +0,0 @@ -@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 deleted file mode 100644 index 751eb7b..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor +++ /dev/null @@ -1,240 +0,0 @@ -@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 deleted file mode 100644 index d197ff8..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor +++ /dev/null @@ -1,259 +0,0 @@ -@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 deleted file mode 100644 index 15e5e7a..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/RentRollReport.razor +++ /dev/null @@ -1,243 +0,0 @@ -@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 deleted file mode 100644 index e653420..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/Reports.razor +++ /dev/null @@ -1,278 +0,0 @@ -@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 deleted file mode 100644 index d2118c2..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Reports/Pages/TaxReport.razor +++ /dev/null @@ -1,287 +0,0 @@ -@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/SecurityDeposits/Pages/CalculateDividends.razor b/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor deleted file mode 100644 index 4d4a7c4..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor +++ /dev/null @@ -1,337 +0,0 @@ -@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 deleted file mode 100644 index c33f310..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor +++ /dev/null @@ -1,345 +0,0 @@ -@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 deleted file mode 100644 index e19b5f0..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor +++ /dev/null @@ -1,359 +0,0 @@ -@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 deleted file mode 100644 index a9ceade..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor +++ /dev/null @@ -1,401 +0,0 @@ -@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 deleted file mode 100644 index 4ab7523..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor +++ /dev/null @@ -1,419 +0,0 @@ -@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 deleted file mode 100644 index 5065e35..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor +++ /dev/null @@ -1,217 +0,0 @@ -@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 deleted file mode 100644 index fbae2fb..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/EditTenant.razor +++ /dev/null @@ -1,339 +0,0 @@ -@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 deleted file mode 100644 index f51e11f..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Tenants.razor +++ /dev/null @@ -1,528 +0,0 @@ -@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 deleted file mode 100644 index 84053fd..0000000 --- a/Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/View.razor +++ /dev/null @@ -1,241 +0,0 @@ -@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 deleted file mode 100644 index 1eb3f46..0000000 --- a/Aquiis.Professional/Features/_Imports.razor +++ /dev/null @@ -1,21 +0,0 @@ -@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/Infrastructure/Data/ApplicationDbContext.cs b/Aquiis.Professional/Infrastructure/Data/ApplicationDbContext.cs deleted file mode 100644 index bae9991..0000000 --- a/Aquiis.Professional/Infrastructure/Data/ApplicationDbContext.cs +++ /dev/null @@ -1,742 +0,0 @@ -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 deleted file mode 100644 index 077b9a1..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251209234246_InitialCreate.Designer.cs +++ /dev/null @@ -1,3914 +0,0 @@ -// -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 deleted file mode 100644 index 11fb45b..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251209234246_InitialCreate.cs +++ /dev/null @@ -1,2076 +0,0 @@ -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 deleted file mode 100644 index 5454978..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.Designer.cs +++ /dev/null @@ -1,3917 +0,0 @@ -// -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 deleted file mode 100644 index 3e6b2dd..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.cs +++ /dev/null @@ -1,90 +0,0 @@ -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 deleted file mode 100644 index 93f0d03..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.Designer.cs +++ /dev/null @@ -1,3917 +0,0 @@ -// -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 deleted file mode 100644 index dca430e..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.cs +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index a94234e..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.Designer.cs +++ /dev/null @@ -1,4123 +0,0 @@ -// -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 deleted file mode 100644 index 28b7d0a..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.cs +++ /dev/null @@ -1,666 +0,0 @@ -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 deleted file mode 100644 index 8189662..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.Designer.cs +++ /dev/null @@ -1,4324 +0,0 @@ -// -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 deleted file mode 100644 index 5c10a08..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.cs +++ /dev/null @@ -1,116 +0,0 @@ -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 deleted file mode 100644 index 643a304..0000000 --- a/Aquiis.Professional/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ /dev/null @@ -1,4321 +0,0 @@ -// -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 deleted file mode 100644 index c1dfbf4..0000000 --- a/Aquiis.Professional/Infrastructure/Services/EmailService.cs +++ /dev/null @@ -1,41 +0,0 @@ - -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 deleted file mode 100644 index eb2e9ca..0000000 --- a/Aquiis.Professional/Infrastructure/Services/EntityRouteHelper.cs +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index d529a23..0000000 --- a/Aquiis.Professional/Infrastructure/Services/SMSService.cs +++ /dev/null @@ -1,29 +0,0 @@ - -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 deleted file mode 100644 index f7ab9f5..0000000 --- a/Aquiis.Professional/Infrastructure/Services/SendGridEmailService.cs +++ /dev/null @@ -1,276 +0,0 @@ -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 deleted file mode 100644 index 9a732bf..0000000 --- a/Aquiis.Professional/Infrastructure/Services/TwilioSMSService.cs +++ /dev/null @@ -1,222 +0,0 @@ -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 deleted file mode 100644 index 86040ca..0000000 --- a/Aquiis.Professional/Program.cs +++ /dev/null @@ -1,586 +0,0 @@ -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/Shared/App.razor b/Aquiis.Professional/Shared/App.razor deleted file mode 100644 index d3fd5a1..0000000 --- a/Aquiis.Professional/Shared/App.razor +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Aquiis.Professional/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs b/Aquiis.Professional/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs deleted file mode 100644 index 388b785..0000000 --- a/Aquiis.Professional/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs +++ /dev/null @@ -1,72 +0,0 @@ -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/Components/Account/ApplicationUser.cs b/Aquiis.Professional/Shared/Components/Account/ApplicationUser.cs deleted file mode 100644 index 3551da0..0000000 --- a/Aquiis.Professional/Shared/Components/Account/ApplicationUser.cs +++ /dev/null @@ -1,25 +0,0 @@ -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/IdentityNoOpEmailSender.cs b/Aquiis.Professional/Shared/Components/Account/IdentityNoOpEmailSender.cs deleted file mode 100644 index 4b1963a..0000000 --- a/Aquiis.Professional/Shared/Components/Account/IdentityNoOpEmailSender.cs +++ /dev/null @@ -1,20 +0,0 @@ -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/IdentityRevalidatingAuthenticationStateProvider.cs b/Aquiis.Professional/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs deleted file mode 100644 index 29df6cb..0000000 --- a/Aquiis.Professional/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 2ca8618..0000000 --- a/Aquiis.Professional/Shared/Components/Account/IdentityUserAccessor.cs +++ /dev/null @@ -1,19 +0,0 @@ -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/ExternalLogin.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ExternalLogin.razor deleted file mode 100644 index b7cd0c2..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/ExternalLogin.razor +++ /dev/null @@ -1,204 +0,0 @@ -@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 deleted file mode 100644 index 15b6bb5..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/ForgotPassword.razor +++ /dev/null @@ -1,67 +0,0 @@ -@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/Login.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Login.razor deleted file mode 100644 index d3c83b4..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Login.razor +++ /dev/null @@ -1,127 +0,0 @@ -@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 deleted file mode 100644 index e117b0c..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/LoginWith2fa.razor +++ /dev/null @@ -1,100 +0,0 @@ -@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 deleted file mode 100644 index 5759f11..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor +++ /dev/null @@ -1,84 +0,0 @@ -@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 deleted file mode 100644 index c7c0ed0..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ChangePassword.razor +++ /dev/null @@ -1,95 +0,0 @@ -@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 deleted file mode 100644 index bbb1034..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor +++ /dev/null @@ -1,85 +0,0 @@ -@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 deleted file mode 100644 index 2ecfb40..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Disable2fa.razor +++ /dev/null @@ -1,62 +0,0 @@ -@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 deleted file mode 100644 index 3b93e23..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Email.razor +++ /dev/null @@ -1,122 +0,0 @@ -@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 deleted file mode 100644 index 0084a6f..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor +++ /dev/null @@ -1,171 +0,0 @@ -@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 deleted file mode 100644 index 0c109c4..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ExternalLogins.razor +++ /dev/null @@ -1,139 +0,0 @@ -@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 deleted file mode 100644 index 99f6438..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor +++ /dev/null @@ -1,67 +0,0 @@ -@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 deleted file mode 100644 index 8ff0c8c..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/Index.razor +++ /dev/null @@ -1,116 +0,0 @@ -@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 deleted file mode 100644 index 3cd179e..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/PersonalData.razor +++ /dev/null @@ -1,34 +0,0 @@ -@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 deleted file mode 100644 index 0a08d34..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor +++ /dev/null @@ -1,51 +0,0 @@ -@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 deleted file mode 100644 index 307a660..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/SetPassword.razor +++ /dev/null @@ -1,86 +0,0 @@ -@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 deleted file mode 100644 index 75b15eb..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor +++ /dev/null @@ -1,100 +0,0 @@ -@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/Register.razor b/Aquiis.Professional/Shared/Components/Account/Pages/Register.razor deleted file mode 100644 index a463067..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/Register.razor +++ /dev/null @@ -1,264 +0,0 @@ -@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/ResendEmailConfirmation.razor b/Aquiis.Professional/Shared/Components/Account/Pages/ResendEmailConfirmation.razor deleted file mode 100644 index b1bd072..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/ResendEmailConfirmation.razor +++ /dev/null @@ -1,67 +0,0 @@ -@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 deleted file mode 100644 index 0503cf3..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/ResetPassword.razor +++ /dev/null @@ -1,102 +0,0 @@ -@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/_Imports.razor b/Aquiis.Professional/Shared/Components/Account/Pages/_Imports.razor deleted file mode 100644 index ee4dd58..0000000 --- a/Aquiis.Professional/Shared/Components/Account/Pages/_Imports.razor +++ /dev/null @@ -1,2 +0,0 @@ -@using Aquiis.Professional.Shared.Components.Account.Shared -@attribute [ExcludeFromInteractiveRouting] diff --git a/Aquiis.Professional/Shared/Components/LeaseRenewalWidget.razor b/Aquiis.Professional/Shared/Components/LeaseRenewalWidget.razor deleted file mode 100644 index 1f13f05..0000000 --- a/Aquiis.Professional/Shared/Components/LeaseRenewalWidget.razor +++ /dev/null @@ -1,179 +0,0 @@ -@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 deleted file mode 100644 index 8c5c9f8..0000000 --- a/Aquiis.Professional/Shared/Components/NotesTimeline.razor +++ /dev/null @@ -1,246 +0,0 @@ -@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 deleted file mode 100644 index a79f909..0000000 --- a/Aquiis.Professional/Shared/Components/NotificationBell.razor +++ /dev/null @@ -1,296 +0,0 @@ -@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 deleted file mode 100644 index 90e5c23..0000000 --- a/Aquiis.Professional/Shared/Components/OrganizationSwitcher.razor +++ /dev/null @@ -1,190 +0,0 @@ -@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/Home.razor b/Aquiis.Professional/Shared/Components/Pages/Home.razor deleted file mode 100644 index 9f0c5d8..0000000 --- a/Aquiis.Professional/Shared/Components/Pages/Home.razor +++ /dev/null @@ -1,478 +0,0 @@ -@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 deleted file mode 100644 index f05c374..0000000 --- a/Aquiis.Professional/Shared/Components/SchemaValidationWarning.razor +++ /dev/null @@ -1,66 +0,0 @@ -@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/Shared/OrganizationAuthorizeView.razor b/Aquiis.Professional/Shared/Components/Shared/OrganizationAuthorizeView.razor deleted file mode 100644 index 59890ac..0000000 --- a/Aquiis.Professional/Shared/Components/Shared/OrganizationAuthorizeView.razor +++ /dev/null @@ -1,62 +0,0 @@ -@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 deleted file mode 100644 index 04a6707..0000000 --- a/Aquiis.Professional/Shared/Components/ToastContainer.razor +++ /dev/null @@ -1,164 +0,0 @@ -@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 deleted file mode 100644 index 85f142e..0000000 --- a/Aquiis.Professional/Shared/Layout/MainLayout.razor +++ /dev/null @@ -1,55 +0,0 @@ -@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/Services/DatabaseBackupService.cs b/Aquiis.Professional/Shared/Services/DatabaseBackupService.cs deleted file mode 100644 index 39faac1..0000000 --- a/Aquiis.Professional/Shared/Services/DatabaseBackupService.cs +++ /dev/null @@ -1,414 +0,0 @@ -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 deleted file mode 100644 index 0fd5654..0000000 --- a/Aquiis.Professional/Shared/Services/ElectronPathService.cs +++ /dev/null @@ -1,80 +0,0 @@ -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/UserContextService.cs b/Aquiis.Professional/Shared/Services/UserContextService.cs deleted file mode 100644 index 64d4c5a..0000000 --- a/Aquiis.Professional/Shared/Services/UserContextService.cs +++ /dev/null @@ -1,300 +0,0 @@ -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 deleted file mode 100644 index a88b088..0000000 --- a/Aquiis.Professional/Shared/_Imports.razor +++ /dev/null @@ -1,19 +0,0 @@ -@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 deleted file mode 100644 index 0b92640..0000000 --- a/Aquiis.Professional/Utilities/CalendarEventRouter.cs +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 766fb8f..0000000 --- a/Aquiis.Professional/Utilities/SchedulableEntityRegistry.cs +++ /dev/null @@ -1,87 +0,0 @@ -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.json b/Aquiis.Professional/appsettings.json deleted file mode 100644 index 5654d9a..0000000 --- a/Aquiis.Professional/appsettings.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "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.SimpleStart.Tests/ApplicationWorkflowServiceTests.cs b/Aquiis.SimpleStart.Tests/ApplicationWorkflowServiceTests.cs deleted file mode 100644 index b0527f7..0000000 --- a/Aquiis.SimpleStart.Tests/ApplicationWorkflowServiceTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Security.Claims; -using System.Threading.Tasks; -using System; -using Aquiis.SimpleStart.Application.Services.Workflows; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Core.Constants; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; - -namespace Aquiis.SimpleStart.Tests; - -public class ApplicationWorkflowServiceTests -{ - [Fact] - public async Task GetApplicationWorkflowStateAsync_ReturnsExpectedState() - { - // Arrange - // Use SQLite in-memory to support transactions used by workflow base class - var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); - connection.Open(); - var options = new DbContextOptionsBuilder() - .UseSqlite(connection) - .Options; - // Create test user and org - var testUserId = "test-user-id"; - var orgId = Guid.NewGuid(); - - // Mock AuthenticationStateProvider - var claims = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, testUserId) }, "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - // Mock UserManager to return an ApplicationUser with ActiveOrganizationId set - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, - null, null, null, null, null, null, null, null); - - var appUser = new ApplicationUser { Id = testUserId, ActiveOrganizationId = orgId }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())).ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - - // Create real UserContextService using mocks - var userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); - - // Create DbContext and seed prospect/property - await using var context = new SimpleStart.Infrastructure.Data.ApplicationDbContext(options); - // Ensure schema is created for SQLite in-memory - await context.Database.EnsureCreatedAsync(); - var appUserEntity = new ApplicationUser { Id = testUserId, UserName = "testuser", Email = "t@t.com", ActiveOrganizationId = orgId }; - context.Users.Add(appUserEntity); - - var org = new Organization { Id = orgId, Name = "TestOrg", OwnerId = testUserId, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; - context.Organizations.Add(org); - - var prospect = new ProspectiveTenant { Id = Guid.NewGuid(), OrganizationId = orgId, FirstName = "Test", LastName = "User", Email = "t@t.com", Phone = "123", Status = "Lead", CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; - var property = new Property { Id = Guid.NewGuid(), OrganizationId = orgId, Address = "123 Main", City = "X", State = "ST", ZipCode = "00000", Status = ApplicationConstants.PropertyStatuses.Available, CreatedBy = testUserId, CreatedOn = DateTime.UtcNow }; - context.ProspectiveTenants.Add(prospect); - context.Properties.Add(property); - await context.SaveChangesAsync(); - - // Create NoteService (not used heavily in this test) - var noteService = new Application.Services.NoteService(context, userContext); - - var workflowService = new ApplicationWorkflowService(context, userContext, noteService); - - // Act - submit application then initiate screening - var submissionModel = new ApplicationSubmissionModel - { - ApplicationFee = 25m, - ApplicationFeePaid = true, - ApplicationFeePaymentMethod = "Card", - CurrentAddress = "Addr", - CurrentCity = "C", - CurrentState = "ST", - CurrentZipCode = "00000", - CurrentRent = 1000m, - LandlordName = "L", - LandlordPhone = "P", - EmployerName = "E", - JobTitle = "J", - MonthlyIncome = 2000m, - EmploymentLengthMonths = 12, - Reference1Name = "R1", - Reference1Phone = "111", - Reference1Relationship = "Friend" - }; - - var submitResult = await workflowService.SubmitApplicationAsync(prospect.Id, property.Id, submissionModel); - Assert.True(submitResult.Success, string.Join(";", submitResult.Errors)); - - var application = submitResult.Data!; - - var screeningResult = await workflowService.InitiateScreeningAsync(application.Id, true, true); - Assert.True(screeningResult.Success, string.Join(";", screeningResult.Errors)); - - // Get aggregated workflow state - var state = await workflowService.GetApplicationWorkflowStateAsync(application.Id); - - // Assert - Assert.NotNull(state.Application); - Assert.NotEqual(Guid.Empty, state.Application.Id); - Assert.NotNull(state.Prospect); - Assert.NotEqual(Guid.Empty, state.Prospect.Id); - Assert.NotNull(state.Property); - Assert.NotEqual(Guid.Empty, state.Property.Id); - Assert.NotNull(state.Screening); - Assert.NotEqual(Guid.Empty, state.Screening.Id); - Assert.NotEmpty(state.AuditHistory); - Assert.All(state.AuditHistory, item => Assert.NotEqual(Guid.Empty, item.Id)); - - } -} diff --git a/Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj b/Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj deleted file mode 100644 index d6ed6c2..0000000 --- a/Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net9.0 - false - enable - - - - - - - - - - - - - - - diff --git a/Aquiis.SimpleStart.Tests/BaseServiceTests.cs b/Aquiis.SimpleStart.Tests/BaseServiceTests.cs deleted file mode 100644 index 23342a1..0000000 --- a/Aquiis.SimpleStart.Tests/BaseServiceTests.cs +++ /dev/null @@ -1,692 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using Xunit; - -namespace Aquiis.SimpleStart.Tests -{ - /// - /// Unit tests for BaseService generic CRUD operations. - /// Tests organization isolation, soft delete, audit fields, and security. - /// - public class BaseServiceTests : IDisposable - { - private readonly ApplicationDbContext _context; - private readonly UserContextService _userContext; - private readonly TestPropertyService _service; - private readonly string _testUserId; - private readonly Guid _testOrgId; - private readonly Microsoft.Data.Sqlite.SqliteConnection _connection; - - public BaseServiceTests() - { - // Setup SQLite in-memory database - _connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); - _connection.Open(); - - var options = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .Options; - - _context = new ApplicationDbContext(options); - _context.Database.EnsureCreated(); - - // Setup test user and organization - _testUserId = "test-user-123"; - _testOrgId = Guid.NewGuid(); - - // Mock AuthenticationStateProvider - var claims = new ClaimsPrincipal(new ClaimsIdentity( - new[] { new Claim(ClaimTypes.NameIdentifier, _testUserId) }, - "TestAuth")); - var mockAuth = new Mock(); - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - // Mock UserManager - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, null, null, null, null, null, null, null, null); - - var appUser = new ApplicationUser - { - Id = _testUserId, - UserName = "testuser", - Email = "test@example.com", - ActiveOrganizationId = _testOrgId - }; - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())) - .ReturnsAsync(appUser); - - var serviceProvider = new Mock(); - _userContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); - - // Seed test data - var user = new ApplicationUser - { - Id = _testUserId, - UserName = "testuser", - Email = "test@example.com", - ActiveOrganizationId = _testOrgId - }; - _context.Users.Add(user); - - var org = new Organization - { - Id = _testOrgId, - Name = "Test Org", - OwnerId = _testUserId, - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Organizations.Add(org); - _context.SaveChanges(); - - // Create service with mocked settings - var mockSettings = Options.Create(new ApplicationSettings - { - SoftDeleteEnabled = true - }); - - var mockLogger = new Mock>(); - _service = new TestPropertyService(_context, mockLogger.Object, _userContext, mockSettings); - } - - public void Dispose() - { - _context?.Dispose(); - _connection?.Dispose(); - } - - #region CreateAsync Tests - - [Fact] - public async Task CreateAsync_ValidEntity_CreatesSuccessfully() - { - // Arrange - var property = new Property - { - Address = "123 Main St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House" - }; - - // Act - var result = await _service.CreateAsync(property); - - // Assert - Assert.NotNull(result); - Assert.NotEqual(Guid.Empty, result.Id); - Assert.Equal(_testOrgId, result.OrganizationId); - Assert.Equal(_testUserId, result.CreatedBy); - Assert.True(result.CreatedOn <= DateTime.UtcNow); - Assert.False(result.IsDeleted); - } - - [Fact] - public async Task CreateAsync_AutoGeneratesIdIfEmpty() - { - // Arrange - var property = new Property - { - Id = Guid.Empty, // Explicitly empty - Address = "456 Oak Ave", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "Apartment" - }; - - // Act - var result = await _service.CreateAsync(property); - - // Assert - Assert.NotEqual(Guid.Empty, result.Id); - } - - [Fact] - public async Task CreateAsync_SetsAuditFieldsAutomatically() - { - // Arrange - var property = new Property - { - Address = "789 Pine St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "Condo" - }; - var beforeCreate = DateTime.UtcNow; - - // Act - var result = await _service.CreateAsync(property); - - // Assert - Assert.Equal(_testUserId, result.CreatedBy); - Assert.True(result.CreatedOn >= beforeCreate); - Assert.True(result.CreatedOn <= DateTime.UtcNow); - Assert.Null(result.LastModifiedBy); - Assert.Null(result.LastModifiedOn); - } - - [Fact] - public async Task CreateAsync_SetsOrganizationIdAutomatically() - { - // Arrange - var property = new Property - { - OrganizationId = Guid.Empty, // Even if explicitly empty - Address = "321 Elm St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "Townhouse" - }; - - // Act - var result = await _service.CreateAsync(property); - - // Assert - Assert.Equal(_testOrgId, result.OrganizationId); - } - - #endregion - - #region GetByIdAsync Tests - - [Fact] - public async Task GetByIdAsync_ExistingEntity_ReturnsEntity() - { - // Arrange - var property = new Property - { - Id = Guid.NewGuid(), - OrganizationId = _testOrgId, - Address = "555 Maple Dr", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Properties.Add(property); - await _context.SaveChangesAsync(); - - // Act - var result = await _service.GetByIdAsync(property.Id); - - // Assert - Assert.NotNull(result); - Assert.Equal(property.Id, result.Id); - Assert.Equal(property.Address, result.Address); - } - - [Fact] - public async Task GetByIdAsync_NonExistentEntity_ReturnsNull() - { - // Arrange - var nonExistentId = Guid.NewGuid(); - - // Act - var result = await _service.GetByIdAsync(nonExistentId); - - // Assert - Assert.Null(result); - } - - [Fact] - public async Task GetByIdAsync_SoftDeletedEntity_ReturnsNull() - { - // Arrange - var property = new Property - { - Id = Guid.NewGuid(), - OrganizationId = _testOrgId, - Address = "777 Birch Ln", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow, - IsDeleted = true // Soft deleted - }; - _context.Properties.Add(property); - await _context.SaveChangesAsync(); - - // Act - var result = await _service.GetByIdAsync(property.Id); - - // Assert - Assert.Null(result); // Should not return deleted entities - } - - [Fact] - public async Task GetByIdAsync_DifferentOrganization_ReturnsNull() - { - // Arrange - var differentOrgId = Guid.NewGuid(); - var differentOrg = new Organization - { - Id = differentOrgId, - Name = "Different Org", - OwnerId = _testUserId, - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Organizations.Add(differentOrg); - - var property = new Property - { - Id = Guid.NewGuid(), - OrganizationId = differentOrgId, // Different organization - Address = "999 Cedar Ct", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Properties.Add(property); - await _context.SaveChangesAsync(); - - // Act - var result = await _service.GetByIdAsync(property.Id); - - // Assert - Assert.Null(result); // Should not return entities from other orgs - } - - #endregion - - #region GetAllAsync Tests - - [Fact] - public async Task GetAllAsync_ReturnsAllActiveEntities() - { - // Arrange - var properties = new[] - { - new Property { Id = Guid.NewGuid(), OrganizationId = _testOrgId, Address = "100 Test St", City = "City", State = "ST", ZipCode = "12345", PropertyType = "House", CreatedBy = _testUserId, CreatedOn = DateTime.UtcNow }, - new Property { Id = Guid.NewGuid(), OrganizationId = _testOrgId, Address = "200 Test St", City = "City", State = "ST", ZipCode = "12345", PropertyType = "Apartment", CreatedBy = _testUserId, CreatedOn = DateTime.UtcNow }, - new Property { Id = Guid.NewGuid(), OrganizationId = _testOrgId, Address = "300 Test St", City = "City", State = "ST", ZipCode = "12345", PropertyType = "Condo", CreatedBy = _testUserId, CreatedOn = DateTime.UtcNow } - }; - _context.Properties.AddRange(properties); - await _context.SaveChangesAsync(); - - // Act - var result = await _service.GetAllAsync(); - - // Assert - Assert.Equal(3, result.Count); - } - - [Fact] - public async Task GetAllAsync_ExcludesSoftDeletedEntities() - { - // Arrange - var activeProperty = new Property { Id = Guid.NewGuid(), OrganizationId = _testOrgId, Address = "400 Test St", City = "City", State = "ST", ZipCode = "12345", PropertyType = "House", CreatedBy = _testUserId, CreatedOn = DateTime.UtcNow, IsDeleted = false }; - var deletedProperty = new Property { Id = Guid.NewGuid(), OrganizationId = _testOrgId, Address = "500 Test St", City = "City", State = "ST", ZipCode = "12345", PropertyType = "House", CreatedBy = _testUserId, CreatedOn = DateTime.UtcNow, IsDeleted = true }; - _context.Properties.AddRange(activeProperty, deletedProperty); - await _context.SaveChangesAsync(); - - // Act - var result = await _service.GetAllAsync(); - - // Assert - Assert.Single(result); - Assert.Equal(activeProperty.Id, result[0].Id); - } - - [Fact] - public async Task GetAllAsync_FiltersOnlyCurrentOrganization() - { - // Arrange - var differentOrgId = Guid.NewGuid(); - var differentOrg = new Organization - { - Id = differentOrgId, - Name = "Different Org", - OwnerId = _testUserId, - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Organizations.Add(differentOrg); - - var myProperty = new Property { Id = Guid.NewGuid(), OrganizationId = _testOrgId, Address = "600 Test St", City = "City", State = "ST", ZipCode = "12345", PropertyType = "House", CreatedBy = _testUserId, CreatedOn = DateTime.UtcNow }; - var otherProperty = new Property { Id = Guid.NewGuid(), OrganizationId = differentOrgId, Address = "700 Test St", City = "City", State = "ST", ZipCode = "12345", PropertyType = "House", CreatedBy = _testUserId, CreatedOn = DateTime.UtcNow }; - _context.Properties.AddRange(myProperty, otherProperty); - await _context.SaveChangesAsync(); - - // Act - var result = await _service.GetAllAsync(); - - // Assert - Assert.Single(result); - Assert.Equal(myProperty.Id, result[0].Id); - } - - #endregion - - #region UpdateAsync Tests - - [Fact] - public async Task UpdateAsync_ValidEntity_UpdatesSuccessfully() - { - // Arrange - var property = new Property - { - Id = Guid.NewGuid(), - OrganizationId = _testOrgId, - Address = "800 Original St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Properties.Add(property); - await _context.SaveChangesAsync(); - - property.Address = "800 Updated St"; - var beforeUpdate = DateTime.UtcNow; - - // Act - var result = await _service.UpdateAsync(property); - - // Assert - Assert.Equal("800 Updated St", result.Address); - Assert.Equal(_testUserId, result.LastModifiedBy); - Assert.NotNull(result.LastModifiedOn); - Assert.True(result.LastModifiedOn >= beforeUpdate); - } - - [Fact] - public async Task UpdateAsync_SetsLastModifiedFields() - { - // Arrange - var property = new Property - { - Id = Guid.NewGuid(), - OrganizationId = _testOrgId, - Address = "900 Test St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Properties.Add(property); - await _context.SaveChangesAsync(); - - property.MonthlyRent = 1500m; - var beforeUpdate = DateTime.UtcNow; - - // Act - var result = await _service.UpdateAsync(property); - - // Assert - Assert.Equal(_testUserId, result.LastModifiedBy); - Assert.NotNull(result.LastModifiedOn); - Assert.True(result.LastModifiedOn >= beforeUpdate); - Assert.True(result.LastModifiedOn <= DateTime.UtcNow); - } - - [Fact] - public async Task UpdateAsync_NonExistentEntity_ThrowsException() - { - // Arrange - var property = new Property - { - Id = Guid.NewGuid(), // Not in database - OrganizationId = _testOrgId, - Address = "1000 Test St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House" - }; - - // Act & Assert - await Assert.ThrowsAsync(() => _service.UpdateAsync(property)); - } - - [Fact] - public async Task UpdateAsync_DifferentOrganization_ThrowsUnauthorizedException() - { - // Arrange - var differentOrgId = Guid.NewGuid(); - var differentOrg = new Organization - { - Id = differentOrgId, - Name = "Different Org", - OwnerId = _testUserId, - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Organizations.Add(differentOrg); - - var property = new Property - { - Id = Guid.NewGuid(), - OrganizationId = differentOrgId, - Address = "1100 Test St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Properties.Add(property); - await _context.SaveChangesAsync(); - - property.Address = "1100 Updated St"; - - // Act & Assert - await Assert.ThrowsAsync(() => _service.UpdateAsync(property)); - } - - [Fact] - public async Task UpdateAsync_PreventsOrganizationHijacking() - { - // Arrange - var property = new Property - { - Id = Guid.NewGuid(), - OrganizationId = _testOrgId, - Address = "1200 Test St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Properties.Add(property); - await _context.SaveChangesAsync(); - - // Detach the entity so we can simulate an external update attempt - _context.Entry(property).State = Microsoft.EntityFrameworkCore.EntityState.Detached; - - // Attempt to change organization on a new instance - var updatedProperty = new Property - { - Id = property.Id, - OrganizationId = Guid.NewGuid(), // Try to hijack - Address = "1200 Updated St", // Also update something else - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = property.CreatedOn - }; - - // Act - var result = await _service.UpdateAsync(updatedProperty); - - // Assert - OrganizationId should be preserved as original - Assert.Equal(_testOrgId, result.OrganizationId); - Assert.Equal("1200 Updated St", result.Address); // Other changes should apply - } - - #endregion - - #region DeleteAsync Tests - - [Fact] - public async Task DeleteAsync_SoftDeleteEnabled_SoftDeletesEntity() - { - // Arrange - var property = new Property - { - Id = Guid.NewGuid(), - OrganizationId = _testOrgId, - Address = "1300 Test St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Properties.Add(property); - await _context.SaveChangesAsync(); - - // Act - var result = await _service.DeleteAsync(property.Id); - - // Assert - Assert.True(result); - var deletedEntity = await _context.Properties.FindAsync(property.Id); - Assert.NotNull(deletedEntity); - Assert.True(deletedEntity!.IsDeleted); - Assert.Equal(_testUserId, deletedEntity.LastModifiedBy); - Assert.NotNull(deletedEntity.LastModifiedOn); - } - - [Fact] - public async Task DeleteAsync_NonExistentEntity_ReturnsFalse() - { - // Arrange - var nonExistentId = Guid.NewGuid(); - - // Act - var result = await _service.DeleteAsync(nonExistentId); - - // Assert - Assert.False(result); - } - - [Fact] - public async Task DeleteAsync_DifferentOrganization_ThrowsUnauthorizedException() - { - // Arrange - var differentOrgId = Guid.NewGuid(); - var differentOrg = new Organization - { - Id = differentOrgId, - Name = "Different Org", - OwnerId = _testUserId, - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Organizations.Add(differentOrg); - - var property = new Property - { - Id = Guid.NewGuid(), - OrganizationId = differentOrgId, - Address = "1400 Test St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House", - CreatedBy = _testUserId, - CreatedOn = DateTime.UtcNow - }; - _context.Properties.Add(property); - await _context.SaveChangesAsync(); - - // Act & Assert - await Assert.ThrowsAsync(() => _service.DeleteAsync(property.Id)); - } - - #endregion - - #region Security & Authorization Tests - - [Fact] - public async Task CreateAsync_UnauthenticatedUser_ThrowsUnauthorizedException() - { - // Arrange - Create service with no authenticated user - var mockAuth = new Mock(); - var claims = new ClaimsPrincipal(new ClaimsIdentity()); // Not authenticated - mockAuth.Setup(a => a.GetAuthenticationStateAsync()) - .ReturnsAsync(new AuthenticationState(claims)); - - var mockUserStore = new Mock>(); - var mockUserManager = new Mock>( - mockUserStore.Object, null, null, null, null, null, null, null, null); - mockUserManager.Setup(u => u.FindByIdAsync(It.IsAny())) - .ReturnsAsync((ApplicationUser?)null); - - var serviceProvider = new Mock(); - var unauthorizedUserContext = new UserContextService(mockAuth.Object, mockUserManager.Object, serviceProvider.Object); - - var mockSettings = Options.Create(new ApplicationSettings { SoftDeleteEnabled = true }); - var mockLogger = new Mock>(); - var unauthorizedService = new TestPropertyService(_context, mockLogger.Object, unauthorizedUserContext, mockSettings); - - var property = new Property - { - Address = "1500 Test St", - City = "Test City", - State = "TS", - ZipCode = "12345", - PropertyType = "House" - }; - - // Act & Assert - await Assert.ThrowsAsync(() => unauthorizedService.CreateAsync(property)); - } - - #endregion - - /// - /// Test implementation of BaseService using Property entity for testing purposes. - /// - public class TestPropertyService : BaseService - { - public TestPropertyService( - ApplicationDbContext context, - ILogger logger, - UserContextService userContext, - IOptions settings) - : base(context, logger, userContext, settings) - { - } - } - } -} diff --git a/Aquiis.SimpleStart.UI.Tests/Aquiis.SimpleStart.UI.Tests.csproj b/Aquiis.SimpleStart.UI.Tests/Aquiis.SimpleStart.UI.Tests.csproj deleted file mode 100644 index e2be57f..0000000 --- a/Aquiis.SimpleStart.UI.Tests/Aquiis.SimpleStart.UI.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net9.0 - latest - enable - enable - false - - - - - - - - - - - - - - - - - - - - diff --git a/Aquiis.SimpleStart.UI.Tests/NewSetupUITests.cs b/Aquiis.SimpleStart.UI.Tests/NewSetupUITests.cs deleted file mode 100644 index 3fb8327..0000000 --- a/Aquiis.SimpleStart.UI.Tests/NewSetupUITests.cs +++ /dev/null @@ -1,406 +0,0 @@ -using Microsoft.Playwright.NUnit; -using Microsoft.Playwright; - -namespace Aquiis.SimpleStart.UI.Tests; - -[Parallelizable(ParallelScope.Self)] -[TestFixture] - -public class NewSetupUITests : PageTest -{ - - private const string BaseUrl = "http://localhost:5197"; - private const int KeepBrowserOpenSeconds = 30; // Set to 0 to close immediately - - public override BrowserNewContextOptions ContextOptions() - { - return new BrowserNewContextOptions - { - IgnoreHTTPSErrors = true, - BaseURL = BaseUrl, - RecordVideoDir = Path.Combine(Directory.GetCurrentDirectory(), "test-videos"), - RecordVideoSize = new RecordVideoSize { Width = 1280, Height = 720 } - }; - } - - [Test, Order(1)] - public async Task CreateNewAccount() - { - await Page.GotoAsync("http://localhost:5197/"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Create Account" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).FillAsync("Aquiis"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Organization Name" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password", Exact = true }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Confirm Password" }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Register" }).ClickAsync(); - - await Page.WaitForSelectorAsync("h1:has-text('Register confirmation')"); - - // await Page.GetByRole(AriaRole.Heading, new() { Name = "Register confirmation" }).ClickAsync(); - await Page.GetByRole(AriaRole.Link, new() { Name = "Click here to confirm your account" }).ClickAsync(); - - await Page.WaitForSelectorAsync("h1:has-text('Confirm email')"); - - // await Page.GetByText("Thank you for confirming your").ClickAsync(); - await Page.GetByRole(AriaRole.Link, new() { Name = "Home" }).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); - - await Page.WaitForSelectorAsync("h1:has-text('Log in')"); - - // await Page.GetByRole(AriaRole.Heading, new() { Name = "Log in", Exact = true }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); - - await Page.WaitForSelectorAsync("text=Dashboard"); - - await Page.GetByRole(AriaRole.Heading, new() { Name = "Property Management Dashboard", Exact= true }).ClickAsync(); - - - } - - [Test, Order(2)] - public async Task AddProperty() - { - await Page.GotoAsync("http://localhost:5197/"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); - - // Wait for login to complete - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - await Page.WaitForSelectorAsync("h1:has-text('Property Management Dashboard')"); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Properties" }).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = "Add Property" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).FillAsync("369 Crescent Drive"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Apt 2B, Unit" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).FillAsync("New Orleans"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).PressAsync("Tab"); - await Page.Locator("select[name=\"propertyModel.State\"]").SelectOptionAsync(new[] { "LA" }); - await Page.Locator("select[name=\"propertyModel.State\"]").PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("70119"); - await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); - await Page.GetByPlaceholder("0.00").ClickAsync(); - await Page.GetByPlaceholder("0.00").FillAsync("1800"); - await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").ClickAsync(); - await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").FillAsync("4"); - await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").FillAsync("4.5"); - await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").FillAsync("2500"); - await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").PressAsync("Tab"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); - - // Verify property was created successfully - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - await Expect(Page.GetByText("369 Crescent Drive").First).ToBeVisibleAsync(); - - await Page.GetByRole(AriaRole.Button, new() { Name = "Add Property" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).FillAsync("354 Maple Avenue"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter property address" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Apt 2B, Unit" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).FillAsync("Los Angeles"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "City" }).PressAsync("Tab"); - await Page.Locator("select[name=\"propertyModel.State\"]").SelectOptionAsync(new[] { "CA" }); - await Page.Locator("select[name=\"propertyModel.State\"]").PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("90210"); - await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); - await Page.GetByPlaceholder("0.00").ClickAsync(); - await Page.GetByPlaceholder("0.00").FillAsync("4900"); - await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").ClickAsync(); - await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").FillAsync("4"); - await Page.Locator("input[name=\"propertyModel.Bedrooms\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").FillAsync("4.5"); - await Page.Locator("input[name=\"propertyModel.Bathrooms\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").FillAsync("3200"); - await Page.Locator("input[name=\"propertyModel.SquareFeet\"]").PressAsync("Tab"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); - - // Verify property was created successfully - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - await Expect(Page.GetByText("354 Maple Avenue").First).ToBeVisibleAsync(); - } - - [Test, Order(3)] - public async Task AddProspect() - { - await Page.GotoAsync("http://localhost:5197/"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Add New Prospect" }).ClickAsync(); - - await Page.Locator("input[name=\"newProspect.FirstName\"]").ClickAsync(); - await Page.Locator("input[name=\"newProspect.FirstName\"]").FillAsync("Mya"); - await Page.Locator("input[name=\"newProspect.FirstName\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"newProspect.LastName\"]").ClickAsync(); - await Page.Locator("input[name=\"newProspect.LastName\"]").FillAsync("Smith"); - await Page.Locator("input[name=\"newProspect.LastName\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"newProspect.Email\"]").FillAsync("mya@gmail.com"); - await Page.Locator("input[name=\"newProspect.Email\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"newProspect.Phone\"]").FillAsync("504-234-3600"); - await Page.Locator("input[name=\"newProspect.Phone\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"newProspect.DateOfBirth\"]").FillAsync("1993-09-29"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Driver's License #" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., Driver's License #" }).FillAsync("12345678"); - await Page.Locator("select[name=\"newProspect.IdentificationState\"]").SelectOptionAsync(new[] { "LA" }); - await Page.Locator("select[name=\"newProspect.Source\"]").SelectOptionAsync(new[] { "Zillow" }); - await Page.Locator("select[name=\"newProspect.InterestedPropertyId\"]").SelectOptionAsync(new[] { "354 Maple Avenue" }); - await Page.Locator("input[name=\"newProspect.DesiredMoveInDate\"]").FillAsync("2026-01-01"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Save Prospect" }).ClickAsync(); - - // Verify property was created successfully - await Page.WaitForSelectorAsync("h1:has-text('Prospective Tenants')"); - await Expect(Page.GetByText("Mya Smith").First).ToBeVisibleAsync(); - } - - [Test, Order(4)] - public async Task ScheduleAndCompleteTour() - { - await Page.GotoAsync("http://localhost:5197/"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); - await Page.GetByTitle("Schedule Tour").ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = "Schedule Tour" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = "Complete Tour", Exact = true }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Continue Editing" }).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).First.ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(1).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(2).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(3).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(4).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Check All" }).Nth(5).ClickAsync(); - - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).First.ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Enter value" }).First.FillAsync("1800"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., $" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "e.g., $" }).FillAsync("1800"); - - await Page.Locator("div:nth-child(10) > .card-header > .btn").ClickAsync(); - await Page.Locator("div:nth-child(11) > .card-header > .btn").ClickAsync(); - - await Page.GetByText("Interested", new() { Exact = true }).ClickAsync(); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Save Progress" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Mark as Complete" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Generate PDF" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - var page1 = await Page.RunAndWaitForPopupAsync(async () => - { - await Page.GetByRole(AriaRole.Button, new() { Name = " View PDF" }).ClickAsync(); - }); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); - } - - [Test, Order(5)] - public async Task SubmitApplication() - { - await Page.GotoAsync("http://localhost:5197/"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Apply" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).FillAsync("123 Main Street"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Main St" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Los Angeles" }).FillAsync("Los Angeles"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Los Angeles" }).PressAsync("Tab"); - await Page.Locator("select[name=\"applicationModel.CurrentState\"]").SelectOptionAsync(new[] { "CA" }); - await Page.Locator("select[name=\"applicationModel.CurrentState\"]").PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "90210" }).FillAsync("90210"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "90210" }).PressAsync("Tab"); - await Page.Locator("input[name=\"applicationModel.CurrentRent\"]").FillAsync("1500"); - await Page.Locator("input[name=\"applicationModel.CurrentRent\"]").PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "John Smith" }).FillAsync("John Smith"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "John Smith" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "(555) 123-" }).FillAsync("555-123-4567"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "(555) 123-" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("CapsLock"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).FillAsync("ABC"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("CapsLock"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).FillAsync("ABC Company"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "ABC Company" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Software Engineer" }).FillAsync("Software Engineer"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Software Engineer" }).PressAsync("Tab"); - await Page.Locator("input[name=\"applicationModel.MonthlyIncome\"]").FillAsync("9600"); - await Page.Locator("input[name=\"applicationModel.MonthlyIncome\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"applicationModel.EmploymentLengthMonths\"]").FillAsync("15"); - await Page.Locator("input[name=\"applicationModel.EmploymentLengthMonths\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"applicationModel.Reference1Name\"]").FillAsync("Richard"); - await Page.Locator("input[name=\"applicationModel.Reference1Name\"]").PressAsync("Tab"); - await Page.Locator("input[name=\"applicationModel.Reference1Phone\"]").FillAsync("Zachary"); - await Page.Locator("input[name=\"applicationModel.Reference1Phone\"]").PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Friend, Coworker, etc." }).FillAsync("Spouse"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Submit Application" }).ClickAsync(); - - // Verify property was created successfully - await Expect(Page.GetByText("Application submitted successfully")).ToBeVisibleAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.WaitForSelectorAsync("h1:has-text('Prospective Tenants')"); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); - } - - [Test, Order(6)] - public async Task ApproveApplication() - { - await Page.GotoAsync("http://localhost:5197/"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByTitle("View Details").ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " View Application" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Collect Application Fee" }).ClickAsync(); - await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new[] { "Online Payment" }); - await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Payment" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Initiate Screening" }).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Pass" }).First.ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Pass" }).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Pass" }).Nth(1).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = " Confirm Pass" }).ClickAsync(); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Approve Application" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); - await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); - } - - [Test] - public async Task GenerateLeaseOfferAndConvertToLease() - { - await Page.GotoAsync("http://localhost:5197/"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).ClickAsync(); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); - await Page.GetByRole(AriaRole.Textbox, new() { Name = "Password" }).FillAsync("Today123"); - await Page.GetByRole(AriaRole.Button, new() { Name = "Log in" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Prospects" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByTitle("View Details").ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " View Application" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Generate Lease Offer" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Generate Lease Offer" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = " Accept Offer (Convert to Lease" }).ClickAsync(); - - await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new[] { "Online Payment" }); - await Page.GetByRole(AriaRole.Button, new() { Name = " Accept & Create Lease" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Link, new() { Name = "Dashboard" }).ClickAsync(); - - await Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - - await Page.GetByRole(AriaRole.Button, new() { Name = "Logout" }).ClickAsync(); - } - -} \ No newline at end of file diff --git a/Aquiis.SimpleStart/Application/Services/ApplicationService.cs b/Aquiis.SimpleStart/Application/Services/ApplicationService.cs deleted file mode 100644 index cab5367..0000000 --- a/Aquiis.SimpleStart/Application/Services/ApplicationService.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Microsoft.Extensions.Options; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Constants; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/CalendarEventService.cs b/Aquiis.SimpleStart/Application/Services/CalendarEventService.cs deleted file mode 100644 index 432fd23..0000000 --- a/Aquiis.SimpleStart/Application/Services/CalendarEventService.cs +++ /dev/null @@ -1,260 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces; -using Aquiis.SimpleStart.Shared.Services; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/CalendarSettingsService.cs b/Aquiis.SimpleStart/Application/Services/CalendarSettingsService.cs deleted file mode 100644 index 8780c58..0000000 --- a/Aquiis.SimpleStart/Application/Services/CalendarSettingsService.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Utilities; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/ChecklistService.cs b/Aquiis.SimpleStart/Application/Services/ChecklistService.cs deleted file mode 100644 index a46be58..0000000 --- a/Aquiis.SimpleStart/Application/Services/ChecklistService.cs +++ /dev/null @@ -1,654 +0,0 @@ -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Core.Constants; -using Microsoft.EntityFrameworkCore; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/DocumentService.cs b/Aquiis.SimpleStart/Application/Services/DocumentService.cs deleted file mode 100644 index 351227d..0000000 --- a/Aquiis.SimpleStart/Application/Services/DocumentService.cs +++ /dev/null @@ -1,432 +0,0 @@ -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Application.Services.PdfGenerators; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.ComponentModel.DataAnnotations; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/EmailSettingsService.cs b/Aquiis.SimpleStart/Application/Services/EmailSettingsService.cs deleted file mode 100644 index c6c559b..0000000 --- a/Aquiis.SimpleStart/Application/Services/EmailSettingsService.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces.Services; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Infrastructure.Services; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using SendGrid; -using SendGrid.Helpers.Mail; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/FinancialReportService.cs b/Aquiis.SimpleStart/Application/Services/FinancialReportService.cs deleted file mode 100644 index 2d8d038..0000000 --- a/Aquiis.SimpleStart/Application/Services/FinancialReportService.cs +++ /dev/null @@ -1,287 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/InspectionService.cs b/Aquiis.SimpleStart/Application/Services/InspectionService.cs deleted file mode 100644 index c3f4270..0000000 --- a/Aquiis.SimpleStart/Application/Services/InspectionService.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/InvoiceService.cs b/Aquiis.SimpleStart/Application/Services/InvoiceService.cs deleted file mode 100644 index 19811b6..0000000 --- a/Aquiis.SimpleStart/Application/Services/InvoiceService.cs +++ /dev/null @@ -1,465 +0,0 @@ -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.ComponentModel.DataAnnotations; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/LeaseOfferService.cs b/Aquiis.SimpleStart/Application/Services/LeaseOfferService.cs deleted file mode 100644 index 6ad41ff..0000000 --- a/Aquiis.SimpleStart/Application/Services/LeaseOfferService.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/LeaseService.cs b/Aquiis.SimpleStart/Application/Services/LeaseService.cs deleted file mode 100644 index 639a8ca..0000000 --- a/Aquiis.SimpleStart/Application/Services/LeaseService.cs +++ /dev/null @@ -1,492 +0,0 @@ -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.ComponentModel.DataAnnotations; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/MaintenanceService.cs b/Aquiis.SimpleStart/Application/Services/MaintenanceService.cs deleted file mode 100644 index f3351ec..0000000 --- a/Aquiis.SimpleStart/Application/Services/MaintenanceService.cs +++ /dev/null @@ -1,492 +0,0 @@ -using Aquiis.SimpleStart.Application.Services.Workflows; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; -using System.ComponentModel.DataAnnotations; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/NoteService.cs b/Aquiis.SimpleStart/Application/Services/NoteService.cs deleted file mode 100644 index 7bb68e6..0000000 --- a/Aquiis.SimpleStart/Application/Services/NoteService.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/NotificationService.cs b/Aquiis.SimpleStart/Application/Services/NotificationService.cs deleted file mode 100644 index 57000c0..0000000 --- a/Aquiis.SimpleStart/Application/Services/NotificationService.cs +++ /dev/null @@ -1,224 +0,0 @@ - -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces.Services; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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 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.SimpleStart/Application/Services/OrganizationService.cs b/Aquiis.SimpleStart/Application/Services/OrganizationService.cs deleted file mode 100644 index 21788e7..0000000 --- a/Aquiis.SimpleStart/Application/Services/OrganizationService.cs +++ /dev/null @@ -1,495 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Shared; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PaymentService.cs b/Aquiis.SimpleStart/Application/Services/PaymentService.cs deleted file mode 100644 index 4f83899..0000000 --- a/Aquiis.SimpleStart/Application/Services/PaymentService.cs +++ /dev/null @@ -1,410 +0,0 @@ -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.ComponentModel.DataAnnotations; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PdfGenerators/ChecklistPdfGenerator.cs b/Aquiis.SimpleStart/Application/Services/PdfGenerators/ChecklistPdfGenerator.cs deleted file mode 100644 index 07e7ec4..0000000 --- a/Aquiis.SimpleStart/Application/Services/PdfGenerators/ChecklistPdfGenerator.cs +++ /dev/null @@ -1,248 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using QuestPDF.Drawing; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs b/Aquiis.SimpleStart/Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs deleted file mode 100644 index 4520e04..0000000 --- a/Aquiis.SimpleStart/Application/Services/PdfGenerators/FinancialReportPdfGenerator.cs +++ /dev/null @@ -1,453 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PdfGenerators/InspectionPdfGenerator.cs b/Aquiis.SimpleStart/Application/Services/PdfGenerators/InspectionPdfGenerator.cs deleted file mode 100644 index 067600c..0000000 --- a/Aquiis.SimpleStart/Application/Services/PdfGenerators/InspectionPdfGenerator.cs +++ /dev/null @@ -1,362 +0,0 @@ -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PdfGenerators/InvoicePdfGenerator.cs b/Aquiis.SimpleStart/Application/Services/PdfGenerators/InvoicePdfGenerator.cs deleted file mode 100644 index b837a8f..0000000 --- a/Aquiis.SimpleStart/Application/Services/PdfGenerators/InvoicePdfGenerator.cs +++ /dev/null @@ -1,244 +0,0 @@ -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PdfGenerators/LeasePdfGenerator.cs b/Aquiis.SimpleStart/Application/Services/PdfGenerators/LeasePdfGenerator.cs deleted file mode 100644 index c66d331..0000000 --- a/Aquiis.SimpleStart/Application/Services/PdfGenerators/LeasePdfGenerator.cs +++ /dev/null @@ -1,262 +0,0 @@ -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs b/Aquiis.SimpleStart/Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs deleted file mode 100644 index fc7a100..0000000 --- a/Aquiis.SimpleStart/Application/Services/PdfGenerators/LeaseRenewalPdfGenerator.cs +++ /dev/null @@ -1,238 +0,0 @@ -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using Aquiis.SimpleStart.Core.Entities; -using PdfDocument = QuestPDF.Fluent.Document; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PdfGenerators/PaymentPdfGenerator.cs b/Aquiis.SimpleStart/Application/Services/PdfGenerators/PaymentPdfGenerator.cs deleted file mode 100644 index 01d8fea..0000000 --- a/Aquiis.SimpleStart/Application/Services/PdfGenerators/PaymentPdfGenerator.cs +++ /dev/null @@ -1,256 +0,0 @@ -using QuestPDF.Fluent; -using QuestPDF.Helpers; -using QuestPDF.Infrastructure; -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PropertyManagementService.cs b/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs deleted file mode 100644 index 8cb63f0..0000000 --- a/Aquiis.SimpleStart/Application/Services/PropertyManagementService.cs +++ /dev/null @@ -1,2677 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Shared.Services; -using System.Security.Claims; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/PropertyService.cs b/Aquiis.SimpleStart/Application/Services/PropertyService.cs deleted file mode 100644 index e96901f..0000000 --- a/Aquiis.SimpleStart/Application/Services/PropertyService.cs +++ /dev/null @@ -1,382 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/ProspectiveTenantService.cs b/Aquiis.SimpleStart/Application/Services/ProspectiveTenantService.cs deleted file mode 100644 index 094ae52..0000000 --- a/Aquiis.SimpleStart/Application/Services/ProspectiveTenantService.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/RentalApplicationService.cs b/Aquiis.SimpleStart/Application/Services/RentalApplicationService.cs deleted file mode 100644 index e37007e..0000000 --- a/Aquiis.SimpleStart/Application/Services/RentalApplicationService.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/SMSSettingsService.cs b/Aquiis.SimpleStart/Application/Services/SMSSettingsService.cs deleted file mode 100644 index b3efc6d..0000000 --- a/Aquiis.SimpleStart/Application/Services/SMSSettingsService.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Infrastructure.Services; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/ScheduledTaskService.cs b/Aquiis.SimpleStart/Application/Services/ScheduledTaskService.cs deleted file mode 100644 index 3aa7ef8..0000000 --- a/Aquiis.SimpleStart/Application/Services/ScheduledTaskService.cs +++ /dev/null @@ -1,802 +0,0 @@ -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Application.Services.Workflows; -using Microsoft.EntityFrameworkCore; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/SchemaValidationService.cs b/Aquiis.SimpleStart/Application/Services/SchemaValidationService.cs deleted file mode 100644 index 3746220..0000000 --- a/Aquiis.SimpleStart/Application/Services/SchemaValidationService.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Constants; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/ScreeningService.cs b/Aquiis.SimpleStart/Application/Services/ScreeningService.cs deleted file mode 100644 index 409fb66..0000000 --- a/Aquiis.SimpleStart/Application/Services/ScreeningService.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/SecurityDepositService.cs b/Aquiis.SimpleStart/Application/Services/SecurityDepositService.cs deleted file mode 100644 index 151ce6f..0000000 --- a/Aquiis.SimpleStart/Application/Services/SecurityDepositService.cs +++ /dev/null @@ -1,741 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Shared.Services; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/TenantConversionService.cs b/Aquiis.SimpleStart/Application/Services/TenantConversionService.cs deleted file mode 100644 index c267bac..0000000 --- a/Aquiis.SimpleStart/Application/Services/TenantConversionService.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; - - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/TenantService.cs b/Aquiis.SimpleStart/Application/Services/TenantService.cs deleted file mode 100644 index 20f705d..0000000 --- a/Aquiis.SimpleStart/Application/Services/TenantService.cs +++ /dev/null @@ -1,418 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/TourService.cs b/Aquiis.SimpleStart/Application/Services/TourService.cs deleted file mode 100644 index d126cfb..0000000 --- a/Aquiis.SimpleStart/Application/Services/TourService.cs +++ /dev/null @@ -1,490 +0,0 @@ -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces; -using Aquiis.SimpleStart.Core.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/Workflows/AccountWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/AccountWorkflowService.cs deleted file mode 100644 index 5be5415..0000000 --- a/Aquiis.SimpleStart/Application/Services/Workflows/AccountWorkflowService.cs +++ /dev/null @@ -1,40 +0,0 @@ - -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Application.Services; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Infrastructure.Data; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs deleted file mode 100644 index 7be3ab5..0000000 --- a/Aquiis.SimpleStart/Application/Services/Workflows/ApplicationWorkflowService.cs +++ /dev/null @@ -1,1277 +0,0 @@ -using Aquiis.SimpleStart.Application.Services.Workflows; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs deleted file mode 100644 index 669f696..0000000 --- a/Aquiis.SimpleStart/Application/Services/Workflows/BaseWorkflowService.cs +++ /dev/null @@ -1,208 +0,0 @@ -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using System.Text.Json; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/Workflows/IWorkflowState.cs b/Aquiis.SimpleStart/Application/Services/Workflows/IWorkflowState.cs deleted file mode 100644 index c505166..0000000 --- a/Aquiis.SimpleStart/Application/Services/Workflows/IWorkflowState.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/Workflows/LeaseWorkflowService.cs b/Aquiis.SimpleStart/Application/Services/Workflows/LeaseWorkflowService.cs deleted file mode 100644 index d635913..0000000 --- a/Aquiis.SimpleStart/Application/Services/Workflows/LeaseWorkflowService.cs +++ /dev/null @@ -1,848 +0,0 @@ -using Aquiis.SimpleStart.Application.Services.Workflows; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs b/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs deleted file mode 100644 index 07b3c8e..0000000 --- a/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowAuditLog.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart/Application/Services/Workflows/WorkflowResult.cs b/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowResult.cs deleted file mode 100644 index 4ab6dcf..0000000 --- a/Aquiis.SimpleStart/Application/Services/Workflows/WorkflowResult.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace Aquiis.SimpleStart.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.SimpleStart/Aquiis.SimpleStart.csproj b/Aquiis.SimpleStart/Aquiis.SimpleStart.csproj deleted file mode 100644 index 4657822..0000000 --- a/Aquiis.SimpleStart/Aquiis.SimpleStart.csproj +++ /dev/null @@ -1,50 +0,0 @@ - - - - net9.0 - enable - enable - aspnet-Aquiis.SimpleStart-c69b6efe-bb20-41de-8cba-044207ebdce1 - 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.SimpleStart/Core/Constants/ApplicationConstants.cs b/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs deleted file mode 100644 index 4fb9eb2..0000000 --- a/Aquiis.SimpleStart/Core/Constants/ApplicationConstants.cs +++ /dev/null @@ -1,789 +0,0 @@ -using System.Security.Cryptography.X509Certificates; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Constants/ApplicationSettings.cs b/Aquiis.SimpleStart/Core/Constants/ApplicationSettings.cs deleted file mode 100644 index b09183a..0000000 --- a/Aquiis.SimpleStart/Core/Constants/ApplicationSettings.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Constants/EntityTypeNames.cs b/Aquiis.SimpleStart/Core/Constants/EntityTypeNames.cs deleted file mode 100644 index 53c1588..0000000 --- a/Aquiis.SimpleStart/Core/Constants/EntityTypeNames.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Property"; - public const string Tenant = "Aquiis.SimpleStart.Core.Entities.Tenant"; - public const string Lease = "Aquiis.SimpleStart.Core.Entities.Lease"; - public const string LeaseOffer = "Aquiis.SimpleStart.Core.Entities.LeaseOffer"; - public const string Invoice = "Aquiis.SimpleStart.Core.Entities.Invoice"; - public const string Payment = "Aquiis.SimpleStart.Core.Entities.Payment"; - public const string MaintenanceRequest = "Aquiis.SimpleStart.Core.Entities.MaintenanceRequest"; - public const string Inspection = "Aquiis.SimpleStart.Core.Entities.Inspection"; - public const string Document = "Aquiis.SimpleStart.Core.Entities.Document"; - - // Application/Prospect Domain - public const string ProspectiveTenant = "Aquiis.SimpleStart.Core.Entities.ProspectiveTenant"; - public const string Application = "Aquiis.SimpleStart.Core.Entities.Application"; - public const string Tour = "Aquiis.SimpleStart.Core.Entities.Tour"; - - // Checklist Domain - public const string Checklist = "Aquiis.SimpleStart.Core.Entities.Checklist"; - public const string ChecklistTemplate = "Aquiis.SimpleStart.Core.Entities.ChecklistTemplate"; - - // Calendar/Events - public const string CalendarEvent = "Aquiis.SimpleStart.Core.Entities.CalendarEvent"; - - // Security Deposits - public const string SecurityDepositPool = "Aquiis.SimpleStart.Core.Entities.SecurityDepositPool"; - public const string SecurityDepositTransaction = "Aquiis.SimpleStart.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.SimpleStart/Core/Constants/NotificationConstants.cs b/Aquiis.SimpleStart/Core/Constants/NotificationConstants.cs deleted file mode 100644 index 1df5510..0000000 --- a/Aquiis.SimpleStart/Core/Constants/NotificationConstants.cs +++ /dev/null @@ -1,58 +0,0 @@ -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 Lease = "Lease"; - public const string Payment = "Payment"; - public const string Maintenance = "Maintenance"; - public const string Application = "Application"; - public const string Property = "Property"; - public const string Inspection = "Inspection"; - public const string Document = "Document"; - 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.SimpleStart/Core/Entities/ApplicationScreening.cs b/Aquiis.SimpleStart/Core/Entities/ApplicationScreening.cs deleted file mode 100644 index 3d1d666..0000000 --- a/Aquiis.SimpleStart/Core/Entities/ApplicationScreening.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/BaseModel.cs b/Aquiis.SimpleStart/Core/Entities/BaseModel.cs deleted file mode 100644 index afd11a5..0000000 --- a/Aquiis.SimpleStart/Core/Entities/BaseModel.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; -using Aquiis.SimpleStart.Core.Interfaces; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/CalendarEvent.cs b/Aquiis.SimpleStart/Core/Entities/CalendarEvent.cs deleted file mode 100644 index 3b60c09..0000000 --- a/Aquiis.SimpleStart/Core/Entities/CalendarEvent.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/CalendarEventTypes.cs b/Aquiis.SimpleStart/Core/Entities/CalendarEventTypes.cs deleted file mode 100644 index 5305042..0000000 --- a/Aquiis.SimpleStart/Core/Entities/CalendarEventTypes.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/CalendarSettings.cs b/Aquiis.SimpleStart/Core/Entities/CalendarSettings.cs deleted file mode 100644 index e9cc02a..0000000 --- a/Aquiis.SimpleStart/Core/Entities/CalendarSettings.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Checklist.cs b/Aquiis.SimpleStart/Core/Entities/Checklist.cs deleted file mode 100644 index ba8970e..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Checklist.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/ChecklistItem.cs b/Aquiis.SimpleStart/Core/Entities/ChecklistItem.cs deleted file mode 100644 index 1615727..0000000 --- a/Aquiis.SimpleStart/Core/Entities/ChecklistItem.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/ChecklistTemplate.cs b/Aquiis.SimpleStart/Core/Entities/ChecklistTemplate.cs deleted file mode 100644 index b427376..0000000 --- a/Aquiis.SimpleStart/Core/Entities/ChecklistTemplate.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/ChecklistTemplateItem.cs b/Aquiis.SimpleStart/Core/Entities/ChecklistTemplateItem.cs deleted file mode 100644 index 8d95ff6..0000000 --- a/Aquiis.SimpleStart/Core/Entities/ChecklistTemplateItem.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Document.cs b/Aquiis.SimpleStart/Core/Entities/Document.cs deleted file mode 100644 index e270a35..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Document.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/ISchedulableEntity.cs b/Aquiis.SimpleStart/Core/Entities/ISchedulableEntity.cs deleted file mode 100644 index f3a9d6b..0000000 --- a/Aquiis.SimpleStart/Core/Entities/ISchedulableEntity.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/IncomeStatement.cs b/Aquiis.SimpleStart/Core/Entities/IncomeStatement.cs deleted file mode 100644 index 9047867..0000000 --- a/Aquiis.SimpleStart/Core/Entities/IncomeStatement.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Inspection.cs b/Aquiis.SimpleStart/Core/Entities/Inspection.cs deleted file mode 100644 index 2b47629..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Inspection.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Invoice.cs b/Aquiis.SimpleStart/Core/Entities/Invoice.cs deleted file mode 100644 index 65ba985..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Invoice.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Lease.cs b/Aquiis.SimpleStart/Core/Entities/Lease.cs deleted file mode 100644 index 8a74319..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Lease.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/LeaseOffer.cs b/Aquiis.SimpleStart/Core/Entities/LeaseOffer.cs deleted file mode 100644 index c6f424e..0000000 --- a/Aquiis.SimpleStart/Core/Entities/LeaseOffer.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/MaintenanceRequest.cs b/Aquiis.SimpleStart/Core/Entities/MaintenanceRequest.cs deleted file mode 100644 index da3ff1c..0000000 --- a/Aquiis.SimpleStart/Core/Entities/MaintenanceRequest.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Note.cs b/Aquiis.SimpleStart/Core/Entities/Note.cs deleted file mode 100644 index a1d74b9..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Note.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Shared.Components.Account; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Notification.cs b/Aquiis.SimpleStart/Core/Entities/Notification.cs deleted file mode 100644 index 4f8bc95..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Notification.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.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.SimpleStart/Core/Entities/NotificationPreferences.cs b/Aquiis.SimpleStart/Core/Entities/NotificationPreferences.cs deleted file mode 100644 index 544d644..0000000 --- a/Aquiis.SimpleStart/Core/Entities/NotificationPreferences.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Validation; - -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.SimpleStart/Core/Entities/OperationResult.cs b/Aquiis.SimpleStart/Core/Entities/OperationResult.cs deleted file mode 100644 index 7ef7375..0000000 --- a/Aquiis.SimpleStart/Core/Entities/OperationResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Organization.cs b/Aquiis.SimpleStart/Core/Entities/Organization.cs deleted file mode 100644 index 33d14f6..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Organization.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/OrganizationEmailSettings.cs b/Aquiis.SimpleStart/Core/Entities/OrganizationEmailSettings.cs deleted file mode 100644 index 0db7d2b..0000000 --- a/Aquiis.SimpleStart/Core/Entities/OrganizationEmailSettings.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/OrganizationSMSSettings.cs b/Aquiis.SimpleStart/Core/Entities/OrganizationSMSSettings.cs deleted file mode 100644 index bc71944..0000000 --- a/Aquiis.SimpleStart/Core/Entities/OrganizationSMSSettings.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/OrganizationSettings.cs b/Aquiis.SimpleStart/Core/Entities/OrganizationSettings.cs deleted file mode 100644 index 7f45afc..0000000 --- a/Aquiis.SimpleStart/Core/Entities/OrganizationSettings.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Payment.cs b/Aquiis.SimpleStart/Core/Entities/Payment.cs deleted file mode 100644 index a3403aa..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Payment.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Property.cs b/Aquiis.SimpleStart/Core/Entities/Property.cs deleted file mode 100644 index 2b9136c..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Property.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; -using Aquiis.SimpleStart.Core.Constants; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/ProspectiveTenant.cs b/Aquiis.SimpleStart/Core/Entities/ProspectiveTenant.cs deleted file mode 100644 index b0419ef..0000000 --- a/Aquiis.SimpleStart/Core/Entities/ProspectiveTenant.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/RentalApplication.cs b/Aquiis.SimpleStart/Core/Entities/RentalApplication.cs deleted file mode 100644 index 53066bd..0000000 --- a/Aquiis.SimpleStart/Core/Entities/RentalApplication.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/SchemaVersion.cs b/Aquiis.SimpleStart/Core/Entities/SchemaVersion.cs deleted file mode 100644 index 2804625..0000000 --- a/Aquiis.SimpleStart/Core/Entities/SchemaVersion.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/SecurityDeposit.cs b/Aquiis.SimpleStart/Core/Entities/SecurityDeposit.cs deleted file mode 100644 index 995be90..0000000 --- a/Aquiis.SimpleStart/Core/Entities/SecurityDeposit.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/SecurityDepositDividend.cs b/Aquiis.SimpleStart/Core/Entities/SecurityDepositDividend.cs deleted file mode 100644 index 77f3c38..0000000 --- a/Aquiis.SimpleStart/Core/Entities/SecurityDepositDividend.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/SecurityDepositInvestmentPool.cs b/Aquiis.SimpleStart/Core/Entities/SecurityDepositInvestmentPool.cs deleted file mode 100644 index 393aad9..0000000 --- a/Aquiis.SimpleStart/Core/Entities/SecurityDepositInvestmentPool.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Tenant.cs b/Aquiis.SimpleStart/Core/Entities/Tenant.cs deleted file mode 100644 index 18408e6..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Tenant.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/Tour.cs b/Aquiis.SimpleStart/Core/Entities/Tour.cs deleted file mode 100644 index 2ec1a09..0000000 --- a/Aquiis.SimpleStart/Core/Entities/Tour.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Entities/UserOrganization.cs b/Aquiis.SimpleStart/Core/Entities/UserOrganization.cs deleted file mode 100644 index b8fd311..0000000 --- a/Aquiis.SimpleStart/Core/Entities/UserOrganization.cs +++ /dev/null @@ -1,67 +0,0 @@ - - -using System.ComponentModel.DataAnnotations; -using Aquiis.SimpleStart.Core.Validation; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Interfaces/IAuditable.cs b/Aquiis.SimpleStart/Core/Interfaces/IAuditable.cs deleted file mode 100644 index 3d93b5a..0000000 --- a/Aquiis.SimpleStart/Core/Interfaces/IAuditable.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Interfaces/ICalendarEventService.cs b/Aquiis.SimpleStart/Core/Interfaces/ICalendarEventService.cs deleted file mode 100644 index d65acb0..0000000 --- a/Aquiis.SimpleStart/Core/Interfaces/ICalendarEventService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Interfaces/Services/IEmailService.cs b/Aquiis.SimpleStart/Core/Interfaces/Services/IEmailService.cs deleted file mode 100644 index 6337a99..0000000 --- a/Aquiis.SimpleStart/Core/Interfaces/Services/IEmailService.cs +++ /dev/null @@ -1,9 +0,0 @@ - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Interfaces/Services/ISMSService.cs b/Aquiis.SimpleStart/Core/Interfaces/Services/ISMSService.cs deleted file mode 100644 index a25929b..0000000 --- a/Aquiis.SimpleStart/Core/Interfaces/Services/ISMSService.cs +++ /dev/null @@ -1,7 +0,0 @@ - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Services/BaseService.cs b/Aquiis.SimpleStart/Core/Services/BaseService.cs deleted file mode 100644 index b40504e..0000000 --- a/Aquiis.SimpleStart/Core/Services/BaseService.cs +++ /dev/null @@ -1,394 +0,0 @@ -using System.Linq.Expressions; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Validation/OptionalGuidAttribute.cs b/Aquiis.SimpleStart/Core/Validation/OptionalGuidAttribute.cs deleted file mode 100644 index ac45d87..0000000 --- a/Aquiis.SimpleStart/Core/Validation/OptionalGuidAttribute.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Aquiis.SimpleStart.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.SimpleStart/Core/Validation/RequiredGuidAttribute.cs b/Aquiis.SimpleStart/Core/Validation/RequiredGuidAttribute.cs deleted file mode 100644 index ef64f6a..0000000 --- a/Aquiis.SimpleStart/Core/Validation/RequiredGuidAttribute.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Application/Pages/DailyReport.razor b/Aquiis.SimpleStart/Features/Administration/Application/Pages/DailyReport.razor deleted file mode 100644 index b97a64f..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Application/Pages/DailyReport.razor +++ /dev/null @@ -1,148 +0,0 @@ -@page "/administration/application/dailyreport" - -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Application/Pages/InitializeSchemaVersion.razor b/Aquiis.SimpleStart/Features/Administration/Application/Pages/InitializeSchemaVersion.razor deleted file mode 100644 index 9cf264b..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Application/Pages/InitializeSchemaVersion.razor +++ /dev/null @@ -1,64 +0,0 @@ -@page "/administration/application/initialize-schema" -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Application/Pages/ManageDatabase.razor b/Aquiis.SimpleStart/Features/Administration/Application/Pages/ManageDatabase.razor deleted file mode 100644 index 7e09fb1..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Application/Pages/ManageDatabase.razor +++ /dev/null @@ -1,777 +0,0 @@ -@page "/administration/application/database" -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Organizations/Pages/CreateOrganization.razor b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/CreateOrganization.razor deleted file mode 100644 index 221e93a..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/CreateOrganization.razor +++ /dev/null @@ -1,197 +0,0 @@ -@page "/administration/organizations/create" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor deleted file mode 100644 index 4b98ecb..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/EditOrganization.razor +++ /dev/null @@ -1,296 +0,0 @@ -@page "/administration/organizations/edit/{Id:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor deleted file mode 100644 index 8b31ee1..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor +++ /dev/null @@ -1,454 +0,0 @@ -@page "/administration/organizations/{Id:guid}/users" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Shared.Components.Account -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor deleted file mode 100644 index 1655a67..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/Organizations.razor +++ /dev/null @@ -1,214 +0,0 @@ -@page "/administration/organizations" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Components.Shared -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor b/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor deleted file mode 100644 index 0fceb30..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ViewOrganization.razor +++ /dev/null @@ -1,344 +0,0 @@ -@page "/administration/organizations/view/{Id:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Shared.Components.Account -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor deleted file mode 100644 index 131ec64..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor +++ /dev/null @@ -1,378 +0,0 @@ -@page "/administration/settings/calendar" -@using Aquiis.SimpleStart.Core.Entities -@using CalendarSettingsEntity = Aquiis.SimpleStart.Core.Entities.CalendarSettings -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Settings/Pages/EmailSettings.razor b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/EmailSettings.razor deleted file mode 100644 index d15070a..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/EmailSettings.razor +++ /dev/null @@ -1,454 +0,0 @@ -@page "/administration/settings/email" -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Settings/Pages/LateFeeSettings.razor b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/LateFeeSettings.razor deleted file mode 100644 index 4b1fc2e..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/LateFeeSettings.razor +++ /dev/null @@ -1,439 +0,0 @@ -@page "/administration/settings/latefees" -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor deleted file mode 100644 index 915ab02..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/OrganizationSettings.razor +++ /dev/null @@ -1,566 +0,0 @@ -@page "/administration/settings/organization" - -@using Aquiis.SimpleStart.Core.Entities -@using OrganizationSettingsEntity = Aquiis.SimpleStart.Core.Entities.OrganizationSettings -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Settings/Pages/SMSSettings.razor b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/SMSSettings.razor deleted file mode 100644 index ee97020..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/SMSSettings.razor +++ /dev/null @@ -1,418 +0,0 @@ -@page "/administration/settings/sms" -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor b/Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor deleted file mode 100644 index 0e2cb7e..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor +++ /dev/null @@ -1,464 +0,0 @@ -@page "/administration/settings/services" -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Infrastructure.Data -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Users/Manage.razor b/Aquiis.SimpleStart/Features/Administration/Users/Manage.razor deleted file mode 100644 index 6506adb..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Users/Manage.razor +++ /dev/null @@ -1,612 +0,0 @@ -@page "/administration/users/manage" - -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Components.Shared -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Core.Constants -@using Microsoft.AspNetCore.Identity -@using Microsoft.EntityFrameworkCore -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.Forms -@using Aquiis.SimpleStart.Shared.Components.Account -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Users/Pages/Create.razor b/Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor deleted file mode 100644 index 20724ac..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor +++ /dev/null @@ -1,311 +0,0 @@ -@page "/Administration/Users/Create" - -@using Aquiis.SimpleStart.Shared.Components.Account -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/Administration/Users/View.razor b/Aquiis.SimpleStart/Features/Administration/Users/View.razor deleted file mode 100644 index 78c01b6..0000000 --- a/Aquiis.SimpleStart/Features/Administration/Users/View.razor +++ /dev/null @@ -1,507 +0,0 @@ -@page "/administration/users/view/{UserId}" - -@using Aquiis.SimpleStart.Shared.Components.Shared -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart/Features/Notifications/Pages/NotificationCenter.razor b/Aquiis.SimpleStart/Features/Notifications/Pages/NotificationCenter.razor deleted file mode 100644 index 657747d..0000000 --- a/Aquiis.SimpleStart/Features/Notifications/Pages/NotificationCenter.razor +++ /dev/null @@ -1,470 +0,0 @@ -@page "/notifications" -@using Aquiis.SimpleStart.Infrastructure.Services -@inject NotificationService NotificationService -@inject NavigationManager NavigationManager -@rendermode InteractiveServer -@namespace Aquiis.SimpleStart.Features.Notifications.Pages - -Notification Center - -
-
-

- Notification Center -

-

- Here you can manage your 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 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 deleted file mode 100644 index 99acda5..0000000 --- a/Aquiis.SimpleStart/Features/Notifications/Pages/NotificationPreferences.razor +++ /dev/null @@ -1 +0,0 @@ -@page "/notifications/preferences" \ No newline at end of file diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Applications.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Applications.razor deleted file mode 100644 index 7abc25f..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Applications.razor +++ /dev/null @@ -1,250 +0,0 @@ -@page "/propertymanagement/applications" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor deleted file mode 100644 index 4868ad7..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/GenerateLeaseOffer.razor +++ /dev/null @@ -1,449 +0,0 @@ -@page "/propertymanagement/applications/{ApplicationId:guid}/generate-lease-offer" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Application.Services.Workflows -@using Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor deleted file mode 100644 index 2b7b68d..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ProspectiveTenants.razor +++ /dev/null @@ -1,475 +0,0 @@ -@page "/PropertyManagement/ProspectiveTenants" -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor deleted file mode 100644 index f765a09..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ReviewApplication.razor +++ /dev/null @@ -1,1177 +0,0 @@ -@page "/propertymanagement/applications/{ApplicationId:guid}/review" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Application.Services.Workflows -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor deleted file mode 100644 index 323fc05..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ScheduleTour.razor +++ /dev/null @@ -1,326 +0,0 @@ -@page "/PropertyManagement/Tours/Schedule/{ProspectId:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor deleted file mode 100644 index ed1ecc8..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/SubmitApplication.razor +++ /dev/null @@ -1,616 +0,0 @@ -@page "/propertymanagement/prospects/{ProspectId:guid}/submit-application" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Core.Validation -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Application.Services.Workflows -@using Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart/Features/PropertyManagement/Applications/Pages/Tours.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Tours.razor deleted file mode 100644 index f572a36..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/Tours.razor +++ /dev/null @@ -1,397 +0,0 @@ -@page "/PropertyManagement/Tours" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor deleted file mode 100644 index ae1d9e1..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ToursCalendar.razor +++ /dev/null @@ -1,667 +0,0 @@ -@page "/PropertyManagement/Tours/Calendar" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor deleted file mode 100644 index 6da4792..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Applications/Pages/ViewProspectiveTenant.razor +++ /dev/null @@ -1,813 +0,0 @@ -@page "/PropertyManagement/ProspectiveTenants/{ProspectId:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Calendar.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Calendar.razor deleted file mode 100644 index b768d8e..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Calendar.razor +++ /dev/null @@ -1,1670 +0,0 @@ -@page "/PropertyManagement/Calendar" -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Utilities -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/CalendarListView.razor b/Aquiis.SimpleStart/Features/PropertyManagement/CalendarListView.razor deleted file mode 100644 index 662c4fe..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/CalendarListView.razor +++ /dev/null @@ -1,877 +0,0 @@ -@page "/PropertyManagement/Calendar/ListView" -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Utilities -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor deleted file mode 100644 index 128abb4..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Checklists.razor +++ /dev/null @@ -1,176 +0,0 @@ -@page "/propertymanagement/checklists" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor deleted file mode 100644 index b110e95..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Complete.razor +++ /dev/null @@ -1,713 +0,0 @@ -@page "/propertymanagement/checklists/complete/{ChecklistId:guid}" -@page "/propertymanagement/checklists/complete/new" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor deleted file mode 100644 index c7e25db..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Create.razor +++ /dev/null @@ -1,352 +0,0 @@ -@page "/propertymanagement/checklists/create" - -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Checklists/Pages/EditTemplate.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/EditTemplate.razor deleted file mode 100644 index 00a57ea..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/EditTemplate.razor +++ /dev/null @@ -1,344 +0,0 @@ -@page "/propertymanagement/checklists/templates/edit/{TemplateId:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor deleted file mode 100644 index 25c3379..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/MyChecklists.razor +++ /dev/null @@ -1,368 +0,0 @@ -@page "/propertymanagement/checklists/mychecklists" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Checklists/Pages/Templates.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Templates.razor deleted file mode 100644 index 697368f..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/Templates.razor +++ /dev/null @@ -1,349 +0,0 @@ -@page "/propertymanagement/checklists/templates" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor deleted file mode 100644 index 97a70c5..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Checklists/Pages/View.razor +++ /dev/null @@ -1,588 +0,0 @@ -@page "/propertymanagement/checklists/view/{ChecklistId:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Documents/Pages/Documents.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/Documents.razor deleted file mode 100644 index bf8342f..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/Documents.razor +++ /dev/null @@ -1,586 +0,0 @@ -@page "/propertymanagement/documents" - -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor deleted file mode 100644 index 1caf011..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/LeaseDocuments.razor +++ /dev/null @@ -1,347 +0,0 @@ -@page "/propertymanagement/leases/{LeaseId:guid}/documents" - -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Documents/Pages/_Imports.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/_Imports.razor deleted file mode 100644 index 51d2d4a..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Documents/Pages/_Imports.razor +++ /dev/null @@ -1,13 +0,0 @@ -@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.SimpleStart -@using Aquiis.SimpleStart.Infrastructure.Data -@using Aquiis.SimpleStart.Core.Entities -@using System.ComponentModel.DataAnnotations diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Create.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Create.razor deleted file mode 100644 index 9453cec..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Create.razor +++ /dev/null @@ -1,552 +0,0 @@ -@page "/propertymanagement/inspections/create" -@page "/propertymanagement/inspections/create/{PropertyId:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Core.Validation -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Inspections/Pages/Schedule.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Schedule.razor deleted file mode 100644 index eb37e12..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Schedule.razor +++ /dev/null @@ -1,323 +0,0 @@ -@page "/propertymanagement/inspections/schedule" - -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Inspections/Pages/View.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/View.razor deleted file mode 100644 index ded2ff1..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/View.razor +++ /dev/null @@ -1,426 +0,0 @@ -@page "/propertymanagement/inspections/view/{InspectionId:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart/Features/PropertyManagement/Invoices/Pages/CreateInvoice.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/CreateInvoice.razor deleted file mode 100644 index cce9b50..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/CreateInvoice.razor +++ /dev/null @@ -1,317 +0,0 @@ -@page "/propertymanagement/invoices/create" -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Invoices/Pages/EditInvoice.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/EditInvoice.razor deleted file mode 100644 index 76d7373..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/EditInvoice.razor +++ /dev/null @@ -1,396 +0,0 @@ -@page "/propertymanagement/invoices/edit/{Id:guid}" - -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Invoices/Pages/Invoices.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Invoices.razor deleted file mode 100644 index 7d6bab7..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Invoices.razor +++ /dev/null @@ -1,592 +0,0 @@ -@page "/propertymanagement/invoices" -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Invoices/Pages/ViewInvoice.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/ViewInvoice.razor deleted file mode 100644 index b3669aa..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/ViewInvoice.razor +++ /dev/null @@ -1,408 +0,0 @@ -@page "/propertymanagement/invoices/view/{Id:guid}" -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/LeaseOffers.razor b/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/LeaseOffers.razor deleted file mode 100644 index 7183903..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/LeaseOffers.razor +++ /dev/null @@ -1,165 +0,0 @@ -@page "/propertymanagement/leaseoffers" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor b/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor deleted file mode 100644 index f0f43d2..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/LeaseOffers/Pages/ViewLeaseOffer.razor +++ /dev/null @@ -1,574 +0,0 @@ -@page "/propertymanagement/leaseoffers/view/{Id:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Application.Services.Workflows -@using Aquiis.SimpleStart.Infrastructure.Data -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Leases/Pages/AcceptLease.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/AcceptLease.razor deleted file mode 100644 index 53563a9..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/AcceptLease.razor +++ /dev/null @@ -1,637 +0,0 @@ -@page "/propertymanagement/leases/{LeaseId:guid}/accept" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Leases/Pages/CreateLease.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/CreateLease.razor deleted file mode 100644 index e245093..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/CreateLease.razor +++ /dev/null @@ -1,383 +0,0 @@ -@page "/propertymanagement/leases/create" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Leases/Pages/EditLease.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/EditLease.razor deleted file mode 100644 index 17e9744..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/EditLease.razor +++ /dev/null @@ -1,357 +0,0 @@ -@page "/propertymanagement/leases/edit/{Id:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components -@using Microsoft.AspNetCore.Components.Web -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Leases/Pages/Leases.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Leases.razor deleted file mode 100644 index 778aca5..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Leases.razor +++ /dev/null @@ -1,837 +0,0 @@ -@page "/propertymanagement/leases" - -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.EntityFrameworkCore -@using Aquiis.SimpleStart.Infrastructure.Data -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Leases/Pages/ViewLease.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/ViewLease.razor deleted file mode 100644 index cfe7713..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/ViewLease.razor +++ /dev/null @@ -1,1263 +0,0 @@ -@page "/propertymanagement/leases/view/{Id:guid}" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Core.Validation -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components -@using Microsoft.AspNetCore.Components.Web -@using Aquiis.SimpleStart.Features.PropertyManagement -@using System.ComponentModel.DataAnnotations -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Application.Services.Workflows -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor b/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor deleted file mode 100644 index fbdc08c..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/CreateMaintenanceRequest.razor +++ /dev/null @@ -1,354 +0,0 @@ -@page "/propertymanagement/maintenance/create/{PropertyId:int?}" -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/EditMaintenanceRequest.razor b/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/EditMaintenanceRequest.razor deleted file mode 100644 index 545ed1b..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/EditMaintenanceRequest.razor +++ /dev/null @@ -1,306 +0,0 @@ -@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.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/MaintenanceRequests.razor b/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/MaintenanceRequests.razor deleted file mode 100644 index bd605ee..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/MaintenanceRequests.razor +++ /dev/null @@ -1,350 +0,0 @@ -@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.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/ViewMaintenanceRequest.razor b/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/ViewMaintenanceRequest.razor deleted file mode 100644 index b490f03..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/ViewMaintenanceRequest.razor +++ /dev/null @@ -1,309 +0,0 @@ -@page "/propertymanagement/maintenance/view/{Id:guid}" - -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor b/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor deleted file mode 100644 index 36bcefe..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor +++ /dev/null @@ -1,4 +0,0 @@ -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Core.Constants -@using Microsoft.AspNetCore.Authorization diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Payments.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Payments.razor deleted file mode 100644 index 71cbd66..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Payments.razor +++ /dev/null @@ -1,492 +0,0 @@ -@page "/propertymanagement/payments" -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Payments/Pages/ViewPayment.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/ViewPayment.razor deleted file mode 100644 index 231abf1..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/ViewPayment.razor +++ /dev/null @@ -1,417 +0,0 @@ -@page "/propertymanagement/payments/view/{PaymentId:guid}" - -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart/Features/PropertyManagement/Payments/Pages/_Imports.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/_Imports.razor deleted file mode 100644 index 2c9fb40..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/_Imports.razor +++ /dev/null @@ -1,12 +0,0 @@ -@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.SimpleStart -@using Aquiis.SimpleStart.Infrastructure.Data -@using Aquiis.SimpleStart.Core.Entities diff --git a/Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor deleted file mode 100644 index 025c696..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor +++ /dev/null @@ -1,260 +0,0 @@ -@page "/propertymanagement/properties/create" -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor deleted file mode 100644 index 4b0bcd8..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor +++ /dev/null @@ -1,399 +0,0 @@ -@page "/propertymanagement/properties/edit/{PropertyId:guid}" - -@using System.ComponentModel.DataAnnotations -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor deleted file mode 100644 index c608ad0..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor +++ /dev/null @@ -1,558 +0,0 @@ -@page "/propertymanagement/properties" -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor deleted file mode 100644 index f4a80c3..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor +++ /dev/null @@ -1,626 +0,0 @@ -@page "/propertymanagement/properties/view/{PropertyId:guid}" -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor deleted file mode 100644 index 5124a69..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor +++ /dev/null @@ -1,240 +0,0 @@ -@page "/reports/income-statement" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor deleted file mode 100644 index 066ea29..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor +++ /dev/null @@ -1,259 +0,0 @@ -@page "/reports/property-performance" -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor deleted file mode 100644 index f3790f8..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor +++ /dev/null @@ -1,243 +0,0 @@ -@page "/reports/rentroll" -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor deleted file mode 100644 index 7bbd5ad..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor +++ /dev/null @@ -1,278 +0,0 @@ -@page "/reports" -@using Microsoft.AspNetCore.Authorization -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor deleted file mode 100644 index f878073..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor +++ /dev/null @@ -1,287 +0,0 @@ -@page "/reports/tax-report" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor b/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor deleted file mode 100644 index a8a3cd6..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/CalculateDividends.razor +++ /dev/null @@ -1,337 +0,0 @@ -@page "/property-management/security-deposits/calculate-dividends/{PoolId:guid}" -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor b/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor deleted file mode 100644 index ad90293..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/InvestmentPools.razor +++ /dev/null @@ -1,345 +0,0 @@ -@page "/property-management/security-deposits/investment-pools" -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor b/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor deleted file mode 100644 index 9858a6a..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/RecordPoolPerformance.razor +++ /dev/null @@ -1,359 +0,0 @@ -@page "/property-management/security-deposits/record-performance" -@page "/property-management/security-deposits/record-performance/{Year:int}" -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor b/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor deleted file mode 100644 index bcac3da..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/SecurityDeposits.razor +++ /dev/null @@ -1,401 +0,0 @@ -@page "/property-management/security-deposits" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor b/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor deleted file mode 100644 index 15575be..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/SecurityDeposits/Pages/ViewInvestmentPool.razor +++ /dev/null @@ -1,419 +0,0 @@ -@page "/property-management/security-deposits/investment-pool/{PoolId:guid}" -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Tenants/Pages/Create.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Create.razor deleted file mode 100644 index 84a03ac..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Create.razor +++ /dev/null @@ -1,217 +0,0 @@ -@page "/propertymanagement/tenants/create" - -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Tenants/Pages/EditTenant.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/EditTenant.razor deleted file mode 100644 index 515b748..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/EditTenant.razor +++ /dev/null @@ -1,339 +0,0 @@ -@page "/propertymanagement/tenants/edit/{Id:guid}" -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Tenants/Pages/Tenants.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Tenants.razor deleted file mode 100644 index 253c47f..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Tenants.razor +++ /dev/null @@ -1,528 +0,0 @@ -@page "/propertymanagement/tenants" -@using Aquiis.SimpleStart.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.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor b/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor deleted file mode 100644 index 0566ddf..0000000 --- a/Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor +++ /dev/null @@ -1,241 +0,0 @@ -@page "/propertymanagement/tenants/view/{Id:guid}" -@using Aquiis.SimpleStart.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.SimpleStart/Features/_Imports.razor b/Aquiis.SimpleStart/Features/_Imports.razor deleted file mode 100644 index e5339e7..0000000 --- a/Aquiis.SimpleStart/Features/_Imports.razor +++ /dev/null @@ -1,21 +0,0 @@ -@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.SimpleStart -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Shared.Layout -@using Aquiis.SimpleStart.Shared.Components -@using Aquiis.SimpleStart.Shared.Components.Account -@using Aquiis.SimpleStart.Shared.Authorization -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Core.Constants -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Features.Administration diff --git a/Aquiis.SimpleStart/Infrastructure/Data/ApplicationDbContext.cs b/Aquiis.SimpleStart/Infrastructure/Data/ApplicationDbContext.cs deleted file mode 100644 index d6dd7f2..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/ApplicationDbContext.cs +++ /dev/null @@ -1,742 +0,0 @@ -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart/Infrastructure/Data/Migrations/20251209234246_InitialCreate.Designer.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251209234246_InitialCreate.Designer.cs deleted file mode 100644 index d31342b..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251209234246_InitialCreate.Designer.cs +++ /dev/null @@ -1,3914 +0,0 @@ -// -using System; -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.Core.Entities.ApplicationScreening", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithOne("Screening") - .HasForeignKey("Aquiis.SimpleStart.Core.Entities.ApplicationScreening", "RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.CalendarEvent", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Checklists") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.ChecklistItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany("Items") - .HasForeignKey("ChecklistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Checklist"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplateItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Items") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChecklistTemplate"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Document", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany() - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Documents") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Payment", "Payment") - .WithMany() - .HasForeignKey("PaymentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Documents") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Inspection", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Invoices") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Leases") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Leases") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany("Leases") - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Property"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.LeaseOffer", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany() - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithMany() - .HasForeignKey("RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.MaintenanceRequest", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Note", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", "User") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Payment", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany("Payments") - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Invoice"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Properties") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "InterestedProperty") - .WithMany() - .HasForeignKey("InterestedPropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("InterestedProperty"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithOne("Application") - .HasForeignKey("Aquiis.SimpleStart.Core.Entities.RentalApplication", "ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany() - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositDividend", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") - .WithMany("Dividends") - .HasForeignKey("InvestmentPoolId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", "SecurityDeposit") - .WithMany("Dividends") - .HasForeignKey("SecurityDepositId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Tenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Tenants") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tour", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany() - .HasForeignKey("ChecklistId"); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Tours") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Checklist"); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.UserOrganization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany("UserOrganizations") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", b => - { - b.Navigation("Checklists"); - - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.Navigation("Payments"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.Navigation("Documents"); - - b.Navigation("Invoices"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.Navigation("Leases"); - - b.Navigation("Properties"); - - b.Navigation("Tenants"); - - b.Navigation("UserOrganizations"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.Navigation("Documents"); - - b.Navigation("Leases"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.Navigation("Application"); - - b.Navigation("Tours"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.Navigation("Screening"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tenant", b => - { - b.Navigation("Leases"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251209234246_InitialCreate.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251209234246_InitialCreate.cs deleted file mode 100644 index 3db4d21..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251209234246_InitialCreate.cs +++ /dev/null @@ -1,2076 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional - -namespace Aquiis.SimpleStart.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.SimpleStart/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.Designer.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.Designer.cs deleted file mode 100644 index 25d0620..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.Designer.cs +++ /dev/null @@ -1,3917 +0,0 @@ -// -using System; -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.Core.Entities.ApplicationScreening", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithOne("Screening") - .HasForeignKey("Aquiis.SimpleStart.Core.Entities.ApplicationScreening", "RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.CalendarEvent", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Checklists") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.ChecklistItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany("Items") - .HasForeignKey("ChecklistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Checklist"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplateItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Items") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChecklistTemplate"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Document", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany() - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Documents") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Payment", "Payment") - .WithMany() - .HasForeignKey("PaymentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Documents") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Inspection", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Invoices") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Leases") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Leases") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany("Leases") - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Property"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.LeaseOffer", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany() - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithMany() - .HasForeignKey("RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.MaintenanceRequest", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Note", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", "User") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Payment", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany("Payments") - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Invoice"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Properties") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "InterestedProperty") - .WithMany() - .HasForeignKey("InterestedPropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("InterestedProperty"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Applications") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany() - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositDividend", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") - .WithMany("Dividends") - .HasForeignKey("InvestmentPoolId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", "SecurityDeposit") - .WithMany("Dividends") - .HasForeignKey("SecurityDepositId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Tenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Tenants") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tour", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany() - .HasForeignKey("ChecklistId"); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Tours") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Checklist"); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.UserOrganization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany("UserOrganizations") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", b => - { - b.Navigation("Checklists"); - - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.Navigation("Payments"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.Navigation("Documents"); - - b.Navigation("Invoices"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.Navigation("Leases"); - - b.Navigation("Properties"); - - b.Navigation("Tenants"); - - b.Navigation("UserOrganizations"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.Navigation("Documents"); - - b.Navigation("Leases"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.Navigation("Applications"); - - b.Navigation("Tours"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.Navigation("Screening"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tenant", b => - { - b.Navigation("Leases"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.cs deleted file mode 100644 index 76d09b5..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211184024_WorkflowAuditLog.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.Designer.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.Designer.cs deleted file mode 100644 index aa2cd57..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.Designer.cs +++ /dev/null @@ -1,3917 +0,0 @@ -// -using System; -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.Core.Entities.ApplicationScreening", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithOne("Screening") - .HasForeignKey("Aquiis.SimpleStart.Core.Entities.ApplicationScreening", "RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.CalendarEvent", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Checklists") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.ChecklistItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany("Items") - .HasForeignKey("ChecklistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Checklist"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplateItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Items") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChecklistTemplate"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Document", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany() - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Documents") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Payment", "Payment") - .WithMany() - .HasForeignKey("PaymentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Documents") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Inspection", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Invoices") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Leases") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Leases") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany("Leases") - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Property"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.LeaseOffer", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany() - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithMany() - .HasForeignKey("RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.MaintenanceRequest", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Note", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", "User") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Payment", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany("Payments") - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Invoice"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Properties") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "InterestedProperty") - .WithMany() - .HasForeignKey("InterestedPropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("InterestedProperty"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Applications") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany() - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositDividend", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") - .WithMany("Dividends") - .HasForeignKey("InvestmentPoolId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", "SecurityDeposit") - .WithMany("Dividends") - .HasForeignKey("SecurityDepositId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Tenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Tenants") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tour", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany() - .HasForeignKey("ChecklistId"); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Tours") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Checklist"); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.UserOrganization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany("UserOrganizations") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", b => - { - b.Navigation("Checklists"); - - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.Navigation("Payments"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.Navigation("Documents"); - - b.Navigation("Invoices"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.Navigation("Leases"); - - b.Navigation("Properties"); - - b.Navigation("Tenants"); - - b.Navigation("UserOrganizations"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.Navigation("Documents"); - - b.Navigation("Leases"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.Navigation("Applications"); - - b.Navigation("Tours"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.Navigation("Screening"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tenant", b => - { - b.Navigation("Leases"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.cs deleted file mode 100644 index 408f6a2..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251211232344_UpdateSeedData.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.Designer.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.Designer.cs deleted file mode 100644 index 40ac1c4..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.Designer.cs +++ /dev/null @@ -1,4123 +0,0 @@ -// -using System; -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.Core.Entities.ApplicationScreening", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithOne("Screening") - .HasForeignKey("Aquiis.SimpleStart.Core.Entities.ApplicationScreening", "RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.CalendarEvent", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Checklists") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.ChecklistItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany("Items") - .HasForeignKey("ChecklistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Checklist"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplateItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Items") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChecklistTemplate"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Document", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany() - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Documents") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Payment", "Payment") - .WithMany() - .HasForeignKey("PaymentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Documents") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Inspection", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Invoices") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Leases") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Leases") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany("Leases") - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Property"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.LeaseOffer", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany() - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithMany() - .HasForeignKey("RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.MaintenanceRequest", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Note", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", "User") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Payment", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany("Payments") - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Invoice"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Properties") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "InterestedProperty") - .WithMany() - .HasForeignKey("InterestedPropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("InterestedProperty"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Applications") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany() - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositDividend", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") - .WithMany("Dividends") - .HasForeignKey("InvestmentPoolId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", "SecurityDeposit") - .WithMany("Dividends") - .HasForeignKey("SecurityDepositId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Tenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Tenants") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tour", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany() - .HasForeignKey("ChecklistId"); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Tours") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Checklist"); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.UserOrganization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany("UserOrganizations") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Notification", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("RecipientUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("NotificationPreferences", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", b => - { - b.Navigation("Checklists"); - - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.Navigation("Payments"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.Navigation("Documents"); - - b.Navigation("Invoices"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.Navigation("Leases"); - - b.Navigation("Properties"); - - b.Navigation("Tenants"); - - b.Navigation("UserOrganizations"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.Navigation("Documents"); - - b.Navigation("Leases"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.Navigation("Applications"); - - b.Navigation("Tours"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.Navigation("Screening"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tenant", b => - { - b.Navigation("Leases"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.cs deleted file mode 100644 index 5a84a1e..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251229235707_AddNotificationInfrastructure.cs +++ /dev/null @@ -1,666 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.Designer.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.Designer.cs deleted file mode 100644 index 5783ab7..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.Designer.cs +++ /dev/null @@ -1,4324 +0,0 @@ -// -using System; -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.Core.Entities.ApplicationScreening", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithOne("Screening") - .HasForeignKey("Aquiis.SimpleStart.Core.Entities.ApplicationScreening", "RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.CalendarEvent", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Checklists") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.ChecklistItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany("Items") - .HasForeignKey("ChecklistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Checklist"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplateItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Items") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChecklistTemplate"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Document", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany() - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Documents") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Payment", "Payment") - .WithMany() - .HasForeignKey("PaymentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Documents") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Inspection", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Invoices") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Leases") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Leases") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany("Leases") - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Property"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.LeaseOffer", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany() - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithMany() - .HasForeignKey("RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.MaintenanceRequest", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Note", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", "User") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.OrganizationEmailSettings", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.OrganizationSMSSettings", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Payment", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany("Payments") - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Invoice"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Properties") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "InterestedProperty") - .WithMany() - .HasForeignKey("InterestedPropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("InterestedProperty"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Applications") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany() - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositDividend", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") - .WithMany("Dividends") - .HasForeignKey("InvestmentPoolId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", "SecurityDeposit") - .WithMany("Dividends") - .HasForeignKey("SecurityDepositId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Tenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Tenants") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tour", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany() - .HasForeignKey("ChecklistId"); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Tours") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Checklist"); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.UserOrganization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany("UserOrganizations") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Notification", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("RecipientUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("NotificationPreferences", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", b => - { - b.Navigation("Checklists"); - - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.Navigation("Payments"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.Navigation("Documents"); - - b.Navigation("Invoices"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.Navigation("Leases"); - - b.Navigation("Properties"); - - b.Navigation("Tenants"); - - b.Navigation("UserOrganizations"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.Navigation("Documents"); - - b.Navigation("Leases"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.Navigation("Applications"); - - b.Navigation("Tours"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.Navigation("Screening"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tenant", b => - { - b.Navigation("Leases"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.cs deleted file mode 100644 index 0e01295..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/20251230141240_OrganizationEmailSMSSettings.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Aquiis.SimpleStart/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs deleted file mode 100644 index 363e36e..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ /dev/null @@ -1,4321 +0,0 @@ -// -using System; -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.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.SimpleStart.Core.Entities.ApplicationScreening", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithOne("Screening") - .HasForeignKey("Aquiis.SimpleStart.Core.Entities.ApplicationScreening", "RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.CalendarEvent", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Checklists") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.ChecklistItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany("Items") - .HasForeignKey("ChecklistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Checklist"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplateItem", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", "ChecklistTemplate") - .WithMany("Items") - .HasForeignKey("ChecklistTemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChecklistTemplate"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Document", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany() - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Documents") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Payment", "Payment") - .WithMany() - .HasForeignKey("PaymentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Documents") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Inspection", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany("Invoices") - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Lease"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Leases") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany("Leases") - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany("Leases") - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Property"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.LeaseOffer", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany() - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.RentalApplication", "RentalApplication") - .WithMany() - .HasForeignKey("RentalApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - - b.Navigation("RentalApplication"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.MaintenanceRequest", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Property"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Note", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", "User") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.OrganizationEmailSettings", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.OrganizationSMSSettings", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Payment", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Invoice", "Invoice") - .WithMany("Payments") - .HasForeignKey("InvoiceId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Document"); - - b.Navigation("Invoice"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Properties") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "InterestedProperty") - .WithMany() - .HasForeignKey("InterestedPropertyId") - .OnDelete(DeleteBehavior.SetNull); - - b.Navigation("InterestedProperty"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Applications") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Tenant", "Tenant") - .WithMany() - .HasForeignKey("TenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Lease"); - - b.Navigation("Tenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositDividend", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", "InvestmentPool") - .WithMany("Dividends") - .HasForeignKey("InvestmentPoolId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Lease", "Lease") - .WithMany() - .HasForeignKey("LeaseId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", "SecurityDeposit") - .WithMany("Dividends") - .HasForeignKey("SecurityDepositId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Core.Entities.Tenant", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", null) - .WithMany("Tenants") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tour", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Checklist", "Checklist") - .WithMany() - .HasForeignKey("ChecklistId"); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Property", "Property") - .WithMany() - .HasForeignKey("PropertyId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", "ProspectiveTenant") - .WithMany("Tours") - .HasForeignKey("ProspectiveTenantId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("Checklist"); - - b.Navigation("Property"); - - b.Navigation("ProspectiveTenant"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.UserOrganization", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany("UserOrganizations") - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Aquiis.SimpleStart.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.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Notification", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("RecipientUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("NotificationPreferences", b => - { - b.HasOne("Aquiis.SimpleStart.Core.Entities.Organization", "Organization") - .WithMany() - .HasForeignKey("OrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Aquiis.SimpleStart.Shared.Components.Account.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Organization"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Checklist", b => - { - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ChecklistTemplate", b => - { - b.Navigation("Checklists"); - - b.Navigation("Items"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Invoice", b => - { - b.Navigation("Payments"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Lease", b => - { - b.Navigation("Documents"); - - b.Navigation("Invoices"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Organization", b => - { - b.Navigation("Leases"); - - b.Navigation("Properties"); - - b.Navigation("Tenants"); - - b.Navigation("UserOrganizations"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Property", b => - { - b.Navigation("Documents"); - - b.Navigation("Leases"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.ProspectiveTenant", b => - { - b.Navigation("Applications"); - - b.Navigation("Tours"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.RentalApplication", b => - { - b.Navigation("Screening"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDeposit", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.SecurityDepositInvestmentPool", b => - { - b.Navigation("Dividends"); - }); - - modelBuilder.Entity("Aquiis.SimpleStart.Core.Entities.Tenant", b => - { - b.Navigation("Leases"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Aquiis.SimpleStart/Infrastructure/Services/EmailService.cs b/Aquiis.SimpleStart/Infrastructure/Services/EmailService.cs deleted file mode 100644 index b340828..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Services/EmailService.cs +++ /dev/null @@ -1,41 +0,0 @@ - -using Aquiis.SimpleStart.Core.Interfaces.Services; - -namespace Aquiis.SimpleStart.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.SimpleStart/Infrastructure/Services/EntityRouteHelper.cs b/Aquiis.SimpleStart/Infrastructure/Services/EntityRouteHelper.cs deleted file mode 100644 index d0c1e6b..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Services/EntityRouteHelper.cs +++ /dev/null @@ -1,63 +0,0 @@ -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/Infrastructure/Services/SMSService.cs b/Aquiis.SimpleStart/Infrastructure/Services/SMSService.cs deleted file mode 100644 index 687b6ea..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Services/SMSService.cs +++ /dev/null @@ -1,29 +0,0 @@ - -using Aquiis.SimpleStart.Core.Interfaces.Services; - -namespace Aquiis.SimpleStart.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.SimpleStart/Infrastructure/Services/SendGridEmailService.cs b/Aquiis.SimpleStart/Infrastructure/Services/SendGridEmailService.cs deleted file mode 100644 index 727351a..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Services/SendGridEmailService.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using SendGrid; -using SendGrid.Helpers.Mail; - -namespace Aquiis.SimpleStart.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.SimpleStart/Infrastructure/Services/TwilioSMSService.cs b/Aquiis.SimpleStart/Infrastructure/Services/TwilioSMSService.cs deleted file mode 100644 index 881d7b6..0000000 --- a/Aquiis.SimpleStart/Infrastructure/Services/TwilioSMSService.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Interfaces.Services; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.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.SimpleStart.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.SimpleStart/Program.cs b/Aquiis.SimpleStart/Program.cs deleted file mode 100644 index f476599..0000000 --- a/Aquiis.SimpleStart/Program.cs +++ /dev/null @@ -1,586 +0,0 @@ -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Authorization; -using Microsoft.EntityFrameworkCore; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Features.PropertyManagement; -using Aquiis.SimpleStart.Core.Constants; -using Aquiis.SimpleStart.Core.Interfaces; -using Aquiis.SimpleStart.Application.Services; -using Aquiis.SimpleStart.Application.Services.PdfGenerators; -using Aquiis.SimpleStart.Shared.Services; -using Aquiis.SimpleStart.Shared.Authorization; -using ElectronNET.API; -using Microsoft.Extensions.Options; -using Aquiis.SimpleStart.Application.Services.Workflows; -using Aquiis.SimpleStart.Core.Interfaces.Services; -using Aquiis.SimpleStart.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.SimpleStart/Shared/App.razor b/Aquiis.SimpleStart/Shared/App.razor deleted file mode 100644 index d955c9d..0000000 --- a/Aquiis.SimpleStart/Shared/App.razor +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Aquiis.SimpleStart/Shared/Authorization/OrganizationAuthorizeAttribute.cs b/Aquiis.SimpleStart/Shared/Authorization/OrganizationAuthorizeAttribute.cs deleted file mode 100644 index 9cc5ed4..0000000 --- a/Aquiis.SimpleStart/Shared/Authorization/OrganizationAuthorizeAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Aquiis.SimpleStart.Shared.Authorization; - -/// -/// Authorization attribute for organization-based role checking. -/// Replaces [Authorize(Roles = "...")] with organization-scoped roles. -/// -public class OrganizationAuthorizeAttribute : AuthorizeAttribute -{ - public OrganizationAuthorizeAttribute(params string[] roles) - { - Policy = $"OrganizationRole:{string.Join(",", roles)}"; - } -} diff --git a/Aquiis.SimpleStart/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs b/Aquiis.SimpleStart/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs deleted file mode 100644 index 352c8a8..0000000 --- a/Aquiis.SimpleStart/Shared/Authorization/OrganizationRoleAuthorizationHandler.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Infrastructure.Data; -using Aquiis.SimpleStart.Core.Constants; -using System.Security.Claims; - -namespace Aquiis.SimpleStart.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 (requirement.AllowedRoles.Contains(userOrganization.Role)) - { - context.Succeed(requirement); - } - } -} diff --git a/Aquiis.SimpleStart/Shared/Components/Account/AccountConstants.cs b/Aquiis.SimpleStart/Shared/Components/Account/AccountConstants.cs deleted file mode 100644 index 7ba1e73..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/AccountConstants.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Account/ApplicationUser.cs b/Aquiis.SimpleStart/Shared/Components/Account/ApplicationUser.cs deleted file mode 100644 index 3a956a7..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/ApplicationUser.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Aquiis.SimpleStart.Shared.Services; -using Microsoft.AspNetCore.Identity; - -namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Account/IdentityNoOpEmailSender.cs b/Aquiis.SimpleStart/Shared/Components/Account/IdentityNoOpEmailSender.cs deleted file mode 100644 index 224fb2f..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/IdentityNoOpEmailSender.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.UI.Services; -using Aquiis.SimpleStart.Infrastructure.Data; - -namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs b/Aquiis.SimpleStart/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs deleted file mode 100644 index 0790ebe..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Components.Server; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using Aquiis.SimpleStart.Infrastructure.Data; - -namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Account/IdentityUserAccessor.cs b/Aquiis.SimpleStart/Shared/Components/Account/IdentityUserAccessor.cs deleted file mode 100644 index 605600d..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/IdentityUserAccessor.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Aquiis.SimpleStart.Infrastructure.Data; - -namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Account/Pages/ExternalLogin.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/ExternalLogin.razor deleted file mode 100644 index b7cd0c2..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/ExternalLogin.razor +++ /dev/null @@ -1,204 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/ForgotPassword.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/ForgotPassword.razor deleted file mode 100644 index 15b6bb5..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/ForgotPassword.razor +++ /dev/null @@ -1,67 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Login.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Login.razor deleted file mode 100644 index d3c83b4..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Login.razor +++ /dev/null @@ -1,127 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/LoginWith2fa.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWith2fa.razor deleted file mode 100644 index e117b0c..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWith2fa.razor +++ /dev/null @@ -1,100 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor deleted file mode 100644 index 5759f11..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/LoginWithRecoveryCode.razor +++ /dev/null @@ -1,84 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/ChangePassword.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ChangePassword.razor deleted file mode 100644 index c7c0ed0..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ChangePassword.razor +++ /dev/null @@ -1,95 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor deleted file mode 100644 index bbb1034..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/DeletePersonalData.razor +++ /dev/null @@ -1,85 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/Disable2fa.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Disable2fa.razor deleted file mode 100644 index 2ecfb40..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Disable2fa.razor +++ /dev/null @@ -1,62 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor deleted file mode 100644 index 3b93e23..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor +++ /dev/null @@ -1,122 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor deleted file mode 100644 index 0084a6f..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/EnableAuthenticator.razor +++ /dev/null @@ -1,171 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/ExternalLogins.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ExternalLogins.razor deleted file mode 100644 index 0c109c4..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ExternalLogins.razor +++ /dev/null @@ -1,139 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor deleted file mode 100644 index 99f6438..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor +++ /dev/null @@ -1,67 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/Index.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Index.razor deleted file mode 100644 index 8ff0c8c..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Index.razor +++ /dev/null @@ -1,116 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/PersonalData.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/PersonalData.razor deleted file mode 100644 index 3cd179e..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/PersonalData.razor +++ /dev/null @@ -1,34 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor deleted file mode 100644 index 0a08d34..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/ResetAuthenticator.razor +++ /dev/null @@ -1,51 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/SetPassword.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/SetPassword.razor deleted file mode 100644 index 307a660..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/SetPassword.razor +++ /dev/null @@ -1,86 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor deleted file mode 100644 index 75b15eb..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/TwoFactorAuthentication.razor +++ /dev/null @@ -1,100 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/Register.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/Register.razor deleted file mode 100644 index f0739d9..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/Register.razor +++ /dev/null @@ -1,264 +0,0 @@ -@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.SimpleStart.Infrastructure.Data -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Account/Pages/ResendEmailConfirmation.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/ResendEmailConfirmation.razor deleted file mode 100644 index b1bd072..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/ResendEmailConfirmation.razor +++ /dev/null @@ -1,67 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/ResetPassword.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/ResetPassword.razor deleted file mode 100644 index 0503cf3..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/ResetPassword.razor +++ /dev/null @@ -1,102 +0,0 @@ -@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.SimpleStart/Shared/Components/Account/Pages/_Imports.razor b/Aquiis.SimpleStart/Shared/Components/Account/Pages/_Imports.razor deleted file mode 100644 index 6cd370c..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Account/Pages/_Imports.razor +++ /dev/null @@ -1,2 +0,0 @@ -@using Aquiis.SimpleStart.Shared.Components.Account.Shared -@attribute [ExcludeFromInteractiveRouting] diff --git a/Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor b/Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor deleted file mode 100644 index cfad360..0000000 --- a/Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor +++ /dev/null @@ -1,179 +0,0 @@ -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.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.SimpleStart/Shared/Components/NotesTimeline.razor b/Aquiis.SimpleStart/Shared/Components/NotesTimeline.razor deleted file mode 100644 index 8980f20..0000000 --- a/Aquiis.SimpleStart/Shared/Components/NotesTimeline.razor +++ /dev/null @@ -1,246 +0,0 @@ -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.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.SimpleStart/Shared/Components/NotificationBell.razor b/Aquiis.SimpleStart/Shared/Components/NotificationBell.razor deleted file mode 100644 index ecf02f3..0000000 --- a/Aquiis.SimpleStart/Shared/Components/NotificationBell.razor +++ /dev/null @@ -1,295 +0,0 @@ -@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/Components/OrganizationSwitcher.razor b/Aquiis.SimpleStart/Shared/Components/OrganizationSwitcher.razor deleted file mode 100644 index 7b7e9f9..0000000 --- a/Aquiis.SimpleStart/Shared/Components/OrganizationSwitcher.razor +++ /dev/null @@ -1,190 +0,0 @@ -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Pages/Home.razor b/Aquiis.SimpleStart/Shared/Components/Pages/Home.razor deleted file mode 100644 index bebad19..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Pages/Home.razor +++ /dev/null @@ -1,478 +0,0 @@ -@page "/" -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.EntityFrameworkCore -@using Aquiis.SimpleStart.Infrastructure.Data -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.SimpleStart.Shared.Components.Account -@using Aquiis.SimpleStart.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.SimpleStart/Shared/Components/SchemaValidationWarning.razor b/Aquiis.SimpleStart/Shared/Components/SchemaValidationWarning.razor deleted file mode 100644 index 96c90fe..0000000 --- a/Aquiis.SimpleStart/Shared/Components/SchemaValidationWarning.razor +++ /dev/null @@ -1,66 +0,0 @@ -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Shared/Components/Shared/OrganizationAuthorizeView.razor b/Aquiis.SimpleStart/Shared/Components/Shared/OrganizationAuthorizeView.razor deleted file mode 100644 index f4f3f7a..0000000 --- a/Aquiis.SimpleStart/Shared/Components/Shared/OrganizationAuthorizeView.razor +++ /dev/null @@ -1,62 +0,0 @@ -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Shared/Components/ToastContainer.razor b/Aquiis.SimpleStart/Shared/Components/ToastContainer.razor deleted file mode 100644 index dce6a02..0000000 --- a/Aquiis.SimpleStart/Shared/Components/ToastContainer.razor +++ /dev/null @@ -1,164 +0,0 @@ -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.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.SimpleStart/Shared/Layout/MainLayout.razor b/Aquiis.SimpleStart/Shared/Layout/MainLayout.razor deleted file mode 100644 index 92dd068..0000000 --- a/Aquiis.SimpleStart/Shared/Layout/MainLayout.razor +++ /dev/null @@ -1,55 +0,0 @@ -@inherits LayoutComponentBase -@using Aquiis.SimpleStart.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.SimpleStart/Shared/Services/DatabaseBackupService.cs b/Aquiis.SimpleStart/Shared/Services/DatabaseBackupService.cs deleted file mode 100644 index 660c3f2..0000000 --- a/Aquiis.SimpleStart/Shared/Services/DatabaseBackupService.cs +++ /dev/null @@ -1,414 +0,0 @@ -using Aquiis.SimpleStart.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using ElectronNET.API; - -namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Services/ElectronPathService.cs b/Aquiis.SimpleStart/Shared/Services/ElectronPathService.cs deleted file mode 100644 index d1da6f5..0000000 --- a/Aquiis.SimpleStart/Shared/Services/ElectronPathService.cs +++ /dev/null @@ -1,80 +0,0 @@ -using ElectronNET.API; -using ElectronNET.API.Entities; -using Microsoft.Extensions.Configuration; - -namespace Aquiis.SimpleStart.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.SimpleStart/Shared/Services/UserContextService.cs b/Aquiis.SimpleStart/Shared/Services/UserContextService.cs deleted file mode 100644 index 572dfc8..0000000 --- a/Aquiis.SimpleStart/Shared/Services/UserContextService.cs +++ /dev/null @@ -1,300 +0,0 @@ -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Identity; -using Aquiis.SimpleStart.Shared.Components.Account; -using Aquiis.SimpleStart.Core.Entities; -using Aquiis.SimpleStart.Core.Constants; -using System.Security.Claims; - -namespace Aquiis.SimpleStart.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.SimpleStart/Shared/_Imports.razor b/Aquiis.SimpleStart/Shared/_Imports.razor deleted file mode 100644 index 5ff60a4..0000000 --- a/Aquiis.SimpleStart/Shared/_Imports.razor +++ /dev/null @@ -1,19 +0,0 @@ -@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.SimpleStart -@using Aquiis.SimpleStart.Application.Services -@using Aquiis.SimpleStart.Application.Services.PdfGenerators -@using Aquiis.SimpleStart.Shared.Services -@using Aquiis.SimpleStart.Shared.Layout -@using Aquiis.SimpleStart.Shared.Components -@using Aquiis.SimpleStart.Shared.Components.Account -@using Aquiis.SimpleStart.Shared.Components.Shared -@using Aquiis.SimpleStart.Core.Entities -@using Aquiis.SimpleStart.Core.Constants diff --git a/Aquiis.SimpleStart/Utilities/CalendarEventRouter.cs b/Aquiis.SimpleStart/Utilities/CalendarEventRouter.cs deleted file mode 100644 index acb0b77..0000000 --- a/Aquiis.SimpleStart/Utilities/CalendarEventRouter.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart/Utilities/SchedulableEntityRegistry.cs b/Aquiis.SimpleStart/Utilities/SchedulableEntityRegistry.cs deleted file mode 100644 index 7d9aff6..0000000 --- a/Aquiis.SimpleStart/Utilities/SchedulableEntityRegistry.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Reflection; -using Aquiis.SimpleStart.Core.Entities; - -namespace Aquiis.SimpleStart.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.SimpleStart/appsettings.json b/Aquiis.SimpleStart/appsettings.json deleted file mode 100644 index 5654d9a..0000000 --- a/Aquiis.SimpleStart/appsettings.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "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.SimpleStart/wwwroot/css/organization-switcher.css b/Aquiis.SimpleStart/wwwroot/css/organization-switcher.css deleted file mode 100644 index 9fecf17..0000000 --- a/Aquiis.SimpleStart/wwwroot/css/organization-switcher.css +++ /dev/null @@ -1,47 +0,0 @@ -/* 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.SimpleStart/wwwroot/js/theme.js b/Aquiis.SimpleStart/wwwroot/js/theme.js deleted file mode 100644 index 168c1bf..0000000 --- a/Aquiis.SimpleStart/wwwroot/js/theme.js +++ /dev/null @@ -1,58 +0,0 @@ -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.sln b/Aquiis.sln index ca456fe..106619c 100644 --- a/Aquiis.sln +++ b/Aquiis.sln @@ -3,13 +3,33 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 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}" +Project("{C69B6EFE-BB20-41DE-8CBA-044207EBDCE1}") = "Aquiis.SimpleStart", "4-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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.Professional", "5-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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.UI.SimpleStart.Tests", "6-Tests\Aquiis.UI.SimpleStart.Tests\Aquiis.UI.SimpleStart.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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.Core", "0-Aquiis.Core\Aquiis.Core.csproj", "{B67A2BC8-38CE-4065-B11F-4B485B888371}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.Infrastructure", "1-Aquiis.Infrastructure\Aquiis.Infrastructure.csproj", "{6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.Application", "2-Aquiis.Application\Aquiis.Application.csproj", "{7C8B4A14-86DE-4D02-8780-3E87395FE585}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.Core.Tests", "6-Tests\Aquiis.Core.Tests\Aquiis.Core.Tests.csproj", "{A6D698AA-AB46-4F20-A3A8-B87C35E73132}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.Infrastructure.Tests", "6-Tests\Aquiis.Infrastructure.Tests\Aquiis.Infrastructure.Tests.csproj", "{BC1975DD-A69B-4527-A021-7E0F3B6F7235}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.Application.Tests", "6-Tests\Aquiis.Application.Tests\Aquiis.Application.Tests.csproj", "{167542BB-B021-43F2-ACAB-B035796581A4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.UI.Shared", "3-Aquiis.UI.Shared\Aquiis.UI.Shared.csproj", "{5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "6-Tests", "6-Tests", "{51414648-A7F8-78F1-F6B9-B622ED145BD3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.UI.Shared.Tests", "6-Tests\Aquiis.UI.Shared.Tests\Aquiis.UI.Shared.Tests.csproj", "{6BBB590E-E57A-405F-A04F-F3B45379BF89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquiis.UI.Professional.Tests", "6-Tests\Aquiis.UI.Professional.Tests\Aquiis.UI.Professional.Tests.csproj", "{2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5-Aquiis.Professional", "5-Aquiis.Professional", "{42F1E5E9-4300-FBD2-1250-4848B8782818}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,7 +41,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C69B6EFE-BB20-41DE-8CBA-044207EBDCE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C69B6EFE-BB20-41DE-8CBA-044207EBDCE1}.Debug|Any CPU.Build.0 = Debug|Any CPU {C69B6EFE-BB20-41DE-8CBA-044207EBDCE1}.Debug|x64.ActiveCfg = Debug|x64 @@ -46,18 +65,6 @@ Global {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 - {D1111111-1111-4111-8111-111111111111}.Debug|x64.Build.0 = Debug|Any CPU - {D1111111-1111-4111-8111-111111111111}.Debug|x86.ActiveCfg = Debug|Any CPU - {D1111111-1111-4111-8111-111111111111}.Debug|x86.Build.0 = Debug|Any CPU - {D1111111-1111-4111-8111-111111111111}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D1111111-1111-4111-8111-111111111111}.Release|Any CPU.Build.0 = Release|Any CPU - {D1111111-1111-4111-8111-111111111111}.Release|x64.ActiveCfg = Release|Any CPU - {D1111111-1111-4111-8111-111111111111}.Release|x64.Build.0 = Release|Any CPU - {D1111111-1111-4111-8111-111111111111}.Release|x86.ActiveCfg = Release|Any CPU - {D1111111-1111-4111-8111-111111111111}.Release|x86.Build.0 = Release|Any CPU {81401A51-BBA1-4B6B-8771-9C26A7B5356E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {81401A51-BBA1-4B6B-8771-9C26A7B5356E}.Debug|Any CPU.Build.0 = Debug|Any CPU {81401A51-BBA1-4B6B-8771-9C26A7B5356E}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -70,11 +77,122 @@ Global {81401A51-BBA1-4B6B-8771-9C26A7B5356E}.Release|x64.Build.0 = Release|Any CPU {81401A51-BBA1-4B6B-8771-9C26A7B5356E}.Release|x86.ActiveCfg = Release|Any CPU {81401A51-BBA1-4B6B-8771-9C26A7B5356E}.Release|x86.Build.0 = Release|Any CPU - + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Debug|x64.ActiveCfg = Debug|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Debug|x64.Build.0 = Debug|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Debug|x86.ActiveCfg = Debug|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Debug|x86.Build.0 = Debug|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Release|Any CPU.Build.0 = Release|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Release|x64.ActiveCfg = Release|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Release|x64.Build.0 = Release|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Release|x86.ActiveCfg = Release|Any CPU + {B67A2BC8-38CE-4065-B11F-4B485B888371}.Release|x86.Build.0 = Release|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Debug|x64.ActiveCfg = Debug|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Debug|x64.Build.0 = Debug|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Debug|x86.ActiveCfg = Debug|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Debug|x86.Build.0 = Debug|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Release|Any CPU.Build.0 = Release|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Release|x64.ActiveCfg = Release|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Release|x64.Build.0 = Release|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Release|x86.ActiveCfg = Release|Any CPU + {6079CFFC-55A2-4A37-9AFF-86879C5AAC0C}.Release|x86.Build.0 = Release|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Debug|x64.Build.0 = Debug|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Debug|x86.Build.0 = Debug|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Release|Any CPU.Build.0 = Release|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Release|x64.ActiveCfg = Release|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Release|x64.Build.0 = Release|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Release|x86.ActiveCfg = Release|Any CPU + {7C8B4A14-86DE-4D02-8780-3E87395FE585}.Release|x86.Build.0 = Release|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Debug|x64.Build.0 = Debug|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Debug|x86.Build.0 = Debug|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Release|Any CPU.Build.0 = Release|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Release|x64.ActiveCfg = Release|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Release|x64.Build.0 = Release|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Release|x86.ActiveCfg = Release|Any CPU + {A6D698AA-AB46-4F20-A3A8-B87C35E73132}.Release|x86.Build.0 = Release|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Debug|x64.Build.0 = Debug|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Debug|x86.ActiveCfg = Debug|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Debug|x86.Build.0 = Debug|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Release|Any CPU.Build.0 = Release|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Release|x64.ActiveCfg = Release|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Release|x64.Build.0 = Release|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Release|x86.ActiveCfg = Release|Any CPU + {BC1975DD-A69B-4527-A021-7E0F3B6F7235}.Release|x86.Build.0 = Release|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Debug|x64.Build.0 = Debug|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Debug|x86.Build.0 = Debug|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Release|Any CPU.Build.0 = Release|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Release|x64.ActiveCfg = Release|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Release|x64.Build.0 = Release|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Release|x86.ActiveCfg = Release|Any CPU + {167542BB-B021-43F2-ACAB-B035796581A4}.Release|x86.Build.0 = Release|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Debug|x64.ActiveCfg = Debug|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Debug|x64.Build.0 = Debug|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Debug|x86.ActiveCfg = Debug|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Debug|x86.Build.0 = Debug|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Release|Any CPU.Build.0 = Release|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Release|x64.ActiveCfg = Release|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Release|x64.Build.0 = Release|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Release|x86.ActiveCfg = Release|Any CPU + {5716100E-5D5F-4F69-A6D4-3CD930EA7C3C}.Release|x86.Build.0 = Release|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Debug|x64.Build.0 = Debug|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Debug|x86.Build.0 = Debug|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Release|Any CPU.Build.0 = Release|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Release|x64.ActiveCfg = Release|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Release|x64.Build.0 = Release|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Release|x86.ActiveCfg = Release|Any CPU + {6BBB590E-E57A-405F-A04F-F3B45379BF89}.Release|x86.Build.0 = Release|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Debug|x64.Build.0 = Debug|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Debug|x86.Build.0 = Debug|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Release|Any CPU.Build.0 = Release|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Release|x64.ActiveCfg = Release|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Release|x64.Build.0 = Release|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Release|x86.ActiveCfg = Release|Any CPU + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6BBB590E-E57A-405F-A04F-F3B45379BF89} = {51414648-A7F8-78F1-F6B9-B622ED145BD3} + {2F27FF7C-A2CD-4D1B-8047-C924E2A59CF2} = {51414648-A7F8-78F1-F6B9-B622ED145BD3} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3665A1AA-3A6F-4EB3-AB3E-808C201DB433} EndGlobalSection