From c8050665e01c1491203705ffc47805bf8c89ec74 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Mon, 12 Jan 2026 16:01:51 -0600 Subject: [PATCH 1/8] Stage 2 Shared UI Migration Complete --- .../Entities/Properties/PropertyCard.razor | 197 +++ .../Entities/Properties/PropertyDetails.razor | 93 ++ .../Entities/Properties/PropertyForm.razor | 237 ++++ .../Entities/Properties/PropertyFormModel.cs | 54 + .../Properties/PropertyListView.razor | 369 +++++ .../Properties/PropertyListViewMode.cs | 17 + .../Notifications/NotificationBell.razor | 2 +- .../Properties/PropertyList.razor | 28 - .../Invoices/CreateForm.razor | 310 ++++ .../Invoices/EditForm.razor | 388 +++++ .../Invoices/ListForm.razor | 585 ++++++++ .../Invoices/ViewForm.razor | 400 ++++++ .../Leases/CreateForm.razor | 371 +++++ .../PropertyManagement/Leases/EditForm.razor | 346 +++++ .../PropertyManagement/Leases/ListForm.razor | 826 +++++++++++ .../PropertyManagement/Leases/ViewForm.razor | 1254 ++++++++++++++++ .../MaintenanceRequests/CreateForm.razor | 344 +++++ .../MaintenanceRequests/EditForm.razor | 301 ++++ .../MaintenanceRequests/ListForm.razor | 345 +++++ .../MaintenanceRequests/ViewForm.razor | 297 ++++ .../Payments/CreateForm.razor | 276 ++++ .../Payments/EditForm.razor | 272 ++++ .../Payments/ListForm.razor | 484 +++++++ .../Payments/ViewForm.razor | 407 ++++++ .../Properties/CreateForm.razor | 64 + .../Properties/EditForm.razor | 186 +++ .../Properties/ListForm.razor | 339 +++++ .../Properties/ViewForm.razor | 551 ++++++++ .../Tenants/CreateForm.razor | 207 +++ .../PropertyManagement/Tenants/EditForm.razor | 323 +++++ .../PropertyManagement/Tenants/ListForm.razor | 502 +++++++ .../PropertyManagement/Tenants/ViewForm.razor | 238 ++++ .../Invoices/Pages/Create.razor | 314 +--- .../Invoices/Pages/Edit.razor | 391 +---- .../Invoices/Pages/Index.razor | 589 +------- .../Invoices/Pages/View.razor | 403 +----- .../Leases/Pages/Create.razor | 380 +---- .../Leases/Pages/Edit.razor | 356 +---- .../Leases/Pages/Index.razor | 834 +---------- .../Leases/Pages/View.razor | 1258 +---------------- .../MaintenanceRequests/Pages/Create.razor | 348 +---- .../MaintenanceRequests/Pages/Edit.razor | 301 +--- .../MaintenanceRequests/Pages/Index.razor | 348 +---- .../MaintenanceRequests/Pages/View.razor | 304 +--- .../MaintenanceRequests/Pages/_Imports.razor | 4 - .../Payments/Pages/Create.razor | 283 +--- .../Payments/Pages/Edit.razor | 269 +--- .../Payments/Pages/Index.razor | 490 +------ .../Payments/Pages/View.razor | 408 +----- .../Payments/Pages/_Imports.razor | 12 - .../Properties/Pages/Create.razor | 259 +--- .../Properties/Pages/Edit.razor | 395 +----- .../Properties/Pages/Index.razor | 556 +------- .../Properties/Pages/View.razor | 620 +------- .../Tenants/Pages/Create.razor | 216 +-- .../Tenants/Pages/Edit.razor | 336 +---- .../Tenants/Pages/Index.razor | 525 +------ .../Tenants/Pages/View.razor | 238 +--- 4-Aquiis.SimpleStart/Features/_Imports.razor | 1 + .../Invoices/Pages/Create.razor | 314 +--- .../Invoices/Pages/Edit.razor | 391 +---- .../Invoices/Pages/Index.razor | 589 +------- .../Invoices/Pages/View.razor | 403 +----- .../Leases/Pages/Create.razor | 380 +---- .../Leases/Pages/Edit.razor | 356 +---- .../Leases/Pages/Index.razor | 834 +---------- .../Leases/Pages/View.razor | 1258 +---------------- .../MaintenanceRequests/Pages/Create.razor | 348 +---- .../MaintenanceRequests/Pages/Edit.razor | 301 +--- .../MaintenanceRequests/Pages/Index.razor | 348 +---- .../MaintenanceRequests/Pages/View.razor | 304 +--- .../MaintenanceRequests/Pages/_Imports.razor | 4 - .../Payments/Pages/Create.razor | 283 +--- .../Payments/Pages/Edit.razor | 269 +--- .../Payments/Pages/Index.razor | 490 +------ .../Payments/Pages/View.razor | 408 +----- .../Payments/Pages/_Imports.razor | 12 - .../Properties/Pages/Create.razor | 259 +--- .../Properties/Pages/Edit.razor | 395 +----- .../Properties/Pages/Index.razor | 556 +------- .../Properties/Pages/View.razor | 620 +------- .../Tenants/Pages/Create.razor | 216 +-- .../Tenants/Pages/Edit.razor | 336 +---- .../Tenants/Pages/Index.razor | 525 +------ .../Tenants/Pages/View.razor | 238 +--- 5-Aquiis.Professional/Features/_Imports.razor | 1 + .../NewSetupUITests.cs | 40 +- .../NewSetupUITests.cs | 40 +- 88 files changed, 10730 insertions(+), 20839 deletions(-) create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyCard.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyForm.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyFormModel.cs create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListViewMode.cs delete mode 100644 3-Aquiis.UI.Shared/Features/PropertiesManagement/Properties/PropertyList.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/CreateForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/EditForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ListForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ViewForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/CreateForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/EditForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/ListForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/ViewForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/CreateForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/EditForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ListForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ViewForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/CreateForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/EditForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/CreateForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/EditForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ListForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ViewForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/CreateForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/EditForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/ListForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/ViewForm.razor delete mode 100644 4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor delete mode 100644 4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/_Imports.razor delete mode 100644 5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor delete mode 100644 5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/_Imports.razor diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyCard.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyCard.razor new file mode 100644 index 0000000..0bee930 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyCard.razor @@ -0,0 +1,197 @@ +@namespace Aquiis.UI.Shared.Components.Entities.Properties +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants + +
+
+
+
@Address
+ + @Status + +
+

@City, @State @ZipCode

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

@Description

+ } + +
+
+ Bedrooms +
@Bedrooms
+
+
+ Bathrooms +
@Bathrooms
+
+
+ Sq Ft +
@SquareFeet.ToString("N0")
+
+
+ +
+ @MonthlyRent.ToString("C") + /month +
+
+ + @if (ShowActions) + { + + } +
+ +@code { + /// + /// Property ID + /// + [Parameter, EditorRequired] + public Guid PropertyId { get; set; } + + /// + /// Property address + /// + [Parameter, EditorRequired] + public string Address { get; set; } = string.Empty; + + /// + /// Unit number (optional) + /// + [Parameter] + public string? UnitNumber { get; set; } + + /// + /// City + /// + [Parameter, EditorRequired] + public string City { get; set; } = string.Empty; + + /// + /// State + /// + [Parameter, EditorRequired] + public string State { get; set; } = string.Empty; + + /// + /// Zip code + /// + [Parameter, EditorRequired] + public string ZipCode { get; set; } = string.Empty; + + /// + /// Property description + /// + [Parameter] + public string? Description { get; set; } + + /// + /// Number of bedrooms + /// + [Parameter] + public int Bedrooms { get; set; } + + /// + /// Number of bathrooms + /// + [Parameter] + public decimal Bathrooms { get; set; } + + /// + /// Square footage + /// + [Parameter] + public int SquareFeet { get; set; } + + /// + /// Monthly rent amount + /// + [Parameter] + public decimal MonthlyRent { get; set; } + + /// + /// Property status + /// + [Parameter, EditorRequired] + public string Status { get; set; } = string.Empty; + + /// + /// Whether to show action buttons + /// + [Parameter] + public bool ShowActions { get; set; } = true; + + /// + /// Whether to show edit/delete buttons (requires ShowActions = true) + /// + [Parameter] + public bool ShowEditDelete { get; set; } = true; + + /// + /// Callback when View button is clicked + /// + [Parameter] + public EventCallback OnView { get; set; } + + /// + /// Callback when Edit button is clicked + /// + [Parameter] + public EventCallback OnEdit { get; set; } + + /// + /// Callback when Delete button is clicked + /// + [Parameter] + public EventCallback OnDelete { get; set; } + + private string DisplayAddress => string.IsNullOrWhiteSpace(UnitNumber) + ? Address + : $"{Address}, {UnitNumber}"; + + private async Task HandleView() + { + await OnView.InvokeAsync(PropertyId); + } + + private async Task HandleEdit() + { + await OnEdit.InvokeAsync(PropertyId); + } + + private async Task HandleDelete() + { + await OnDelete.InvokeAsync(PropertyId); + } + + private string GetStatusBadgeClass() + { + 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" + }; + } +} diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor new file mode 100644 index 0000000..a5a7199 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor @@ -0,0 +1,93 @@ +@namespace Aquiis.UI.Shared.Components.Entities.Properties +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants + +
+
+
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

+
+
+ } + + @if (ShowDates) + { +
+
+ Created: +

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

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

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

+
+ } +
+ } +
+
+ +@code { + /// + /// Property entity to display + /// + [Parameter, EditorRequired] + public Property Property { get; set; } = default!; + + /// + /// Whether to show created/modified dates + /// + [Parameter] + public bool ShowDates { get; set; } = true; + + private string GetAvailabilityBadgeClass() + { + return Property.IsAvailable ? "bg-success" : "bg-warning"; + } +} diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyForm.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyForm.razor new file mode 100644 index 0000000..8698aee --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyForm.razor @@ -0,0 +1,237 @@ +@namespace Aquiis.UI.Shared.Components.Entities.Properties +@using Aquiis.Core.Constants +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations + + + + + @if (!string.IsNullOrEmpty(ErrorMessage)) + { + + } + + @if (!string.IsNullOrEmpty(SuccessMessage)) + { + + } + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + @foreach (var state in States.StatesArray()) + { + + } + + +
+
+ +
+
+ + + + + + + + + + + + +
+
+ + + +
+
+ +
+
+ + + + + + + + + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+
+ + +
+
+
+ +
+ + @if (ShowCancelButton) + { + + } + @if (ShowViewButton && OnView.HasDelegate) + { + + } +
+
+ +@code { + /// + /// Form model containing property data + /// + [Parameter, EditorRequired] + public PropertyFormModel Model { get; set; } = new(); + + /// + /// Form name for SSR scenarios + /// + [Parameter] + public string FormName { get; set; } = "property-form"; + + /// + /// Text for submit button + /// + [Parameter] + public string SubmitButtonText { get; set; } = "Save Property"; + + /// + /// Whether form is currently submitting + /// + [Parameter] + public bool IsSubmitting { get; set; } + + /// + /// Error message to display + /// + [Parameter] + public string ErrorMessage { get; set; } = string.Empty; + + /// + /// Success message to display + /// + [Parameter] + public string SuccessMessage { get; set; } = string.Empty; + + /// + /// Whether to show cancel button + /// + [Parameter] + public bool ShowCancelButton { get; set; } = true; + + /// + /// Whether to show view button (for edit mode) + /// + [Parameter] + public bool ShowViewButton { get; set; } = false; + + /// + /// Callback when form is validly submitted + /// + [Parameter] + public EventCallback OnValidSubmit { get; set; } + + /// + /// Callback when cancel is clicked + /// + [Parameter] + public EventCallback OnCancel { get; set; } + + /// + /// Callback when view is clicked + /// + [Parameter] + public EventCallback OnView { get; set; } + + private async Task HandleValidSubmit() + { + await OnValidSubmit.InvokeAsync(Model); + } + + private async Task HandleCancel() + { + await OnCancel.InvokeAsync(); + } + + private async Task HandleView() + { + await OnView.InvokeAsync(); + } +} diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyFormModel.cs b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyFormModel.cs new file mode 100644 index 0000000..c7a1075 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyFormModel.cs @@ -0,0 +1,54 @@ +using System.ComponentModel.DataAnnotations; +using Aquiis.Core.Constants; + +namespace Aquiis.UI.Shared.Components.Entities.Properties; + +/// +/// Form model for property create/edit operations +/// +public class PropertyFormModel +{ + [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")] + [DataType(DataType.PostalCode)] + [RegularExpression(@"^\d{5}(-\d{4})?$", ErrorMessage = "Invalid Zip Code format.")] + 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; +} diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor new file mode 100644 index 0000000..1a6ca38 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor @@ -0,0 +1,369 @@ +@namespace Aquiis.UI.Shared.Components.Entities.Properties +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants + +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +@if (ShowMetrics) +{ +
+
+
+
+
Available
+

@AvailableCount

+
+
+
+
+
+
+
Pending Lease
+

@PendingCount

+
+
+
+
+
+
+
Occupied
+

@OccupiedCount

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

@TotalMonthlyRent.ToString("C")

+
+
+
+
+} + +@if (ViewMode == PropertyListViewMode.Grid) +{ + +
+ @foreach (var property in Properties) + { +
+ +
+ } +
+} +else +{ + +
+
+
+ + + + + + + + + + + + + + + + @foreach (var property in Properties) + { + + + + + + + + + + + + } + +
+ 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 (ShowEditDelete) + { + + + } +
+
+
+
+
+} + +@code { + /// + /// List of properties to display + /// + [Parameter, EditorRequired] + public List Properties { get; set; } = new(); + + /// + /// View mode (Grid or List/Table) + /// + [Parameter] + public PropertyListViewMode ViewMode { get; set; } = PropertyListViewMode.Grid; + + /// + /// Whether to show metrics cards + /// + [Parameter] + public bool ShowMetrics { get; set; } = true; + + /// + /// Whether to show edit/delete buttons + /// + [Parameter] + public bool ShowEditDelete { get; set; } = true; + + /// + /// Current search term + /// + [Parameter] + public string SearchTerm { get; set; } = string.Empty; + + /// + /// Current status filter + /// + [Parameter] + public string StatusFilter { get; set; } = string.Empty; + + /// + /// Current sort column + /// + [Parameter] + public string SortColumn { get; set; } = nameof(Property.Address); + + /// + /// Sort direction + /// + [Parameter] + public bool SortAscending { get; set; } = true; + + /// + /// Available properties count (for metrics) + /// + [Parameter] + public int AvailableCount { get; set; } + + /// + /// Pending properties count (for metrics) + /// + [Parameter] + public int PendingCount { get; set; } + + /// + /// Occupied properties count (for metrics) + /// + [Parameter] + public int OccupiedCount { get; set; } + + /// + /// Total monthly rent (for metrics) + /// + [Parameter] + public decimal TotalMonthlyRent { get; set; } + + /// + /// Callback when search term changes + /// + [Parameter] + public EventCallback OnSearchChanged { get; set; } + + /// + /// Callback when status filter changes + /// + [Parameter] + public EventCallback OnStatusFilterChanged { get; set; } + + /// + /// Callback when filters are cleared + /// + [Parameter] + public EventCallback OnClearFilters { get; set; } + + /// + /// Callback when sort is requested + /// + [Parameter] + public EventCallback<(string Column, bool Ascending)> OnSort { get; set; } + + /// + /// Callback when View button is clicked + /// + [Parameter] + public EventCallback OnView { get; set; } + + /// + /// Callback when Edit button is clicked + /// + [Parameter] + public EventCallback OnEdit { get; set; } + + /// + /// Callback when Delete button is clicked + /// + [Parameter] + public EventCallback OnDelete { get; set; } + + private async Task HandleSearchChanged(ChangeEventArgs e) + { + await OnSearchChanged.InvokeAsync(e.Value?.ToString() ?? string.Empty); + } + + private async Task HandleStatusFilterChanged(ChangeEventArgs e) + { + await OnStatusFilterChanged.InvokeAsync(e.Value?.ToString() ?? string.Empty); + } + + private async Task HandleSort(string column) + { + var ascending = SortColumn == column ? !SortAscending : true; + await OnSort.InvokeAsync((column, ascending)); + } + + private async Task HandleView(Guid propertyId) + { + await OnView.InvokeAsync(propertyId); + } + + private async Task HandleEdit(Guid propertyId) + { + await OnEdit.InvokeAsync(propertyId); + } + + private async Task HandleDelete(Guid propertyId) + { + await OnDelete.InvokeAsync(propertyId); + } + + 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/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListViewMode.cs b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListViewMode.cs new file mode 100644 index 0000000..bf96091 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListViewMode.cs @@ -0,0 +1,17 @@ +namespace Aquiis.UI.Shared.Components.Entities.Properties; + +/// +/// Defines the display mode for property list views +/// +public enum PropertyListViewMode +{ + /// + /// Display properties in a grid of cards + /// + Grid, + + /// + /// Display properties in a table/list format + /// + Table +} diff --git a/3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor b/3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor index e69c61a..b377d96 100644 --- a/3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor +++ b/3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor @@ -3,7 +3,7 @@ @namespace Aquiis.UI.Shared.Features.Notifications @using Aquiis.Application.Services -Notification Bell +@* Notification Bell *@ @if (isLoading) { diff --git a/3-Aquiis.UI.Shared/Features/PropertiesManagement/Properties/PropertyList.razor b/3-Aquiis.UI.Shared/Features/PropertiesManagement/Properties/PropertyList.razor deleted file mode 100644 index 80f9762..0000000 --- a/3-Aquiis.UI.Shared/Features/PropertiesManagement/Properties/PropertyList.razor +++ /dev/null @@ -1,28 +0,0 @@ -@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/Features/PropertyManagement/Invoices/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/CreateForm.razor new file mode 100644 index 0000000..14b7b7c --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/CreateForm.razor @@ -0,0 +1,310 @@ +@using Aquiis.Core.Entities +@using Aquiis.Core.Validation +@using Aquiis.Application.Services +@using Microsoft.AspNetCore.Components.Authorization +@using System.ComponentModel.DataAnnotations +@inject NavigationManager NavigationManager +@inject InvoiceService InvoiceService +@inject LeaseService LeaseService +@rendermode InteractiveServer + +
+

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/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/EditForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/EditForm.razor new file mode 100644 index 0000000..51e2be7 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/EditForm.razor @@ -0,0 +1,388 @@ +@using Aquiis.Core.Entities +@using Aquiis.Core.Validation +@using Aquiis.Application.Services +@using Microsoft.AspNetCore.Components.Authorization +@using System.ComponentModel.DataAnnotations +@inject NavigationManager NavigationManager +@inject InvoiceService InvoiceService +@inject LeaseService LeaseService +@rendermode InteractiveServer + +
+

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 InvoiceId { 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(InvoiceId); + + 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/{InvoiceId}"); + } + + private void ViewLease() + { + if (invoice?.LeaseId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{invoice.LeaseId}"); + } + } + + private void RecordPayment() + { + NavigationManager.NavigateTo($"/propertymanagement/payments/create?invoiceId={InvoiceId}"); + } + + 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/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ListForm.razor new file mode 100644 index 0000000..7e6e6ed --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ListForm.razor @@ -0,0 +1,585 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Microsoft.AspNetCore.Components.Authorization +@inject NavigationManager Navigation +@inject InvoiceService InvoiceService +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +
+

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/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ViewForm.razor new file mode 100644 index 0000000..b1564f9 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ViewForm.razor @@ -0,0 +1,400 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Microsoft.AspNetCore.Components.Authorization +@inject NavigationManager NavigationManager +@inject InvoiceService InvoiceService +@inject DocumentService DocumentService +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +@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 InvoiceId { 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(InvoiceId); + + // 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/{InvoiceId}/edit"); + } + + private void RecordPayment() + { + NavigationManager.NavigateTo($"/propertymanagement/payments/create?invoiceId={InvoiceId}"); + } + + 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/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/CreateForm.razor new file mode 100644 index 0000000..64e8c57 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/CreateForm.razor @@ -0,0 +1,371 @@ +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.Core.Validation +@using Aquiis.Application.Services +@using Microsoft.AspNetCore.Components.Authorization +@using System.ComponentModel.DataAnnotations +@inject NavigationManager Navigation +@inject OrganizationService OrganizationService +@inject LeaseService LeaseService +@inject PropertyService PropertyService +@inject TenantService TenantService +@rendermode InteractiveServer + +
+
+
+
+

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/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/EditForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/EditForm.razor new file mode 100644 index 0000000..aec1bd3 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/EditForm.razor @@ -0,0 +1,346 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Microsoft.AspNetCore.Components.Authorization +@using System.ComponentModel.DataAnnotations +@inject NavigationManager Navigation +@inject LeaseService LeaseService +@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 +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + + + + + + +
+
+ +
+
+ + + +
+
+ + @* NotesTimeline removed - product-specific component *@ + +
+ + + +
+
+
+
+
+ +
+
+
+
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 LeaseId { 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(LeaseId); + + 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 != LeaseId && 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/{LeaseId}"); + } + + private void CreateInvoice() + { + Navigation.NavigateTo($"/propertymanagement/invoices/create?leaseId={LeaseId}"); + } + + 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 != LeaseId && 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/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/ListForm.razor new file mode 100644 index 0000000..6e2bc55 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/ListForm.razor @@ -0,0 +1,826 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@inject NavigationManager NavigationManager +@inject LeaseService LeaseService +@inject TenantService TenantService +@inject PropertyService PropertyService +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +
+
+

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/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/ViewForm.razor new file mode 100644 index 0000000..f962865 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Leases/ViewForm.razor @@ -0,0 +1,1254 @@ +@using Aquiis.Core.Entities +@using Aquiis.Core.Validation +@using Aquiis.Application.Services +@using Aquiis.Application.Services.Workflows +@using Aquiis.Application.Services.PdfGenerators +@using Microsoft.AspNetCore.Components.Authorization +@using System.ComponentModel.DataAnnotations +@inject NavigationManager Navigation +@inject LeaseService LeaseService +@inject InvoiceService InvoiceService +@inject DocumentService DocumentService +@inject LeaseWorkflowService LeaseWorkflowService +@inject LeaseRenewalPdfGenerator RenewalPdfGenerator +@inject OrganizationService OrganizationService +@inject IJSRuntime JSRuntime +@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
+
+
+ @* NotesTimeline removed - product-specific component *@ +

Notes feature is product-specific and will be rendered by the product wrapper.

+
+
+
+
+ @* 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 LeaseId { 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(LeaseId); + + if (lease == null) + { + isAuthorized = false; + return; + } + + var invoices = await InvoiceService.GetInvoicesByLeaseIdAsync(LeaseId); + 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(); + } + catch (Exception ex) + { + // Error handling: Could be logged or passed to parent component + throw ex; + } + } + + 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); + + // PDF generated successfully + } + catch (Exception ex) + { + // Error handling: Could be logged or passed to parent component + throw ex; + } + 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(LeaseId, renewalModel); + + if (result.Success && result.Data != null) + { + await LoadLease(); + StateHasChanged(); + + // Renewal accepted successfully + } + else + { + // Validation errors: Could be displayed in UI + } + } + catch (Exception ex) + { + // Error handling: Could be logged or passed to parent component + throw ex; + } + } + + private async Task MarkRenewalDeclined() + { + if (lease == null) return; + + try + { + lease.RenewalStatus = "Declined"; + lease.RenewalResponseOn = DateTime.UtcNow; + await LeaseService.UpdateAsync(lease); + await LoadLease(); + StateHasChanged(); + + // Renewal offer marked as declined + } + catch (Exception ex) + { + // Error handling: Could be logged or passed to parent component + throw ex; + } + } + + #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( + LeaseId, + terminationNoticeDate, + terminationExpectedMoveOutDate, + terminationNoticeType, + terminationReason); + + if (result.Success) + { + showTerminationNoticeModal = false; + ResetTerminationNoticeForm(); + await LoadLease(); + } + else + { + // Validation errors: Could be displayed in UI + } + } + catch (Exception ex) + { + // Error handling: Could be logged or passed to parent component + throw ex; + } + 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( + LeaseId, + earlyTerminationType, + earlyTerminationReason, + earlyTerminationDate); + + if (result.Success) + { + showEarlyTerminationModal = false; + ResetEarlyTerminationForm(); + await LoadLease(); + } + else + { + // Validation errors: Could be displayed in UI + } + } + catch (Exception ex) + { + // Error handling: Could be logged or passed to parent component + throw ex; + } + 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( + LeaseId, + actualMoveOutDate, + moveOutModel); + + if (result.Success) + { + showMoveOutModal = false; + ResetMoveOutForm(); + await LoadLease(); + } + else + { + // Validation errors: Could be displayed in UI + } + } + catch (Exception ex) + { + // Error handling: Could be logged or passed to parent component + throw ex; + } + finally + { + isSubmitting = false; + StateHasChanged(); + } + } + + private async Task ConvertToMonthToMonth() + { + if (lease == null) return; + + isSubmitting = true; + StateHasChanged(); + + try + { + var result = await LeaseWorkflowService.ConvertToMonthToMonthAsync( + LeaseId, + mtmNewRent); + + if (result.Success) + { + showConvertMTMModal = false; + mtmNewRent = null; + await LoadLease(); + } + else + { + // Validation errors: Could be displayed in UI + } + } + catch (Exception ex) + { + // Error handling: Could be logged or passed to parent component + throw ex; + } + 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/{LeaseId}/edit"); + } + + private void BackToList() + { + Navigation.NavigateTo("/propertymanagement/leases"); + } + + private void CreateInvoice() + { + Navigation.NavigateTo($"/propertymanagement/invoices/create?leaseId={LeaseId}"); + } + + private void ViewInvoices() + { + Navigation.NavigateTo($"/propertymanagement/invoices?leaseId={LeaseId}"); + } + + private void ViewDocuments() + { + Navigation.NavigateTo($"/propertymanagement/leases/{LeaseId}/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/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/CreateForm.razor new file mode 100644 index 0000000..44eda0f --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/CreateForm.razor @@ -0,0 +1,344 @@ +@using System.ComponentModel.DataAnnotations +@inject MaintenanceService MaintenanceService +@inject PropertyService PropertyService +@inject LeaseService LeaseService +@inject NavigationManager NavigationManager + +
+
+

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/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/EditForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/EditForm.razor new file mode 100644 index 0000000..1fb83ce --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/EditForm.razor @@ -0,0 +1,301 @@ +@inject MaintenanceService MaintenanceService +@inject PropertyService PropertyService +@inject LeaseService LeaseService +@inject NavigationManager NavigationManager +@inject IJSRuntime JSRuntime + +
+
+

Edit Maintenance Request #@MaintenanceRequestId

+ +
+ + @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 MaintenanceRequestId { 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(MaintenanceRequestId); + 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/{MaintenanceRequestId}"); + } + 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(MaintenanceRequestId); + NavigationManager.NavigateTo("/propertymanagement/maintenance"); + } + } + + private void Cancel() + { + NavigationManager.NavigateTo($"/propertymanagement/maintenance/{MaintenanceRequestId}"); + } +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ListForm.razor new file mode 100644 index 0000000..883b7ff --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ListForm.razor @@ -0,0 +1,345 @@ +@inject MaintenanceService MaintenanceService +@inject NavigationManager NavigationManager + +
+

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/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ViewForm.razor new file mode 100644 index 0000000..fdc81b2 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ViewForm.razor @@ -0,0 +1,297 @@ +@inject MaintenanceService MaintenanceService +@inject NavigationManager NavigationManager + +@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 MaintenanceRequestId { 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(MaintenanceRequestId); + } + finally + { + isLoading = false; + } + } + + private async Task UpdateStatus(string newStatus) + { + if (maintenanceRequest != null) + { + await MaintenanceService.UpdateMaintenanceRequestStatusAsync(maintenanceRequest.Id, newStatus); + // TODO: Add toast notification for status update + await LoadMaintenanceRequest(); + } + } + + private void Edit() + { + NavigationManager.NavigateTo($"/propertymanagement/maintenance/{MaintenanceRequestId}/edit"); + } + + private void ViewProperty() + { + if (maintenanceRequest?.PropertyId != null) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/{maintenanceRequest.PropertyId}"); + } + } + + private void GoBack() + { + NavigationManager.NavigateTo("/propertymanagement/maintenance"); + } +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/CreateForm.razor new file mode 100644 index 0000000..223fa75 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/CreateForm.razor @@ -0,0 +1,276 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations +@inject NavigationManager Navigation +@inject PaymentService PaymentService +@inject InvoiceService InvoiceService + +
+
+
+

Record Payment

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

No Unpaid Invoices

+

There are no outstanding invoices to record payments for.

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

@selectedInvoice.InvoiceNumber

+
+
+ +

@selectedInvoice.Lease?.Property?.Address

+
+
+ +

@selectedInvoice.Lease?.Tenant?.FullName

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

Select an invoice to see details

+
+
+ } +
+
+ +@code { + private List? invoices; + private Invoice? selectedInvoice; + private PaymentModel paymentModel = new(); + private decimal remainingBalance => selectedInvoice != null ? selectedInvoice.BalanceDue - paymentModel.Amount : 0; + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } = default!; + + [Parameter] + [SupplyParameterFromQuery] + public Guid? InvoiceId { get; set; } + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateTask; + var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + + // Get all invoices and filter to those with outstanding balance + List? allInvoices = await InvoiceService.GetAllAsync(); + invoices = allInvoices + .Where(i => i.BalanceDue > 0 && i.Status != "Cancelled") + .OrderByDescending(i => i.DueOn) + .ToList(); + + paymentModel.PaidOn = DateTime.Now; + if (InvoiceId.HasValue) + { + paymentModel.InvoiceId = InvoiceId.Value; + await OnInvoiceSelected(); + } + } + + private async Task OnInvoiceSelected() + { + if (paymentModel.InvoiceId != Guid.Empty) + { + selectedInvoice = invoices?.FirstOrDefault(i => i.Id == paymentModel.InvoiceId); + if (selectedInvoice != null) + { + // Default payment amount to the balance due + paymentModel.Amount = selectedInvoice.BalanceDue; + } + } + else + { + selectedInvoice = null; + paymentModel.Amount = 0; + } + + await InvokeAsync(StateHasChanged); + } + + private async Task HandleCreatePayment() + { + Payment payment = new Payment + { + InvoiceId = paymentModel.InvoiceId, + PaidOn = paymentModel.PaidOn, + Amount = paymentModel.Amount, + PaymentMethod = paymentModel.PaymentMethod, + Notes = paymentModel.Notes! + }; + await PaymentService.CreateAsync(payment); + Navigation.NavigateTo("/propertymanagement/payments"); + } + + private void Cancel() + { + Navigation.NavigateTo("/propertymanagement/payments"); + } + + public class PaymentModel + { + [Required(ErrorMessage = "Please select an invoice.")] + public Guid InvoiceId { get; set; } + + [Required(ErrorMessage = "Payment date is required.")] + public DateTime PaidOn { get; set; } = DateTime.Now; + + [Required(ErrorMessage = "Amount is required.")] + [Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero.")] + public decimal Amount { get; set; } + + [Required(ErrorMessage = "Payment method is required.")] + public string PaymentMethod { get; set; } = string.Empty; + + [MaxLength(1000)] + public string? Notes { get; set; } = string.Empty; + } +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/EditForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/EditForm.razor new file mode 100644 index 0000000..93914cb --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/EditForm.razor @@ -0,0 +1,272 @@ +@using Aquiis.Application.Services +@using Aquiis.Core.Entities +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using System.ComponentModel.DataAnnotations +@inject NavigationManager Navigation +@inject PaymentService PaymentService + +@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/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor new file mode 100644 index 0000000..db106a2 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor @@ -0,0 +1,484 @@ +@using Aquiis.Core.Entities +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@inject NavigationManager Navigation +@inject PaymentService PaymentService +@inject IJSRuntime JSRuntime + +
+

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/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor new file mode 100644 index 0000000..b67c38c --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor @@ -0,0 +1,407 @@ +@using Aquiis.Core.Entities +@using Microsoft.AspNetCore.Components.Authorization +@inject NavigationManager Navigation +@inject PaymentService PaymentService +@inject Application.Services.DocumentService DocumentService +@inject IJSRuntime JSRuntime + +@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/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/CreateForm.razor new file mode 100644 index 0000000..0e3b121 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/CreateForm.razor @@ -0,0 +1,64 @@ +@using Aquiis.Core.Constants +@using Aquiis.Core.Entities +@using Aquiis.UI.Shared.Components.Entities.Properties +@inject NavigationManager Navigation +@inject PropertyService PropertyService +@namespace Aquiis.UI.Shared.Features.PropertyManagement.Properties + +
+
+
+
+

Add New Property

+
+
+ +
+
+
+
+ +@code { + private PropertyFormModel propertyModel = new(); + private bool isSubmitting = false; + private string errorMessage = string.Empty; + + private async Task SaveProperty(PropertyFormModel model) + { + isSubmitting = true; + errorMessage = string.Empty; + + var property = new Property + { + Address = model.Address, + UnitNumber = model.UnitNumber, + City = model.City, + State = model.State, + ZipCode = model.ZipCode, + PropertyType = model.PropertyType, + MonthlyRent = model.MonthlyRent, + Bedrooms = model.Bedrooms, + Bathrooms = model.Bathrooms, + SquareFeet = model.SquareFeet, + Description = model.Description, + Status = model.Status, + IsAvailable = model.IsAvailable, + }; + + await PropertyService.CreateAsync(property); + isSubmitting = false; + Navigation.NavigateTo("/propertymanagement/properties"); + } + + private void Cancel() + { + Navigation.NavigateTo("/propertymanagement/properties"); + } +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/EditForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/EditForm.razor new file mode 100644 index 0000000..4930bd4 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/EditForm.razor @@ -0,0 +1,186 @@ +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@using Aquiis.UI.Shared.Components.Entities.Properties +@using Microsoft.AspNetCore.Components.Authorization +@inject PropertyService PropertyService +@inject NavigationManager NavigationManager +@namespace Aquiis.UI.Shared.Features.PropertyManagement.Properties + +@if (property == null) +{ +
+
+ Loading... +
+
+} +else if (!isAuthorized) +{ +
+

Access Denied

+

You don't have permission to edit this property.

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

Edit Property

+
+
+ +
+
+
+ +
+
+
+
Property Actions
+
+
+
+ + +
+
+
+ + +
+
+} + +@code { + [Parameter] + public Guid PropertyId { get; set; } + + private string errorMessage = string.Empty; + private Property? property; + private PropertyFormModel 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; + } + + propertyModel = new PropertyFormModel + { + 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 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(PropertyFormModel model) + { + if (property != null) + { + try + { + isSubmitting = true; + errorMessage = string.Empty; + successMessage = string.Empty; + + property.Address = model.Address; + property.UnitNumber = model.UnitNumber; + property.City = model.City; + property.State = model.State; + property.ZipCode = model.ZipCode; + property.PropertyType = model.PropertyType; + property.MonthlyRent = model.MonthlyRent; + property.Bedrooms = model.Bedrooms; + property.Bathrooms = model.Bathrooms; + property.SquareFeet = model.SquareFeet; + property.Description = model.Description; + property.Status = model.Status; + property.IsAvailable = model.IsAvailable; + + await PropertyService.UpdateAsync(property); + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + catch (Exception ex) + { + errorMessage = $"An error occurred while updating the property: {ex.Message}"; + } + finally + { + isSubmitting = false; + } + } + } + + private void Cancel() + { + NavigationManager.NavigateTo("/propertymanagement/properties"); + } +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ListForm.razor new file mode 100644 index 0000000..31d8513 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ListForm.razor @@ -0,0 +1,339 @@ +@using Aquiis.Core.Constants +@using Aquiis.Core.Entities +@using Aquiis.UI.Shared.Components.Entities.Properties +@using Microsoft.AspNetCore.Components.Authorization +@using System.Security.Claims +@inject NavigationManager Navigation +@inject PropertyService PropertyService +@inject IJSRuntime JSRuntime +@namespace Aquiis.UI.Shared.Features.PropertyManagement.Properties + +
+

Properties

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

No Properties Found

+

@(properties.Any() ? "No properties match your search criteria." : "Get started by adding your first property to the system.")

+ @if (!properties.Any()) + { + + } +
+} +else +{ + + + @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 PropertyListViewMode viewMode = PropertyListViewMode.Table; + + // 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() + { + var authState = await AuthenticationStateTask; + var roleClaim = authState.User.FindFirst(ClaimTypes.Role); + currentUserRole = roleClaim?.Value; + + 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); + await LoadProperties(); + FilterProperties(); + CalculateMetrics(); + } + + private void ClearFilters() + { + searchTerm = string.Empty; + selectedPropertyStatus = string.Empty; + FilterProperties(); + } + + private void SetViewMode(PropertyListViewMode mode) + { + viewMode = mode; + } + + private void HandleSearchChanged(string search) + { + searchTerm = search; + FilterProperties(); + } + + private void HandleStatusFilterChanged(string status) + { + selectedPropertyStatus = status; + FilterProperties(); + } + + private void HandleClearFilters() + { + ClearFilters(); + } + + private void HandleSort((string Column, bool Ascending) sortInfo) + { + sortColumn = sortInfo.Column; + sortAscending = sortInfo.Ascending; + SortAndPaginateProperties(); + } + + private void SortAndPaginateProperties() + { + 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() + }; + + 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(); + } +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ViewForm.razor new file mode 100644 index 0000000..eaa3824 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ViewForm.razor @@ -0,0 +1,551 @@ +@using Aquiis.Core.Constants +@using Aquiis.Core.Entities +@using Microsoft.AspNetCore.Components.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 +@namespace Aquiis.UI.Shared.Features.PropertyManagement.Properties + +@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(); + if (lastInspection != null) + { + + + 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; } + + private Guid LeaseId { get; set; } + private List activeLeases = new(); + private List propertyDocuments = new(); + private List maintenanceRequests = new(); + private List propertyInspections = new(); + private 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; + } + + propertyDocuments = await DocumentService.GetDocumentsByPropertyIdAsync(PropertyId); + propertyDocuments = propertyDocuments.Where(d => !d.IsDeleted).ToList(); + + maintenanceRequests = await MaintenanceService.GetMaintenanceRequestsByPropertyAsync(PropertyId); + propertyInspections = await InspectionService.GetByPropertyIdAsync(PropertyId); + + 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 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}"); + + 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) + { + await JSRuntime.InvokeVoidAsync("downloadFile", doc.FileName, Convert.ToBase64String(doc.FileData), doc.FileType); + } + + private string GetFileIcon(string extension) => 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) => 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) => status switch + { + "Overdue" => "bg-danger", + "Due Soon" => "bg-warning", + "Scheduled" => "bg-success", + "Not Scheduled" => "bg-secondary", + _ => "bg-secondary" + }; + + private string GetChecklistStatusBadge(string status) => status switch + { + "Completed" => "bg-success", + "In Progress" => "bg-warning", + "Draft" => "bg-secondary", + _ => "bg-secondary" + }; +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/CreateForm.razor new file mode 100644 index 0000000..82eae13 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/CreateForm.razor @@ -0,0 +1,207 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Components.Authorization +@inject TenantService TenantService +@inject NavigationManager NavigationManager +@rendermode InteractiveServer + +

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."; + 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}"; + 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); + NavigationManager.NavigateTo("/propertymanagement/tenants"); + } + catch (Exception ex) + { + errorMessage = $"Error creating 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; + } +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/EditForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/EditForm.razor new file mode 100644 index 0000000..0d46b63 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/EditForm.razor @@ -0,0 +1,323 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Components.Authorization +@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 TenantId { 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(TenantId); + + if (tenant == null) + { + isAuthorized = false; + return; + } + + 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() + { + try + { + isSubmitting = true; + errorMessage = string.Empty; + successMessage = string.Empty; + + 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/{TenantId}"); + } + + private void CreateLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/create?tenantId={TenantId}"); + } + + 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; + } +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/ListForm.razor new file mode 100644 index 0000000..17af8a9 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/ListForm.razor @@ -0,0 +1,502 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using System.Security.Claims +@using Microsoft.AspNetCore.Components.Authorization +@inject TenantService TenantService +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@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; + 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 == "User"; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateTask; + currentUserRole = authState.User.FindFirst(ClaimTypes.Role)?.Value; + + await LoadTenants(); + FilterTenants(); + CalculateMetrics(); + } + + private async Task LoadTenants() + { + 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 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); + + if (currentPage > totalPages && totalPages > 0) + { + currentPage = totalPages; + } + else if (currentPage < 1) + { + currentPage = 1; + } + + 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(); + } + } +} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/ViewForm.razor new file mode 100644 index 0000000..fe47176 --- /dev/null +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/ViewForm.razor @@ -0,0 +1,238 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Microsoft.AspNetCore.Components.Authorization +@inject NavigationManager NavigationManager +@inject TenantService TenantService +@inject LeaseService LeaseService +@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 TenantId { 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(TenantId); + + if (tenant == null) + { + isAuthorized = false; + return; + } + + tenantLeases = await LeaseService.GetLeasesByTenantIdAsync(TenantId); + } + + private void EditTenant() + { + NavigationManager.NavigateTo($"/propertymanagement/tenants/{TenantId}/edit"); + } + + private void BackToList() + { + NavigationManager.NavigateTo("/propertymanagement/tenants"); + } + + private void CreateLease() + { + NavigationManager.NavigateTo($"/propertymanagement/leases/create?tenantId={TenantId}"); + } + + private void ViewLeases() + { + NavigationManager.NavigateTo($"/propertymanagement/leases?tenantId={TenantId}"); + } + + private void ViewDocuments() + { + NavigationManager.NavigateTo($"/propertymanagement/documents?tenantId={TenantId}"); + } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Create.razor index b022f89..7472452 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Create.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Create.razor @@ -1,317 +1,5 @@ @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 index 0dc81cd..4b5ce36 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Edit.razor @@ -1,396 +1,9 @@ @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; } - } + [Parameter] public Guid Id { get; set; } } diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Index.razor index 7f12872..e28d304 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Index.razor @@ -1,592 +1,5 @@ @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 index bb72d9b..4a11104 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/View.razor @@ -1,408 +1,9 @@ @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(); - } - } + [Parameter] public Guid Id { get; set; } } diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Create.razor index 3e6b16b..bc6dbce 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Create.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Create.razor @@ -1,383 +1,5 @@ @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 index fd234a1..869e570 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Edit.razor @@ -1,357 +1,9 @@ -@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 - +@page "/propertymanagement/leases/{Id:guid}/edit" +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @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 index 4639e55..94b987e 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Index.razor @@ -1,837 +1,5 @@ @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 index dee2f21..ee2d8bc 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor @@ -1,1263 +1,9 @@ @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 index f66997f..02faf70 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor @@ -1,354 +1,12 @@ -@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 - +@page "/propertymanagement/maintenance/create/{PropertyId:guid?}" +@using Aquiis.UI.Shared.Features.PropertyManagement.MaintenanceRequests @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 index 81a664a..fb4c298 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor @@ -1,306 +1,13 @@ -@page "/propertymanagement/maintenance/{Id:guid}/edit" -@inject MaintenanceService MaintenanceService -@inject PropertyService PropertyService -@inject LeaseService LeaseService -@inject NavigationManager NavigationManager -@inject IJSRuntime JSRuntime +@page "/propertymanagement/maintenance/{MaintenanceRequestId:guid}/edit" +@using Aquiis.UI.Shared.Features.PropertyManagement.MaintenanceRequests @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}"); - } + public Guid MaintenanceRequestId { get; set; } } diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor index c0f61d9..396f7d3 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor @@ -1,350 +1,6 @@ @page "/propertymanagement/maintenance" -@inject MaintenanceService MaintenanceService -@inject NavigationManager NavigationManager +@using Aquiis.UI.Shared.Features.PropertyManagement.MaintenanceRequests @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 index b6aa0f3..585827c 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor @@ -1,309 +1,13 @@ -@page "/propertymanagement/maintenance/{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 - +@page "/propertymanagement/maintenance/{MaintenanceRequestId:guid}" +@using Aquiis.UI.Shared.Features.PropertyManagement.MaintenanceRequests @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"); - } + public Guid MaintenanceRequestId { get; set; } } diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor deleted file mode 100644 index 538a4b3..0000000 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor +++ /dev/null @@ -1,4 +0,0 @@ -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Aquiis.Core.Entities -@using Aquiis.Core.Constants -@using Microsoft.AspNetCore.Authorization diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Create.razor index 0b9cd96..87052f9 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Create.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Create.razor @@ -1,285 +1,6 @@ @page "/propertymanagement/payments/create" -@using Aquiis.SimpleStart.Features.PropertyManagement -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.Forms -@using System.ComponentModel.DataAnnotations -@using Microsoft.AspNetCore.Http.HttpResults -@inject NavigationManager Navigation -@inject PaymentService PaymentService -@inject InvoiceService InvoiceService - +@using Aquiis.UI.Shared.Features.PropertyManagement.Payments @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer -Record Payment - Property Management - -
-
-
-

Record Payment

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

No Unpaid Invoices

-

There are no outstanding invoices to record payments for.

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

@selectedInvoice.InvoiceNumber

-
-
- -

@selectedInvoice.Lease?.Property?.Address

-
-
- -

@selectedInvoice.Lease?.Tenant?.FullName

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

Select an invoice to see details

-
-
- } -
-
- -@code { - private List? invoices; - private Invoice? selectedInvoice; - private PaymentModel paymentModel = new(); - private decimal remainingBalance => selectedInvoice != null ? selectedInvoice.BalanceDue - paymentModel.Amount : 0; - - [CascadingParameter] - private Task AuthenticationStateTask { get; set; } = default!; - - [Parameter] - [SupplyParameterFromQuery] - public Guid? InvoiceId { get; set; } - - protected override async Task OnInitializedAsync() - { - var authState = await AuthenticationStateTask; - var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; - - - // Get all invoices and filter to those with outstanding balance - List? allInvoices = await InvoiceService.GetAllAsync(); - invoices = allInvoices - .Where(i => i.BalanceDue > 0 && i.Status != "Cancelled") - .OrderByDescending(i => i.DueOn) - .ToList(); - - paymentModel.PaidOn = DateTime.Now; - if (InvoiceId.HasValue) - { - paymentModel.InvoiceId = InvoiceId.Value; - await OnInvoiceSelected(); - } - } - - private async Task OnInvoiceSelected() - { - if (paymentModel.InvoiceId != Guid.Empty) - { - selectedInvoice = invoices?.FirstOrDefault(i => i.Id == paymentModel.InvoiceId); - if (selectedInvoice != null) - { - // Default payment amount to the balance due - paymentModel.Amount = selectedInvoice.BalanceDue; - } - } - else - { - selectedInvoice = null; - paymentModel.Amount = 0; - } - - await InvokeAsync(StateHasChanged); - } - - private async Task HandleCreatePayment() - { - Payment payment = new Payment - { - InvoiceId = paymentModel.InvoiceId, - PaidOn = paymentModel.PaidOn, - Amount = paymentModel.Amount, - PaymentMethod = paymentModel.PaymentMethod, - Notes = paymentModel.Notes! - }; - await PaymentService.CreateAsync(payment); - Navigation.NavigateTo("/propertymanagement/payments"); - } - - private void Cancel() - { - Navigation.NavigateTo("/propertymanagement/payments"); - } - - public class PaymentModel - { - [Required(ErrorMessage = "Please select an invoice.")] - public Guid InvoiceId { get; set; } - - [Required(ErrorMessage = "Payment date is required.")] - public DateTime PaidOn { get; set; } = DateTime.Now; - - [Required(ErrorMessage = "Amount is required.")] - [Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero.")] - public decimal Amount { get; set; } - - [Required(ErrorMessage = "Payment method is required.")] - public string PaymentMethod { get; set; } = string.Empty; - - [MaxLength(1000)] - public string? Notes { get; set; } = string.Empty; - } -} + diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Edit.razor index 10e6073..89d5b17 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Edit.razor @@ -1,278 +1,13 @@ @page "/propertymanagement/payments/{PaymentId:guid}/edit" -@using Aquiis.SimpleStart.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 - +@using Aquiis.UI.Shared.Features.PropertyManagement.Payments @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/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Index.razor index 2eb4402..beb63e4 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/Index.razor @@ -1,492 +1,6 @@ @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 - +@using Aquiis.UI.Shared.Features.PropertyManagement.Payments @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 index cbe6ad0..fcffe53 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Payments/Pages/View.razor @@ -1,417 +1,13 @@ @page "/propertymanagement/payments/{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 - +@using Aquiis.UI.Shared.Features.PropertyManagement.Payments @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 deleted file mode 100644 index 74a1e88..0000000 --- a/4-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.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 index ad962d9..210ea29 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor @@ -1,260 +1,5 @@ @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")] +@rendermode InteractiveServer -
-
-
-
-

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 index 0556bda..2edf4b2 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor @@ -1,399 +1,10 @@ @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") - } -
-
-
-
-
-} +@rendermode InteractiveServer + @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 index d2ab887..62a0547 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor @@ -1,559 +1,5 @@ @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 index 8f30a40..0748e9f 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor @@ -1,626 +1,10 @@ @page "/propertymanagement/properties/{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/Tenants/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Create.razor index 9eff5e7..2a80f89 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Create.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Create.razor @@ -1,217 +1,5 @@ @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; +@rendermode InteractiveServer - [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 index 2768da8..13dab08 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Edit.razor @@ -1,339 +1,9 @@ -@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 - +@page "/propertymanagement/tenants/{Id:guid}/edit" @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 index b5e4a7b..edf1763 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/Index.razor @@ -1,528 +1,5 @@ @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 index 2e5021f..c02c25b 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor @@ -1,241 +1,9 @@ @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 + [Parameter] public Guid Id { get; set; } +} diff --git a/4-Aquiis.SimpleStart/Features/_Imports.razor b/4-Aquiis.SimpleStart/Features/_Imports.razor index cf019eb..d8ab796 100644 --- a/4-Aquiis.SimpleStart/Features/_Imports.razor +++ b/4-Aquiis.SimpleStart/Features/_Imports.razor @@ -19,3 +19,4 @@ @using Aquiis.Core.Constants @using Aquiis.SimpleStart.Features.PropertyManagement @using Aquiis.SimpleStart.Features.Administration +@using Aquiis.UI.Shared.Components.Entities.Properties diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Create.razor index 1608f35..7472452 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Create.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Create.razor @@ -1,317 +1,5 @@ @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 index 7030a48..4b5ce36 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Edit.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Edit.razor @@ -1,396 +1,9 @@ @page "/propertymanagement/invoices/edit/{Id:guid}" - -@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; } - } + [Parameter] public Guid Id { get; set; } } diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Index.razor index d2d680e..e28d304 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Index.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/Index.razor @@ -1,592 +1,5 @@ @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 index 77db724..4a11104 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/View.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Invoices/Pages/View.razor @@ -1,408 +1,9 @@ @page "/propertymanagement/invoices/view/{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(); - } - } + [Parameter] public Guid Id { get; set; } } diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Create.razor index 3e6b16b..bc6dbce 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Create.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Create.razor @@ -1,383 +1,5 @@ @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 index 948aa4b..869e570 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Edit.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Edit.razor @@ -1,357 +1,9 @@ -@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.Professional.Features.PropertyManagement -@using System.ComponentModel.DataAnnotations - -@inject NavigationManager Navigation -@inject LeaseService LeaseService -@inject UserContextService UserContextService - +@page "/propertymanagement/leases/{Id:guid}/edit" +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @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 index f2a8cfa..94b987e 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Index.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/Index.razor @@ -1,837 +1,5 @@ @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 index def44f5..ee2d8bc 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/View.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Leases/Pages/View.razor @@ -1,1263 +1,9 @@ @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.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 index 6e8c922..02faf70 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor @@ -1,354 +1,12 @@ -@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 - +@page "/propertymanagement/maintenance/create/{PropertyId:guid?}" +@using Aquiis.UI.Shared.Features.PropertyManagement.MaintenanceRequests @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 index 81a664a..fb4c298 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor @@ -1,306 +1,13 @@ -@page "/propertymanagement/maintenance/{Id:guid}/edit" -@inject MaintenanceService MaintenanceService -@inject PropertyService PropertyService -@inject LeaseService LeaseService -@inject NavigationManager NavigationManager -@inject IJSRuntime JSRuntime +@page "/propertymanagement/maintenance/{MaintenanceRequestId:guid}/edit" +@using Aquiis.UI.Shared.Features.PropertyManagement.MaintenanceRequests @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}"); - } + public Guid MaintenanceRequestId { get; set; } } diff --git a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor index c0f61d9..396f7d3 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor @@ -1,350 +1,6 @@ @page "/propertymanagement/maintenance" -@inject MaintenanceService MaintenanceService -@inject NavigationManager NavigationManager +@using Aquiis.UI.Shared.Features.PropertyManagement.MaintenanceRequests @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 index c60a539..585827c 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor @@ -1,309 +1,13 @@ -@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 - +@page "/propertymanagement/maintenance/{MaintenanceRequestId:guid}" +@using Aquiis.UI.Shared.Features.PropertyManagement.MaintenanceRequests @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"); - } + public Guid MaintenanceRequestId { get; set; } } diff --git a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor b/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor deleted file mode 100644 index c24e5c5..0000000 --- a/5-Aquiis.Professional/Features/PropertyManagement/MaintenanceRequests/Pages/_Imports.razor +++ /dev/null @@ -1,4 +0,0 @@ -@using Aquiis.Professional.Features.PropertyManagement -@using Aquiis.Core.Entities -@using Aquiis.Core.Constants -@using Microsoft.AspNetCore.Authorization diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Create.razor index a8bbbec..87052f9 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Create.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Create.razor @@ -1,285 +1,6 @@ @page "/propertymanagement/payments/create" -@using Aquiis.Professional.Features.PropertyManagement -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.Forms -@using System.ComponentModel.DataAnnotations -@using Microsoft.AspNetCore.Http.HttpResults -@inject NavigationManager Navigation -@inject PaymentService PaymentService -@inject InvoiceService InvoiceService - +@using Aquiis.UI.Shared.Features.PropertyManagement.Payments @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer -Record Payment - Property Management - -
-
-
-

Record Payment

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

No Unpaid Invoices

-

There are no outstanding invoices to record payments for.

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

@selectedInvoice.InvoiceNumber

-
-
- -

@selectedInvoice.Lease?.Property?.Address

-
-
- -

@selectedInvoice.Lease?.Tenant?.FullName

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

Select an invoice to see details

-
-
- } -
-
- -@code { - private List? invoices; - private Invoice? selectedInvoice; - private PaymentModel paymentModel = new(); - private decimal remainingBalance => selectedInvoice != null ? selectedInvoice.BalanceDue - paymentModel.Amount : 0; - - [CascadingParameter] - private Task AuthenticationStateTask { get; set; } = default!; - - [Parameter] - [SupplyParameterFromQuery] - public Guid? InvoiceId { get; set; } - - protected override async Task OnInitializedAsync() - { - var authState = await AuthenticationStateTask; - var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; - - - // Get all invoices and filter to those with outstanding balance - List? allInvoices = await InvoiceService.GetAllAsync(); - invoices = allInvoices - .Where(i => i.BalanceDue > 0 && i.Status != "Cancelled") - .OrderByDescending(i => i.DueOn) - .ToList(); - - paymentModel.PaidOn = DateTime.Now; - if (InvoiceId.HasValue) - { - paymentModel.InvoiceId = InvoiceId.Value; - await OnInvoiceSelected(); - } - } - - private async Task OnInvoiceSelected() - { - if (paymentModel.InvoiceId != Guid.Empty) - { - selectedInvoice = invoices?.FirstOrDefault(i => i.Id == paymentModel.InvoiceId); - if (selectedInvoice != null) - { - // Default payment amount to the balance due - paymentModel.Amount = selectedInvoice.BalanceDue; - } - } - else - { - selectedInvoice = null; - paymentModel.Amount = 0; - } - - await InvokeAsync(StateHasChanged); - } - - private async Task HandleCreatePayment() - { - Payment payment = new Payment - { - InvoiceId = paymentModel.InvoiceId, - PaidOn = paymentModel.PaidOn, - Amount = paymentModel.Amount, - PaymentMethod = paymentModel.PaymentMethod, - Notes = paymentModel.Notes! - }; - await PaymentService.CreateAsync(payment); - Navigation.NavigateTo("/propertymanagement/payments"); - } - - private void Cancel() - { - Navigation.NavigateTo("/propertymanagement/payments"); - } - - public class PaymentModel - { - [Required(ErrorMessage = "Please select an invoice.")] - public Guid InvoiceId { get; set; } - - [Required(ErrorMessage = "Payment date is required.")] - public DateTime PaidOn { get; set; } = DateTime.Now; - - [Required(ErrorMessage = "Amount is required.")] - [Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero.")] - public decimal Amount { get; set; } - - [Required(ErrorMessage = "Payment method is required.")] - public string PaymentMethod { get; set; } = string.Empty; - - [MaxLength(1000)] - public string? Notes { get; set; } = string.Empty; - } -} + diff --git a/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Edit.razor b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Edit.razor index f898c3b..89d5b17 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Edit.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Edit.razor @@ -1,278 +1,13 @@ @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 - +@using Aquiis.UI.Shared.Features.PropertyManagement.Payments @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 index 47f7825..beb63e4 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Index.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/Index.razor @@ -1,492 +1,6 @@ @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 - +@using Aquiis.UI.Shared.Features.PropertyManagement.Payments @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 index 2077f29..fcffe53 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/View.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Payments/Pages/View.razor @@ -1,417 +1,13 @@ @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 - +@using Aquiis.UI.Shared.Features.PropertyManagement.Payments @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 deleted file mode 100644 index 09581e0..0000000 --- a/5-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.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 index ad962d9..210ea29 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Create.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Create.razor @@ -1,260 +1,5 @@ @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")] +@rendermode InteractiveServer -
-
-
-
-

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 index 0556bda..2edf4b2 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Edit.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Edit.razor @@ -1,399 +1,10 @@ @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") - } -
-
-
-
-
-} +@rendermode InteractiveServer + @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 index 5bc5acb..62a0547 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Index.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/Index.razor @@ -1,559 +1,5 @@ @page "/propertymanagement/properties" -@using Aquiis.Professional.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/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor index 804944d..0748e9f 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Properties/Pages/View.razor @@ -1,626 +1,10 @@ @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/Tenants/Pages/Create.razor b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor index 7599aca..2a80f89 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Create.razor @@ -1,217 +1,5 @@ @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; +@rendermode InteractiveServer - [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 index 2768da8..13dab08 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Edit.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Edit.razor @@ -1,339 +1,9 @@ -@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 - +@page "/propertymanagement/tenants/{Id:guid}/edit" @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 index 1e6eb85..edf1763 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Index.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/Index.razor @@ -1,528 +1,5 @@ @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 index 2e5021f..c02c25b 100644 --- a/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/View.razor +++ b/5-Aquiis.Professional/Features/PropertyManagement/Tenants/Pages/View.razor @@ -1,241 +1,9 @@ @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 + [Parameter] public Guid Id { get; set; } +} diff --git a/5-Aquiis.Professional/Features/_Imports.razor b/5-Aquiis.Professional/Features/_Imports.razor index 0c7307e..e20426d 100644 --- a/5-Aquiis.Professional/Features/_Imports.razor +++ b/5-Aquiis.Professional/Features/_Imports.razor @@ -19,3 +19,4 @@ @using Aquiis.Core.Constants @using Aquiis.Professional.Features.PropertyManagement @using Aquiis.Professional.Features.Administration +@using Aquiis.UI.Shared.Components.Entities.Properties diff --git a/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs b/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs index 37fa703..babac6d 100644 --- a/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs +++ b/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs @@ -88,19 +88,19 @@ public async Task AddProperty() 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.Locator("select[name=\"Model.State\"]").SelectOptionAsync(new[] { "LA" }); + await Page.Locator("select[name=\"Model.State\"]").PressAsync("Tab"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("70119"); - await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.Locator("select[name=\"Model.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.Locator("input[name=\"Model.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"Model.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"Model.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"Model.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"Model.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"Model.SquareFeet\"]").FillAsync("2500"); + await Page.Locator("input[name=\"Model.SquareFeet\"]").PressAsync("Tab"); await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); // Verify property was created successfully @@ -114,19 +114,19 @@ public async Task AddProperty() 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.Locator("select[name=\"Model.State\"]").SelectOptionAsync(new[] { "CA" }); + await Page.Locator("select[name=\"Model.State\"]").PressAsync("Tab"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("90210"); - await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.Locator("select[name=\"Model.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.Locator("input[name=\"Model.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"Model.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"Model.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"Model.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"Model.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"Model.SquareFeet\"]").FillAsync("3200"); + await Page.Locator("input[name=\"Model.SquareFeet\"]").PressAsync("Tab"); await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); // Verify property was created successfully diff --git a/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs b/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs index 19d579b..355aa95 100644 --- a/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs +++ b/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs @@ -88,19 +88,19 @@ public async Task AddProperty() 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.Locator("select[name=\"Model.State\"]").SelectOptionAsync(new[] { "LA" }); + await Page.Locator("select[name=\"Model.State\"]").PressAsync("Tab"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("70119"); - await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.Locator("select[name=\"Model.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.Locator("input[name=\"Model.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"Model.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"Model.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"Model.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"Model.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"Model.SquareFeet\"]").FillAsync("2500"); + await Page.Locator("input[name=\"Model.SquareFeet\"]").PressAsync("Tab"); await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); // Verify property was created successfully @@ -114,19 +114,19 @@ public async Task AddProperty() 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.Locator("select[name=\"Model.State\"]").SelectOptionAsync(new[] { "CA" }); + await Page.Locator("select[name=\"Model.State\"]").PressAsync("Tab"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "#####-####" }).FillAsync("90210"); - await Page.Locator("select[name=\"propertyModel.PropertyType\"]").SelectOptionAsync(new[] { "House" }); + await Page.Locator("select[name=\"Model.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.Locator("input[name=\"Model.Bedrooms\"]").ClickAsync(); + await Page.Locator("input[name=\"Model.Bedrooms\"]").FillAsync("4"); + await Page.Locator("input[name=\"Model.Bedrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"Model.Bathrooms\"]").FillAsync("4.5"); + await Page.Locator("input[name=\"Model.Bathrooms\"]").PressAsync("Tab"); + await Page.Locator("input[name=\"Model.SquareFeet\"]").FillAsync("3200"); + await Page.Locator("input[name=\"Model.SquareFeet\"]").PressAsync("Tab"); await Page.GetByRole(AriaRole.Button, new() { Name = "Create Property" }).ClickAsync(); // Verify property was created successfully From 8e1fb3b8bc70ff0c273b6a7cccc504397a6d0586 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Wed, 14 Jan 2026 21:08:19 -0600 Subject: [PATCH 2/8] simple-start-refactor --- 0-Aquiis.Core/Entities/Organization.cs | 2 +- ...serOrganization.cs => OrganizationUser.cs} | 4 +- 0-Aquiis.Core/Entities/UserProfile.cs | 54 + .../Services/IUserContextService.cs | 25 + 0-Aquiis.Core/Utilities/StringSanitizer.cs | 97 + 0-Aquiis.Core/Validation/TrimAttribute.cs | 28 + .../Data/ApplicationDbContext.cs | 63 +- .../20260104205822_InitialCreate.Designer.cs | 10 +- .../20260104205822_InitialCreate.cs | 24 +- ...0114233331_AddUserProfileTable.Designer.cs | 4097 +++++++++++++++++ .../20260114233331_AddUserProfileTable.cs | 71 + .../ApplicationDbContextModelSnapshot.cs | 124 +- .../Services/DocumentNotificationService.cs | 2 +- .../Services/NotificationService.cs | 4 +- .../Services/OrganizationService.cs | 50 +- .../Components/Common/TrimmedInputText.razor | 31 + .../Components/Common/UserAvatar.razor | 40 + .../OrganizationUserViewModel.cs | 32 + .../OrganizationUsers/UserAccessCard.razor | 55 + .../OrganizationUsers/UserProfileCard.razor | 25 + .../Organizations/OrganizationCard.razor | 72 + .../OrganizationUserStatistics.razor | 34 + .../OrganizationUsersListView.razor | 101 + .../Organizations/OrganizationViewModel.cs | 11 + .../Notifications/NotificationBell.razor | 8 +- .../Notifications/NotificationCenter.razor | 4 +- .../NotificationPreferences.razor | 2 +- .../OrganizationSwitcher.razor | 44 +- .../Organizations/OrganizationDetails.razor | 67 + .../Organizations/OrganizationForm.razor | 2 + .../Organizations/OrganizationListView.razor | 232 + .../Pages/InitializeSchemaVersion.razor | 2 + .../Features/Administration/Dashboard.razor | 126 +- .../Pages/EditOrganization.razor | 6 +- .../Organizations/Pages/ManageUsers.razor | 51 +- .../Organizations/Pages/Organizations.razor | 13 +- .../Pages/ViewOrganization.razor | 278 +- ...ttings.razor => ApplicationSettings.razor} | 505 +- .../Settings/Pages/CalendarSettings.razor | 378 -- .../Pages/DatabaseSettings.razor} | 69 +- .../Settings/Pages/EmailSettings.razor | 454 -- .../Pages/InitializeSchemaVersion.razor | 65 + .../Settings/Pages/LateFeeSettings.razor | 439 -- .../Settings/Pages/SMSSettings.razor | 417 -- .../Settings/Pages/ServiceSettings.razor | 464 -- .../Administration/Users/Manage.razor | 28 +- .../Features/Notifications/Pages/Index.razor | 2 +- .../Notifications/Pages/Preferences.razor | 2 +- 4-Aquiis.SimpleStart/Program.cs | 2 +- ...ganizationRoleAuthorizationHandler copy.cs | 6 +- .../Account/Pages/Manage/Index.razor | 16 +- .../Components/Account/Pages/Register.razor | 134 +- .../Components/AuthorizedHeaderSection.razor | 2 +- .../Components/NotificationBellWrapper.razor | 2 +- .../OrganizationSwitcherWrapper.razor | 4 + .../Components/SchemaValidationWarning.razor | 2 +- .../Shared/Layout/NavMenu.razor | 67 +- .../Shared/Services/UserContextService.cs | 106 +- 4-Aquiis.SimpleStart/_Imports.razor | 4 +- .../Organizations/Pages/ManageUsers.razor | 51 +- .../Organizations/Pages/Organizations.razor | 4 +- .../Pages/ViewOrganization.razor | 273 +- .../Administration/Users/Manage.razor | 19 +- .../Features/Notifications/Pages/Index.razor | 2 +- .../Notifications/Pages/Preferences.razor | 2 +- 5-Aquiis.Professional/Program.cs | 2 +- ...ganizationRoleAuthorizationHandler copy.cs | 6 +- .../Account/Pages/Manage/Index.razor | 16 +- .../Components/Account/Pages/Register.razor | 134 +- .../Components/AuthorizedHeaderSection.razor | 4 +- .../Components/NotificationBellWrapper.razor | 2 +- .../Components/OrganizationSwitcher.razor | 2 +- .../Shared/Services/UserContextService.cs | 108 +- 5-Aquiis.Professional/_Imports.razor | 2 +- ...pplicationWorkflowService.EdgeCaseTests.cs | 6 +- .../NewSetupUITests.cs | 6 + .../Notifications/NotificationBellTests.cs | 2 +- .../Notifications/NotificationCenterTests.cs | 2 +- .../NotificationPreferencesTests.cs | 2 +- .../NewSetupUITests.cs | 478 +- 80 files changed, 6696 insertions(+), 3486 deletions(-) rename 0-Aquiis.Core/Entities/{UserOrganization.cs => OrganizationUser.cs} (95%) create mode 100644 0-Aquiis.Core/Entities/UserProfile.cs create mode 100644 0-Aquiis.Core/Utilities/StringSanitizer.cs create mode 100644 0-Aquiis.Core/Validation/TrimAttribute.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260114233331_AddUserProfileTable.Designer.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260114233331_AddUserProfileTable.cs create mode 100644 3-Aquiis.UI.Shared/Components/Common/TrimmedInputText.razor create mode 100644 3-Aquiis.UI.Shared/Components/Common/UserAvatar.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/OrganizationUserViewModel.cs create mode 100644 3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserAccessCard.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserProfileCard.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationUserStatistics.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationUsersListView.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationViewModel.cs rename 3-Aquiis.UI.Shared/{Features => Components}/Notifications/NotificationBell.razor (98%) rename 3-Aquiis.UI.Shared/{Features => Components}/Notifications/NotificationCenter.razor (99%) rename 3-Aquiis.UI.Shared/{Features => Components}/Notifications/NotificationPreferences.razor (99%) rename {4-Aquiis.SimpleStart/Shared/Components => 3-Aquiis.UI.Shared/Components/OrganizationSwitcher}/OrganizationSwitcher.razor (84%) create mode 100644 3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationDetails.razor create mode 100644 3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationForm.razor create mode 100644 3-Aquiis.UI.Shared/Features/Administration/Organizations/OrganizationListView.razor rename 4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/{OrganizationSettings.razor => ApplicationSettings.razor} (52%) delete mode 100644 4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/CalendarSettings.razor rename 4-Aquiis.SimpleStart/Features/Administration/{Application/Pages/ManageDatabase.razor => Settings/Pages/DatabaseSettings.razor} (93%) delete mode 100644 4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/EmailSettings.razor create mode 100644 4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/InitializeSchemaVersion.razor delete mode 100644 4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/LateFeeSettings.razor delete mode 100644 4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/SMSSettings.razor delete mode 100644 4-Aquiis.SimpleStart/Features/Administration/Settings/Pages/ServiceSettings.razor create mode 100644 4-Aquiis.SimpleStart/Shared/Components/OrganizationSwitcherWrapper.razor diff --git a/0-Aquiis.Core/Entities/Organization.cs b/0-Aquiis.Core/Entities/Organization.cs index d1521e9..6c4db00 100644 --- a/0-Aquiis.Core/Entities/Organization.cs +++ b/0-Aquiis.Core/Entities/Organization.cs @@ -41,7 +41,7 @@ public class Organization public bool IsDeleted { get; set; } = false; // Navigation properties - public virtual ICollection UserOrganizations { get; set; } = new List(); + public virtual ICollection OrganizationUsers { 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/UserOrganization.cs b/0-Aquiis.Core/Entities/OrganizationUser.cs similarity index 95% rename from 0-Aquiis.Core/Entities/UserOrganization.cs rename to 0-Aquiis.Core/Entities/OrganizationUser.cs index 9789e98..aa881c5 100644 --- a/0-Aquiis.Core/Entities/UserOrganization.cs +++ b/0-Aquiis.Core/Entities/OrganizationUser.cs @@ -8,11 +8,11 @@ namespace Aquiis.Core.Entities /// /// Junction table for multi-organization user assignments with role-based permissions /// - public class UserOrganization + public class OrganizationUser { [RequiredGuid] - [Display(Name = "UserOrganization ID")] + [Display(Name = "OrganizationUser ID")] public Guid Id { get; set; } = Guid.NewGuid(); /// diff --git a/0-Aquiis.Core/Entities/UserProfile.cs b/0-Aquiis.Core/Entities/UserProfile.cs new file mode 100644 index 0000000..eb57330 --- /dev/null +++ b/0-Aquiis.Core/Entities/UserProfile.cs @@ -0,0 +1,54 @@ +using System; + +namespace Aquiis.Core.Entities; + +/// +/// User profile information stored in business context (ApplicationDbContext). +/// Separate from Identity (AspNetUsers) to enable single-context queries with business entities. +/// Email is denormalized for query simplification (considered read-only in B2B context). +/// +public class UserProfile : BaseModel +{ + /// + /// Foreign key to AspNetUsers.Id (Identity context). + /// This links the profile to the authentication user. + /// + public string UserId { get; set; } = string.Empty; + + // Personal Information (denormalized from Identity for query efficiency) + + /// + /// User's email address (cached from AspNetUsers). + /// Considered read-only - changes require sync with Identity table. + /// + public string Email { get; set; } = string.Empty; + + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string? PhoneNumber { get; set; } + + // Organization Context + + /// + /// User's "home" organization - their primary/default organization. + /// + public Guid? OrganizationId { get; set; } + + /// + /// Currently active organization the user is viewing/working with. + /// Can differ from home organization for users with multi-org access. + /// + public Guid? ActiveOrganizationId { get; set; } + + // Computed Properties + + /// + /// Full name combining first and last name. + /// + public string FullName => $"{FirstName} {LastName}".Trim(); + + /// + /// Display name for UI - uses full name if available, falls back to email. + /// + public string DisplayName => string.IsNullOrWhiteSpace(FullName) ? Email : FullName; +} diff --git a/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs b/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs index 78ad844..8e1cfb6 100644 --- a/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs +++ b/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs @@ -1,3 +1,5 @@ +using Aquiis.Core.Entities; + namespace Aquiis.Core.Interfaces.Services; /// @@ -30,4 +32,27 @@ public interface IUserContextService /// Gets the current user's OrganizationId (DEPRECATED: Use GetActiveOrganizationIdAsync). /// Task GetOrganizationIdAsync(); + + /// + /// Forces a refresh of the cached user data. + /// Call this if user data has been updated and you need to reload it. + /// + Task RefreshAsync(); + + Task GetActiveOrganizationAsync(); + + Task GetCurrentOrganizationRoleAsync(); + + Task IsAccountOwnerAsync(); + + + /// + /// Switch the user's active organization + /// + Task SwitchOrganizationAsync(Guid organizationId); + + /// + /// Check if the current user has a specific permission in their active organization + /// + Task HasPermissionAsync(string permission); } \ No newline at end of file diff --git a/0-Aquiis.Core/Utilities/StringSanitizer.cs b/0-Aquiis.Core/Utilities/StringSanitizer.cs new file mode 100644 index 0000000..bb5254b --- /dev/null +++ b/0-Aquiis.Core/Utilities/StringSanitizer.cs @@ -0,0 +1,97 @@ +namespace Aquiis.Core.Utilities; + +/// +/// Provides string sanitization and normalization utilities for consistent data storage. +/// +public static class StringSanitizer +{ + /// + /// Trims leading and trailing whitespace from a string. + /// Returns empty string if input is null or whitespace. + /// + public static string Trim(string? value) + { + return value?.Trim() ?? string.Empty; + } + + /// + /// Trims and converts to lowercase for case-insensitive comparisons. + /// Useful for emails, usernames, etc. + /// + public static string NormalizeForComparison(string? value) + { + return value?.Trim().ToLowerInvariant() ?? string.Empty; + } + + /// + /// Normalizes email addresses: trims and converts to lowercase. + /// + public static string NormalizeEmail(string? email) + { + return email?.Trim().ToLowerInvariant() ?? string.Empty; + } + + /// + /// Normalizes phone numbers: removes all non-digit characters. + /// Example: "(555) 123-4567" becomes "5551234567" + /// + public static string NormalizePhoneNumber(string? phoneNumber) + { + if (string.IsNullOrWhiteSpace(phoneNumber)) + return string.Empty; + + return new string(phoneNumber.Where(char.IsDigit).ToArray()); + } + + /// + /// Formats a normalized phone number for display. + /// Example: "5551234567" becomes "(555) 123-4567" + /// + public static string FormatPhoneNumber(string? phoneNumber) + { + var normalized = NormalizePhoneNumber(phoneNumber); + + if (normalized.Length == 10) + { + return $"({normalized.Substring(0, 3)}) {normalized.Substring(3, 3)}-{normalized.Substring(6, 4)}"; + } + else if (normalized.Length == 11 && normalized.StartsWith("1")) + { + return $"+1 ({normalized.Substring(1, 3)}) {normalized.Substring(4, 3)}-{normalized.Substring(7, 4)}"; + } + + return phoneNumber ?? string.Empty; + } + + /// + /// Collapses multiple consecutive spaces into a single space and trims. + /// Example: "Hello World " becomes "Hello World" + /// + public static string CollapseWhitespace(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return string.Empty; + + return string.Join(" ", value.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); + } + + /// + /// Removes all whitespace from a string. + /// Useful for IDs, codes, etc. + /// + public static string RemoveWhitespace(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return string.Empty; + + return new string(value.Where(c => !char.IsWhiteSpace(c)).ToArray()); + } + + /// + /// Sanitizes a string for safe display by trimming and collapsing whitespace. + /// + public static string Sanitize(string? value) + { + return CollapseWhitespace(value); + } +} diff --git a/0-Aquiis.Core/Validation/TrimAttribute.cs b/0-Aquiis.Core/Validation/TrimAttribute.cs new file mode 100644 index 0000000..522ed7a --- /dev/null +++ b/0-Aquiis.Core/Validation/TrimAttribute.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.Core.Validation; + +/// +/// Validation attribute that automatically trims string values during model binding. +/// Removes leading and trailing whitespace from the property value. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] +public class TrimAttribute : ValidationAttribute +{ + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + var trimmedValue = stringValue.Trim(); + + // Use reflection to set the trimmed value back to the property + var propertyInfo = validationContext.ObjectType.GetProperty(validationContext.MemberName!); + if (propertyInfo != null && propertyInfo.CanWrite) + { + propertyInfo.SetValue(validationContext.ObjectInstance, trimmedValue); + } + } + + return ValidationResult.Success; + } +} diff --git a/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs index 825dad4..94c7bd4 100644 --- a/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs +++ b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs @@ -16,6 +16,40 @@ public ApplicationDbContext(DbContextOptions options) { } + public override int SaveChanges() + { + SanitizeStringProperties(); + return base.SaveChanges(); + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + SanitizeStringProperties(); + return base.SaveChangesAsync(cancellationToken); + } + + private void SanitizeStringProperties() + { + var entries = ChangeTracker.Entries() + .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified); + + foreach (var entry in entries) + { + foreach (var property in entry.Properties) + { + if (property.Metadata.ClrType == typeof(string) && property.CurrentValue != null) + { + var value = property.CurrentValue as string; + if (!string.IsNullOrEmpty(value)) + { + // Trim leading/trailing whitespace + property.CurrentValue = value.Trim(); + } + } + } + } + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); @@ -54,7 +88,10 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) // Multi-organization support public DbSet Organizations { get; set; } - public DbSet UserOrganizations { get; set; } + public DbSet OrganizationUsers { get; set; } + + // User profiles (business context - separate from Identity) + public DbSet UserProfiles { get; set; } // Workflow audit logging public DbSet WorkflowAuditLogs { get; set; } @@ -499,13 +536,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // No navigation property configured here }); - // Configure UserOrganization entity - modelBuilder.Entity(entity => + // Configure OrganizationUser entity + modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.HasOne(uo => uo.Organization) - .WithMany(o => o.UserOrganizations) + .WithMany(o => o.OrganizationUsers) .HasForeignKey(uo => uo.OrganizationId) .OnDelete(DeleteBehavior.Cascade); @@ -607,6 +644,24 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.CostPerSMS).HasPrecision(18, 4); }); + // Configure UserProfile entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + + // Unique constraint: one profile per user + entity.HasIndex(e => e.UserId).IsUnique(); + + // Additional indexes for common queries + entity.HasIndex(e => e.Email); + entity.HasIndex(e => e.OrganizationId); + entity.HasIndex(e => e.ActiveOrganizationId); + entity.HasIndex(e => e.IsDeleted); + + // Note: No navigation property to AspNetUsers (different context) + // UserId is a string FK to Identity context, but no EF relationship configured + }); + // Seed System Checklist Templates SeedChecklistTemplates(modelBuilder); } diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs index 7cc048d..0a34aa3 100644 --- a/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs @@ -3263,7 +3263,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Tours"); }); - modelBuilder.Entity("Aquiis.Core.Entities.UserOrganization", b => + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -3320,7 +3320,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("UserId", "OrganizationId") .IsUnique(); - b.ToTable("UserOrganizations"); + b.ToTable("OrganizationUsers"); }); modelBuilder.Entity("Aquiis.Core.Entities.WorkflowAuditLog", b => @@ -3937,10 +3937,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("ProspectiveTenant"); }); - modelBuilder.Entity("Aquiis.Core.Entities.UserOrganization", b => + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => { b.HasOne("Aquiis.Core.Entities.Organization", "Organization") - .WithMany("UserOrganizations") + .WithMany("OrganizationUsers") .HasForeignKey("OrganizationId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -3991,7 +3991,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Tenants"); - b.Navigation("UserOrganizations"); + b.Navigation("OrganizationUsers"); }); modelBuilder.Entity("Aquiis.Core.Entities.Property", b => diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.cs index 5257751..cc241fc 100644 --- a/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.cs +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.cs @@ -481,7 +481,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateTable( - name: "UserOrganizations", + name: "OrganizationUsers", columns: table => new { Id = table.Column(type: "TEXT", nullable: false), @@ -500,9 +500,9 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_UserOrganizations", x => x.Id); + table.PrimaryKey("PK_OrganizationUsers", x => x.Id); table.ForeignKey( - name: "FK_UserOrganizations_Organizations_OrganizationId", + name: "FK_OrganizationUsers_Organizations_OrganizationId", column: x => x.OrganizationId, principalTable: "Organizations", principalColumn: "Id", @@ -1839,23 +1839,23 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "Status"); migrationBuilder.CreateIndex( - name: "IX_UserOrganizations_IsActive", - table: "UserOrganizations", + name: "IX_OrganizationUsers_IsActive", + table: "OrganizationUsers", column: "IsActive"); migrationBuilder.CreateIndex( - name: "IX_UserOrganizations_OrganizationId", - table: "UserOrganizations", + name: "IX_OrganizationUsers_OrganizationId", + table: "OrganizationUsers", column: "OrganizationId"); migrationBuilder.CreateIndex( - name: "IX_UserOrganizations_Role", - table: "UserOrganizations", + name: "IX_OrganizationUsers_Role", + table: "OrganizationUsers", column: "Role"); migrationBuilder.CreateIndex( - name: "IX_UserOrganizations_UserId_OrganizationId", - table: "UserOrganizations", + name: "IX_OrganizationUsers_UserId_OrganizationId", + table: "OrganizationUsers", columns: new[] { "UserId", "OrganizationId" }, unique: true); @@ -2018,7 +2018,7 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "Tours"); migrationBuilder.DropTable( - name: "UserOrganizations"); + name: "OrganizationUsers"); migrationBuilder.DropTable( name: "WorkflowAuditLogs"); diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260114233331_AddUserProfileTable.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260114233331_AddUserProfileTable.Designer.cs new file mode 100644 index 0000000..2cb376c --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260114233331_AddUserProfileTable.Designer.cs @@ -0,0 +1,4097 @@ +// +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("20260114233331_AddUserProfileTable")] + partial class AddUserProfileTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + 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.OrganizationUser", 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("OrganizationUsers"); + }); + + 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.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActiveOrganizationId"); + + b.HasIndex("Email"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserProfiles"); + }); + + 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.OrganizationUser", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("OrganizationUsers") + .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("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("OrganizationUsers"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + }); + + 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/20260114233331_AddUserProfileTable.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260114233331_AddUserProfileTable.cs new file mode 100644 index 0000000..1fc7e3b --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260114233331_AddUserProfileTable.cs @@ -0,0 +1,71 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Infrastructure.Data.Migrations +{ + /// + public partial class AddUserProfileTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserProfiles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Email = table.Column(type: "TEXT", nullable: false), + FirstName = table.Column(type: "TEXT", nullable: false), + LastName = table.Column(type: "TEXT", nullable: false), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + OrganizationId = table.Column(type: "TEXT", nullable: true), + ActiveOrganizationId = 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_UserProfiles", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserProfiles_ActiveOrganizationId", + table: "UserProfiles", + column: "ActiveOrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_UserProfiles_Email", + table: "UserProfiles", + column: "Email"); + + migrationBuilder.CreateIndex( + name: "IX_UserProfiles_IsDeleted", + table: "UserProfiles", + column: "IsDeleted"); + + migrationBuilder.CreateIndex( + name: "IX_UserProfiles_OrganizationId", + table: "UserProfiles", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_UserProfiles_UserId", + table: "UserProfiles", + column: "UserId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserProfiles"); + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 5eb5864..eb70014 100644 --- a/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class ApplicationDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); modelBuilder.Entity("Aquiis.Core.Entities.ApplicationScreening", b => { @@ -2355,6 +2355,66 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OrganizationSettings"); }); + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", 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("OrganizationUsers"); + }); + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => { b.Property("Id") @@ -3260,46 +3320,48 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Tours"); }); - modelBuilder.Entity("Aquiis.Core.Entities.UserOrganization", b => + modelBuilder.Entity("Aquiis.Core.Entities.UserProfile", b => { b.Property("Id") - .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ActiveOrganizationId") .HasColumnType("TEXT"); b.Property("CreatedBy") .IsRequired() + .HasMaxLength(100) .HasColumnType("TEXT"); b.Property("CreatedOn") .HasColumnType("TEXT"); - b.Property("GrantedBy") + b.Property("Email") .IsRequired() .HasColumnType("TEXT"); - b.Property("GrantedOn") + b.Property("FirstName") + .IsRequired() .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("OrganizationId") + b.Property("LastName") + .IsRequired() .HasColumnType("TEXT"); - b.Property("RevokedOn") + b.Property("OrganizationId") .HasColumnType("TEXT"); - b.Property("Role") - .IsRequired() + b.Property("PhoneNumber") .HasColumnType("TEXT"); b.Property("UserId") @@ -3308,16 +3370,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("IsActive"); + b.HasIndex("ActiveOrganizationId"); - b.HasIndex("OrganizationId"); + b.HasIndex("Email"); - b.HasIndex("Role"); + b.HasIndex("IsDeleted"); - b.HasIndex("UserId", "OrganizationId") + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") .IsUnique(); - b.ToTable("UserOrganizations"); + b.ToTable("UserProfiles"); }); modelBuilder.Entity("Aquiis.Core.Entities.WorkflowAuditLog", b => @@ -3784,6 +3848,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Organization"); }); + modelBuilder.Entity("Aquiis.Core.Entities.OrganizationUser", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + modelBuilder.Entity("Aquiis.Core.Entities.Payment", b => { b.HasOne("Aquiis.Core.Entities.Document", "Document") @@ -3934,17 +4009,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -3984,11 +4048,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Leases"); + b.Navigation("OrganizationUsers"); + b.Navigation("Properties"); b.Navigation("Tenants"); - - b.Navigation("UserOrganizations"); }); modelBuilder.Entity("Aquiis.Core.Entities.Property", b => diff --git a/2-Aquiis.Application/Services/DocumentNotificationService.cs b/2-Aquiis.Application/Services/DocumentNotificationService.cs index 06ce32b..fbdcf99 100644 --- a/2-Aquiis.Application/Services/DocumentNotificationService.cs +++ b/2-Aquiis.Application/Services/DocumentNotificationService.cs @@ -47,7 +47,7 @@ public async Task CheckDocumentExpirationsAsync() foreach (var organizationId in organizations) { // Get admin users for this organization - var adminUsers = await _dbContext.UserOrganizations + var adminUsers = await _dbContext.OrganizationUsers .Where(uom => uom.OrganizationId == organizationId && uom.Role == "Admin" && !uom.IsDeleted) diff --git a/2-Aquiis.Application/Services/NotificationService.cs b/2-Aquiis.Application/Services/NotificationService.cs index cc5275d..5e9c7fd 100644 --- a/2-Aquiis.Application/Services/NotificationService.cs +++ b/2-Aquiis.Application/Services/NotificationService.cs @@ -116,8 +116,8 @@ public async Task NotifyAllUsersAsync( Guid? relatedEntityId = null, string? relatedEntityType = null) { - // Query users through UserOrganizations to find all users in the organization - var userIds = await _context.UserOrganizations + // Query users through OrganizationUsers to find all users in the organization + var userIds = await _context.OrganizationUsers .Where(uo => uo.OrganizationId == organizationId && uo.IsActive && !uo.IsDeleted) .Select(uo => uo.UserId) .ToListAsync(); diff --git a/2-Aquiis.Application/Services/OrganizationService.cs b/2-Aquiis.Application/Services/OrganizationService.cs index 9605793..6c7046d 100644 --- a/2-Aquiis.Application/Services/OrganizationService.cs +++ b/2-Aquiis.Application/Services/OrganizationService.cs @@ -37,8 +37,8 @@ public async Task CreateOrganizationAsync(string ownerId, string n _dbContext.Organizations.Add(organization); - // Create Owner entry in UserOrganizations - var userOrganization = new UserOrganization + // Create Owner entry in OrganizationUsers + var OrganizationUser = new OrganizationUser { Id = Guid.NewGuid(), UserId = ownerId, @@ -51,7 +51,7 @@ public async Task CreateOrganizationAsync(string ownerId, string n CreatedBy = ownerId }; - _dbContext.UserOrganizations.Add(userOrganization); + _dbContext.OrganizationUsers.Add(OrganizationUser); // add organization settings record with defaults var settings = new OrganizationSettings @@ -96,8 +96,8 @@ public async Task CreateOrganizationAsync(Organization organizatio _dbContext.Organizations.Add(organization); - // Create Owner entry in UserOrganizations - var userOrganization = new UserOrganization + // Create Owner entry in OrganizationUsers + var OrganizationUser = new OrganizationUser { Id = Guid.NewGuid(), UserId = userId, @@ -110,7 +110,7 @@ public async Task CreateOrganizationAsync(Organization organizatio CreatedBy = userId }; - _dbContext.UserOrganizations.Add(userOrganization); + _dbContext.OrganizationUsers.Add(OrganizationUser); await _dbContext.SaveChangesAsync(); // add organization settings record with defaults @@ -143,7 +143,7 @@ public async Task CreateOrganizationAsync(Organization organizatio public async Task GetOrganizationByIdAsync(Guid organizationId) { return await _dbContext.Organizations - .Include(o => o.UserOrganizations) + .Include(o => o.OrganizationUsers) .FirstOrDefaultAsync(o => o.Id == organizationId && !o.IsDeleted); } @@ -159,11 +159,11 @@ public async Task> GetOwnedOrganizationsAsync(string userId) } /// - /// Get all organizations a user has access to (via UserOrganizations) + /// Get all organizations a user has access to (via OrganizationUsers) /// - public async Task> GetUserOrganizationsAsync(string userId) + public async Task> GetOrganizationUsersAsync(string userId) { - return await _dbContext.UserOrganizations + return await _dbContext.OrganizationUsers .Include(uo => uo.Organization) .Where(uo => uo.UserId == userId && uo.IsActive && !uo.IsDeleted) .Where(uo => !uo.Organization.IsDeleted) @@ -205,8 +205,8 @@ public async Task DeleteOrganizationAsync(Guid organizationId, string dele organization.LastModifiedOn = DateTime.UtcNow; organization.LastModifiedBy = deletedBy; - // Soft delete all UserOrganizations entries - var userOrgs = await _dbContext.UserOrganizations + // Soft delete all OrganizationUsers entries + var userOrgs = await _dbContext.OrganizationUsers .Where(uo => uo.OrganizationId == organizationId) .ToListAsync(); @@ -250,7 +250,7 @@ public async Task IsAdministratorAsync(string userId, Guid organizationId) /// public async Task CanAccessOrganizationAsync(string userId, Guid organizationId) { - return await _dbContext.UserOrganizations + return await _dbContext.OrganizationUsers .AnyAsync(uo => uo.UserId == userId && uo.OrganizationId == organizationId && uo.IsActive @@ -262,7 +262,7 @@ public async Task CanAccessOrganizationAsync(string userId, Guid organizat /// public async Task GetUserRoleForOrganizationAsync(string userId, Guid organizationId) { - var userOrg = await _dbContext.UserOrganizations + var userOrg = await _dbContext.OrganizationUsers .FirstOrDefaultAsync(uo => uo.UserId == userId && uo.OrganizationId == organizationId && uo.IsActive @@ -290,7 +290,7 @@ public async Task GrantOrganizationAccessAsync(string userId, Guid organiz return false; // Check if user already has access - var existing = await _dbContext.UserOrganizations + var existing = await _dbContext.OrganizationUsers .FirstOrDefaultAsync(uo => uo.UserId == userId && uo.OrganizationId == organizationId); if (existing != null) @@ -314,7 +314,7 @@ public async Task GrantOrganizationAccessAsync(string userId, Guid organiz else { // Create new access - var userOrganization = new UserOrganization + var OrganizationUser = new OrganizationUser { Id = Guid.NewGuid(), UserId = userId, @@ -327,7 +327,7 @@ public async Task GrantOrganizationAccessAsync(string userId, Guid organiz CreatedBy = grantedBy }; - _dbContext.UserOrganizations.Add(userOrganization); + _dbContext.OrganizationUsers.Add(OrganizationUser); } await _dbContext.SaveChangesAsync(); @@ -339,7 +339,7 @@ public async Task GrantOrganizationAccessAsync(string userId, Guid organiz /// public async Task RevokeOrganizationAccessAsync(string userId, Guid organizationId, string revokedBy) { - var userOrg = await _dbContext.UserOrganizations + var userOrg = await _dbContext.OrganizationUsers .FirstOrDefaultAsync(uo => uo.UserId == userId && uo.OrganizationId == organizationId && uo.IsActive); @@ -373,7 +373,7 @@ public async Task UpdateUserRoleAsync(string userId, Guid organizationId, if (!ApplicationConstants.OrganizationRoles.IsValid(newRole)) throw new ArgumentException($"Invalid role: {newRole}"); - var userOrg = await _dbContext.UserOrganizations + var userOrg = await _dbContext.OrganizationUsers .FirstOrDefaultAsync(uo => uo.UserId == userId && uo.OrganizationId == organizationId && uo.IsActive @@ -401,9 +401,9 @@ public async Task UpdateUserRoleAsync(string userId, Guid organizationId, /// /// Get all users with access to an organization /// - public async Task> GetOrganizationUsersAsync(Guid organizationId) + public async Task> GetOrganizationUsersAsync(Guid organizationId) { - return await _dbContext.UserOrganizations + return await _dbContext.OrganizationUsers .Where(uo => uo.OrganizationId == organizationId && uo.IsActive && !uo.IsDeleted && uo.UserId != ApplicationConstants.SystemUser.Id) .OrderBy(uo => uo.Role) .ThenBy(uo => uo.UserId) @@ -413,13 +413,13 @@ public async Task> GetOrganizationUsersAsync(Guid organiz /// /// Get all organization assignments for a user (including revoked) /// - public async Task> GetUserAssignmentsAsync() + 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 + return await _dbContext.OrganizationUsers .Include(uo => uo.Organization) .Where(uo => uo.UserId == userId && !uo.IsDeleted && uo.UserId != ApplicationConstants.SystemUser.Id) .OrderByDescending(uo => uo.IsActive) @@ -430,13 +430,13 @@ public async Task> GetUserAssignmentsAsync() /// /// Get all organization assignments for a user (including revoked) /// - public async Task> GetActiveUserAssignmentsAsync() + 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 + return await _dbContext.OrganizationUsers .Include(uo => uo.Organization) .Where(uo => uo.UserId == userId && !uo.IsDeleted && uo.IsActive && uo.UserId != ApplicationConstants.SystemUser.Id) .OrderByDescending(uo => uo.IsActive) diff --git a/3-Aquiis.UI.Shared/Components/Common/TrimmedInputText.razor b/3-Aquiis.UI.Shared/Components/Common/TrimmedInputText.razor new file mode 100644 index 0000000..b83584d --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Common/TrimmedInputText.razor @@ -0,0 +1,31 @@ +@inherits InputText + +@* + A text input component that automatically trims leading and trailing whitespace + on blur (when user leaves the field). Extends standard InputText functionality. + + Usage: + +*@ + + + +@code { + private void HandleInput(ChangeEventArgs e) + { + CurrentValueAsString = e.Value?.ToString(); + } + + private void HandleBlur(FocusEventArgs e) + { + // Trim the value when user leaves the field + if (!string.IsNullOrEmpty(CurrentValueAsString)) + { + CurrentValueAsString = CurrentValueAsString.Trim(); + } + } +} diff --git a/3-Aquiis.UI.Shared/Components/Common/UserAvatar.razor b/3-Aquiis.UI.Shared/Components/Common/UserAvatar.razor new file mode 100644 index 0000000..6bfa197 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Common/UserAvatar.razor @@ -0,0 +1,40 @@ + +
+ @if (!string.IsNullOrWhiteSpace(FirstName) || !string.IsNullOrWhiteSpace(LastName)) + { + @GetInitials() + } + else + { + + } +
+ +@code { + [Parameter, EditorRequired] + public int Size { get; set; } = 48; + + [Parameter , EditorRequired] + public string FirstName { get; set; } = string.Empty; + + [Parameter , EditorRequired] + public string LastName { get; set; } = string.Empty; + + private string GetInitials() + { + var initials = string.Empty; + + if (!string.IsNullOrWhiteSpace(FirstName)) + { + initials += FirstName[0]; + } + + if (!string.IsNullOrWhiteSpace(LastName)) + { + initials += LastName[0]; + } + + return initials.ToUpper(); + } +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/OrganizationUserViewModel.cs b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/OrganizationUserViewModel.cs new file mode 100644 index 0000000..917a532 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/OrganizationUserViewModel.cs @@ -0,0 +1,32 @@ +using Aquiis.Core.Entities; + +namespace Aquiis.UI.Shared.Components.Entities.OrganizationUsers; + + +/// +/// View model representing a user's membership in an organization. +/// Combines data from OrganizationUser entity and ApplicationUser (AspNetUsers). +/// +public class OrganizationUserViewModel +{ + // From OrganizationUser entity + public Guid Id { get; set; } // OrganizationUser.Id + public Guid OrganizationId { get; set; } + public string UserId { get; set; } = string.Empty; + public string Role { get; set; } = string.Empty; + public bool IsActive { get; set; } + public DateTime GrantedOn { get; set; } + public DateTime? RevokedOn { get; set; } + public string? GrantedBy { get; set; } // UserId of granter + public string? GrantedByEmail { get; set; } // Email of granter (from UserProfile join) + + // From UserProfile + public string Email { get; set; } = string.Empty; + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string? PhoneNumber { get; set; } + + // Computed properties + public string FullName => $"{FirstName} {LastName}".Trim(); + public string DisplayRole => Role; +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserAccessCard.razor b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserAccessCard.razor new file mode 100644 index 0000000..412d251 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserAccessCard.razor @@ -0,0 +1,55 @@ + +@using Aquiis.Core.Entities +@using Aquiis.Core.Constants +@namespace Aquiis.UI.Shared.Components.Entities.OrganizationUsers + + +
+
+
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, EditorRequired] + public string CurrentUserRole { get; set; } + + private bool isOwner => CurrentUserRole == ApplicationConstants.OrganizationRoles.Owner; + private bool isAdministrator => CurrentUserRole == ApplicationConstants.OrganizationRoles.Administrator; + + 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" + }; + } +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserProfileCard.razor b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserProfileCard.razor new file mode 100644 index 0000000..a19e4d4 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/OrganizationUsers/UserProfileCard.razor @@ -0,0 +1,25 @@ + +@namespace Aquiis.UI.Shared.Components.Entities.OrganizationUsers + +
+
+
+ +
+
+
@OrganizationUserViewModel!.FirstName @OrganizationUserViewModel!.LastName
+

Email: @OrganizationUserViewModel!.Email

+

Role: @OrganizationUserViewModel!.Role

+
+
+
+ +@code { + + [Parameter, EditorRequired] + public OrganizationUserViewModel? OrganizationUserViewModel { get; set; } = new(); + +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor new file mode 100644 index 0000000..e5f767e --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationCard.razor @@ -0,0 +1,72 @@ +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.Core.Constants + +@namespace Aquiis.UI.Shared.Components.Entities.Organizations + + +
+
+
Organization Information
+ +
+ @if(OrganizationViewModel?.Organization != null) + { +
+
+
Organization Name:
+
@OrganizationViewModel?.Organization?.Name
+ +
Display Name:
+
@(OrganizationViewModel?.Organization?.DisplayName ?? "-")
+ +
State:
+
@(OrganizationViewModel?.Organization?.State ?? "-")
+ +
Status:
+
+ @if (OrganizationViewModel?.Organization?.IsActive == true) + { + Active + } + else + { + Inactive + } +
+ +
Owner:
+
@OrganizationViewModel?.OrganizationOwner?.Email
+ +
Created On:
+
@OrganizationViewModel?.Organization?.CreatedOn.ToString("MMMM dd, yyyy")
+ + @if (OrganizationViewModel?.Organization?.LastModifiedOn.HasValue == true) + { +
Last Modified:
+
@OrganizationViewModel?.Organization?.LastModifiedOn.Value.ToString("MMMM dd, yyyy")
+ } +
+
+ } + else + { +
+
+ Organization details are not available. +
+
+ } +
+ +@code { + + [Parameter, EditorRequired] + public OrganizationViewModel? OrganizationViewModel { get; set; } = new(); + + [Parameter] + public EventCallback OnEdit { get; set; } + +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationUserStatistics.razor b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationUserStatistics.razor new file mode 100644 index 0000000..c08fe30 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationUserStatistics.razor @@ -0,0 +1,34 @@ + +@namespace Aquiis.UI.Shared.Components.Entities.Organizations +@using Aquiis.UI.Shared.Components.Entities.OrganizationUsers + +
+
+
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)
+
+
+
+ +@code { + [Parameter, EditorRequired] + public OrganizationViewModel OrganizationViewModel { get; set; } + + public List OrganizationUsers => OrganizationViewModel.OrganizationUsers; +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationUsersListView.razor b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationUsersListView.razor new file mode 100644 index 0000000..eacc86b --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationUsersListView.razor @@ -0,0 +1,101 @@ + +@namespace Aquiis.UI.Shared.Components.Entities.Organizations +@using Aquiis.UI.Shared.Components.Entities.OrganizationUsers + + +
+
+
Users with Access
+ @if (IsOwner || IsAdministrator && IsCurrentOrganization) + { + + } +
+
+ @if (!OrganizationUsers.Any()) + { +
+ No users assigned to this organization. +
+ } + else + { +
+ + + + + + + + + + + + @foreach (var orgUser in OrganizationUsers) + { + + + + + + + + } + +
UserRoleGranted ByGranted OnStatus
+
+ @orgUser.Email +
+
+ + @orgUser.Role + + @(orgUser.GrantedByEmail ?? "Unknown")@orgUser.GrantedOn.ToString("MMM dd, yyyy") + @if (orgUser.IsActive && orgUser.RevokedOn == null) + { + Active + } + else + { + Revoked + } +
+
+ } +
+
+ +@code { + + [Parameter, EditorRequired] + public OrganizationViewModel? OrganizationViewModel { get; set; } = new(); + + public List OrganizationUsers => OrganizationViewModel?.OrganizationUsers ?? new List(); + + [Parameter, EditorRequired] + public bool IsOwner { get; set; } = false; + [Parameter, EditorRequired] + public bool IsAdministrator { get; set; } = false; + + [Parameter, EditorRequired] + public bool IsCurrentOrganization { get; set; } = false; + + [Parameter] + public EventCallback OnManageUsers { get; set; } + + 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" + }; + } + +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationViewModel.cs b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationViewModel.cs new file mode 100644 index 0000000..1c454bf --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Organizations/OrganizationViewModel.cs @@ -0,0 +1,11 @@ +using Aquiis.Core.Entities; +using Aquiis.UI.Shared.Components.Entities.OrganizationUsers; + +namespace Aquiis.UI.Shared.Components.Entities.Organizations; + +public class OrganizationViewModel +{ + public Organization? Organization { get; set; } + public OrganizationUserViewModel? OrganizationOwner { get; set; } + public List OrganizationUsers { get; set; } = new(); +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor b/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor similarity index 98% rename from 3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor rename to 3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor index b377d96..4a96dd0 100644 --- a/3-Aquiis.UI.Shared/Features/Notifications/NotificationBell.razor +++ b/3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor @@ -1,9 +1,7 @@ +@using Aquiis.Application.Services @inject NotificationService NotificationService @inject NavigationManager NavigationManager -@namespace Aquiis.UI.Shared.Features.Notifications -@using Aquiis.Application.Services - -@* Notification Bell *@ +@namespace Aquiis.UI.Shared.Components.Notifications @if (isLoading) { @@ -15,7 +13,7 @@ else if (notifications.Count > 0) { @@ -1129,9 +1130,9 @@ else Navigation.NavigateTo($"/propertymanagement/leases/{LeaseId}/edit"); } - private void BackToList() + private void BackToProperty() { - Navigation.NavigateTo("/propertymanagement/leases"); + Navigation.NavigateTo($"/propertymanagement/properties/{lease?.PropertyId}"); } private void CreateInvoice() diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/MaintenanceCreateForm.razor similarity index 100% rename from 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/CreateForm.razor rename to 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/MaintenanceCreateForm.razor diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/EditForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/MaintenanceEditForm.razor similarity index 100% rename from 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/EditForm.razor rename to 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/MaintenanceEditForm.razor diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/MaintenanceListForm.razor similarity index 100% rename from 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ListForm.razor rename to 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/MaintenanceListForm.razor diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/MaintenanceViewForm.razor similarity index 100% rename from 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/ViewForm.razor rename to 3-Aquiis.UI.Shared/Features/PropertyManagement/MaintenanceRequests/MaintenanceViewForm.razor diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/CreateForm.razor index 223fa75..cee2403 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/CreateForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/CreateForm.razor @@ -247,12 +247,13 @@ Notes = paymentModel.Notes! }; await PaymentService.CreateAsync(payment); - Navigation.NavigateTo("/propertymanagement/payments"); + + Navigation.NavigateTo($"/propertymanagement/invoices/{paymentModel.InvoiceId}"); } private void Cancel() { - Navigation.NavigateTo("/propertymanagement/payments"); + Navigation.NavigateTo($"/propertymanagement/invoices/{paymentModel.InvoiceId}"); } public class PaymentModel diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor index db106a2..678e376 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor @@ -329,7 +329,7 @@ else if (!string.IsNullOrEmpty(userId)) { - payments = await PaymentService.GetAllAsync(); + payments = await PaymentService.GetPaymentsWithRelationsAsync(); FilterPayments(); UpdateStatistics(); } diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor index b67c38c..c630f64 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor @@ -78,7 +78,7 @@ else

- + @payment.Invoice.InvoiceNumber

@@ -246,18 +246,18 @@ else Download Receipt } - + View Invoice @if (payment.Invoice?.Lease != null) { - + View Lease - + View Property - + View Tenant } @@ -291,6 +291,40 @@ else }
+ +
+
+
Status Guide
+
+
+ Payment status definitions: + +
+ Completed + Payment successfully processed +
+ +
+ Pending + Payment submitted, awaiting clearance +
+ +
+ Failed + Payment attempt failed (bounced check, declined card) +
+ +
+ Refunded + Payment returned to payer +
+ +
+ Voided + Created in error, cancelled before processing +
+
+
} diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyCreateForm.razor similarity index 95% rename from 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/CreateForm.razor rename to 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyCreateForm.razor index 0e3b121..dd507ea 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/CreateForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyCreateForm.razor @@ -18,8 +18,8 @@ SubmitButtonText="Create Property" IsSubmitting="@isSubmitting" ErrorMessage="@errorMessage" - OnValidSubmit="SaveProperty" - OnCancel="Cancel" /> + OnValidSubmit="@SaveProperty" + OnCancel="@Cancel" /> diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/EditForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyEditForm.razor similarity index 96% rename from 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/EditForm.razor rename to 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyEditForm.razor index 4930bd4..e0f4c29 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/EditForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyEditForm.razor @@ -37,11 +37,8 @@ else SubmitButtonText="Update Property" IsSubmitting="@isSubmitting" ErrorMessage="@errorMessage" - SuccessMessage="@successMessage" - ShowViewButton="true" OnValidSubmit="UpdatePropertyAsync" - OnCancel="Cancel" - OnView="ViewProperty" /> + OnCancel="Cancel" /> diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyListForm.razor similarity index 100% rename from 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ListForm.razor rename to 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyListForm.razor diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyViewForm.razor similarity index 59% rename from 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ViewForm.razor rename to 3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyViewForm.razor index eaa3824..0584da6 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/ViewForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyViewForm.razor @@ -112,74 +112,77 @@ else -
-
-
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) - { - - } + @if(ShowMaintenanceRequests) + { +
+
+
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") + } +
- @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
} -
- @if (maintenanceRequests.Count > 5) - {
- Showing 5 of @maintenanceRequests.Count requests +
} -
- -
- } - else - { -
- -

No maintenance requests for this property

- -
- } + else + { +
+ +

No maintenance requests for this property

+ +
+ } +
-
- + } + - @if (propertyDocuments.Any()) + @if (ShowDocuments && propertyDocuments.Any()) {
@@ -261,81 +264,85 @@ 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(); - if (lastInspection != null) + @if(ShowInspections) + { +
+
+
Routine Inspection
+
+
+ @if (property.LastRoutineInspectionDate.HasValue) + { +
+ Last Routine Inspection: +

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

+ @if (propertyInspections.Any()) { - - - View Last Routine Inspection - - + var lastInspection = propertyInspections.Where(i => i.CompletedOn == property.LastRoutineInspectionDate.Value && i.InspectionType == "Routine").FirstOrDefault(); + if (lastInspection != null) + { + + + 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) + @if (property.NextRoutineInspectionDueDate.HasValue) { -
- - - Overdue by @property.DaysOverdue days - +
+ 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 if (property.DaysUntilInspectionDue <= 30) + else { -
- - - Due in @property.DaysUntilInspectionDue days - +
+ No inspection scheduled
} - } - else - { -
- No inspection scheduled -
- } -
- +
+ +
-
+ } + @if (activeLeases.Any()) {
@@ -360,72 +367,75 @@ else } -
-
-
Completed Checklists
- -
-
- @if (propertyChecklists.Any()) - { -
- @foreach (var checklist in propertyChecklists.OrderByDescending(c => c.CompletedOn ?? c.CreatedOn).Take(5)) - { -
-
-
-
- @checklist.Name - @checklist.Status + @if(ShowCheckLists) + { +
+
+
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") + } +
- @checklist.ChecklistType - - @if (checklist.CompletedOn.HasValue) - { - Completed: @checklist.CompletedOn.Value.ToString("MMM dd, yyyy") - } - else +
+ + @if (checklist.Status != ApplicationConstants.ChecklistStatuses.Completed) { - Created: @checklist.CreatedOn.ToString("MMM dd, yyyy") + } - -
-
- - @if (checklist.Status != ApplicationConstants.ChecklistStatuses.Completed) - { - - } +
+ } +
+ @if (propertyChecklists.Count > 5) + { +
+ Showing 5 of @propertyChecklists.Count checklists
} -
- @if (propertyChecklists.Count > 5) + } + else { -
- Showing 5 of @propertyChecklists.Count checklists +
+ +

No checklists for this property

+
} - } - else - { -
- -

No checklists for this property

- -
- } +
-
+ }
} @@ -434,6 +444,18 @@ else [Parameter] public Guid PropertyId { get; set; } + [Parameter] + public bool ShowCheckLists { get; set; } = true; + + [Parameter] + public bool ShowDocuments { get; set; } = true; + + [Parameter] + public bool ShowMaintenanceRequests { get; set; } = true; + + [Parameter] + public bool ShowInspections { get; set; } = true; + private Guid LeaseId { get; set; } private List activeLeases = new(); private List propertyDocuments = new(); @@ -490,16 +512,16 @@ else } private void EditProperty() => NavigationManager.NavigateTo($"/propertymanagement/properties/{PropertyId}/edit"); - private void CreateLease() => NavigationManager.NavigateTo($"/propertymanagement/leases/create/?propertyid={PropertyId}"); + 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 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 void CreateChecklist() => NavigationManager.NavigateTo("/propertymanagement/checklists"); - private void ViewChecklist(Guid checklistId) => NavigationManager.NavigateTo($"/propertymanagement/checklists/view/{checklistId}"); + private void ViewChecklist(Guid checklistId) => NavigationManager.NavigateTo($"/propertymanagement/checklists/{checklistId}"); private void CompleteChecklist(Guid checklistId) => NavigationManager.NavigateTo($"/propertymanagement/checklists/complete/{checklistId}"); private async Task ViewDocument(Document doc) diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/CreateForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/CreateForm.razor index 82eae13..d7bb008 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/CreateForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Tenants/CreateForm.razor @@ -102,6 +102,10 @@
@code { + [Parameter] + [SupplyParameterFromQuery(Name = "PropertyId")] + public Guid? PropertyId { get; set; } + private TenantModel tenantModel = new TenantModel(); private bool isSubmitting = false; private string errorMessage = string.Empty; @@ -109,6 +113,19 @@ [CascadingParameter] private Task AuthenticationStateTask { get; set; } = default!; + protected override void OnInitialized() + { + // Manually parse PropertyId from query string for InteractiveServer rendermode + var uri = new Uri(NavigationManager.Uri); + var query = System.Web.HttpUtility.ParseQueryString(uri.Query); + var propertyIdString = query["PropertyId"]; + + if (!string.IsNullOrEmpty(propertyIdString) && Guid.TryParse(propertyIdString, out var propertyId)) + { + PropertyId = propertyId; + } + } + private async Task SaveTenant() { try @@ -151,8 +168,8 @@ IsActive = true }; - await TenantService.CreateAsync(tenant); - NavigationManager.NavigateTo("/propertymanagement/tenants"); + var newTenant = await TenantService.CreateAsync(tenant); + NavigationManager.NavigateTo("/propertymanagement/leases/create/?PropertyId=" + (PropertyId.HasValue ? PropertyId.ToString() : "") + "&TenantId=" + newTenant.Id); } catch (Exception ex) { @@ -166,7 +183,7 @@ private void Cancel() { - NavigationManager.NavigateTo("/propertymanagement/tenants"); + NavigationManager.NavigateTo("/propertymanagement/leases/create/?PropertyId=" + (PropertyId.HasValue ? PropertyId.ToString() : "")); } public class TenantModel diff --git a/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor b/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor index 65816a0..f76bc80 100644 --- a/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor +++ b/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor @@ -27,9 +27,12 @@

User Management

- - Add User - + @* Max users for SimpleStart is 2. *@ + @if(@totalUsers < 2){ + + Add User + + } diff --git a/4-Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor index 2c40516..40a810c 100644 --- a/4-Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor +++ b/4-Aquiis.SimpleStart/Features/Administration/Users/Pages/Create.razor @@ -249,6 +249,22 @@ return; } + var newUserProfile = new UserProfile + { + Id = Guid.NewGuid(), + UserId = newUser.Id, + FirstName = newUser.FirstName, + LastName = newUser.LastName, + Email = newUser.Email, + PhoneNumber = newUser.PhoneNumber, + ActiveOrganizationId = currentOrganizationId.Value, + OrganizationId = currentOrganizationId.Value, + CreatedBy = currentUserId, + CreatedOn = DateTime.UtcNow + }; + + await UserContext.CreateUserProfile(newUserProfile); + successMessage = $"User account created successfully! Username: {userModel.Email}, Role: {userModel.SelectedRole}"; // Reset form diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Edit.razor index 0cf17db..9711419 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Inspections/Pages/Edit.razor @@ -1 +1 @@ -@page "/propertymanagement/inspections/{InspectionId:guid}/" \ No newline at end of file +@page "/propertymanagement/inspections/{InspectionId:guid}/edit" \ No newline at end of file diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Edit.razor index 4b5ce36..e7bd6c8 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/Edit.razor @@ -1,4 +1,4 @@ -@page "/propertymanagement/invoices/edit/{Id:guid}" +@page "/propertymanagement/invoices/{Id:guid}/edit" @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/View.razor index 4a11104..f9b3fc8 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Invoices/Pages/View.razor @@ -1,4 +1,4 @@ -@page "/propertymanagement/invoices/view/{Id:guid}" +@page "/propertymanagement/invoices/{Id:guid}" @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Index.razor index 94b987e..56a08e2 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/Index.razor @@ -1,5 +1,523 @@ @page "/propertymanagement/leases" + +@using Microsoft.AspNetCore.Components.Authorization +@using Aquiis.Core.Entities +@using Aquiis.Application.Services +@using Aquiis.UI.Shared.Components.Entities.Leases +@inject NavigationManager NavigationManager +@inject LeaseService LeaseService +@inject TenantService TenantService +@inject PropertyService PropertyService +@inject IJSRuntime JSRuntime + @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer - +
+
+

Leases

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

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

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

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

+ } +
+
+ @* Hidden features available in Professional *@ + @* + *@ + @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 +{ + + + @if (totalPages > 1 && !groupByProperty) + { + + } +} + +@code { + [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!; + + 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; + + 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.GetLeasesWithRelationsAsync(); + leases = allLeases + .Where(l => + (!TenantId.HasValue || l.TenantId == TenantId.Value) && + (!PropertyId.HasValue || l.PropertyId == PropertyId.Value)) + .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(); + } + + // Event handlers for LeaseListView component + private async Task HandleSearchChanged(string value) + { + searchTerm = value; + FilterLeases(); + await Task.CompletedTask; + } + + private async Task HandleStatusFilterChanged(string value) + { + selectedLeaseStatus = value; + FilterLeases(); + await Task.CompletedTask; + } + + private async Task HandleTenantFilterChanged(string value) + { + selectedTenantId = string.IsNullOrEmpty(value) ? null : Guid.Parse(value); + FilterLeases(); + await Task.CompletedTask; + } + + private async Task HandleGroupByPropertyChanged(bool value) + { + groupByProperty = value; + FilterLeases(); + await Task.CompletedTask; + } + + private async Task HandleSort((string Column, bool Ascending) sort) + { + sortColumn = sort.Column; + sortAscending = sort.Ascending; + currentPage = 1; + FilterLeases(); + await Task.CompletedTask; + } + + private async Task HandleTogglePropertyGroup(Guid propertyId) + { + TogglePropertyGroup(propertyId); + await Task.CompletedTask; + } + + private async Task HandleView(Guid leaseId) + { + ViewLease(leaseId); + await Task.CompletedTask; + } + + private async Task HandleEdit(Guid leaseId) + { + EditLease(leaseId); + await Task.CompletedTask; + } + + private async Task HandleDelete(Guid leaseId) + { + await DeleteLease(leaseId); + } + + 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 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 void ViewLeaseOffers() + { + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); + } + + private void CreateLease() + { + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); + } + + private void CreateLeaseForTenant() + { + NavigationManager.NavigateTo("/propertymanagement/leaseoffers"); + } + + private void CreateLeaseForProperty() + { + 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; + + var confirmed = await JSRuntime.InvokeAsync("confirm", $"Are you sure you want to delete lease {id}?"); + if (!confirmed) + return; + + await LeaseService.DeleteAsync(id); + await LoadLeases(); + } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor index ee2d8bc..a2c897e 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor @@ -1,9 +1,139 @@ -@page "/propertymanagement/leases/{Id:guid}/view" +@page "/propertymanagement/leases/{Id:guid}" +@using Aquiis.SimpleStart.Features.PropertyManagement.Documents.Pages +@using Aquiis.UI.Shared.Components.Entities.Leases + +@inject LeaseService LeaseService +@inject NavigationManager Navigation + @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer - +
+

Lease Details

+
+ + +
+
+ +
+
+ + + +
+
+ + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+@* *@ @code { - [Parameter] public Guid Id { get; set; } -} + [Parameter] + public Guid Id { get; set; } + + private Lease lease = new Lease(); + + private List recentInvoices = new List(); + + private List LeaseDocuments = new List(); + + protected override async Task OnInitializedAsync() + { + lease = await LeaseService.GetLeaseWithRelationsAsync(Id) ?? new Lease(); + recentInvoices = lease.Invoices + .OrderByDescending(i => i.InvoicedOn) + .ToList(); + LeaseDocuments = lease.Documents! + .OrderByDescending(d => d.CreatedOn) + .ToList(); + } + + private void BackToProperty() + { + Navigation.NavigateTo($"/propertymanagement/properties/{lease?.PropertyId}"); + } + + private void EditLease() + { + Navigation.NavigateTo($"/propertymanagement/leases/{lease?.Id}/edit"); + } + + private void CreateInvoice(Guid leaseId) + { + Navigation.NavigateTo($"/propertymanagement/invoices/create?LeaseId={leaseId}"); + } + + private void ViewInvoice(Guid invoiceId) + { + Navigation.NavigateTo($"/propertymanagement/invoices/{invoiceId}"); + } + + private void GenerateLeaseDocument(Guid leaseId) + { + // Implement document generation logic here + } + + private void ViewDocument(Guid leaseId) + { + // Implement document viewing logic here + // Navigate to the lease document or open in viewer + if (lease.DocumentId.HasValue) + { + Navigation.NavigateTo($"/propertymanagement/documents/{lease.DocumentId}"); + } + } + + private void DownloadDocument(Guid leaseId) + { + // Implement document downloading logic here + // Trigger download for the lease document + } + + private void ViewDocuments(Guid leaseId) + { + Navigation.NavigateTo($"/propertymanagement/documents/?LeaseId={leaseId}"); + } + + private void ViewInvoices(Guid leaseId) + { + Navigation.NavigateTo($"/propertymanagement/invoices/?LeaseId={leaseId}"); + } + + private void CreatePayment(Guid invoiceId) + { + Navigation.NavigateTo($"/propertymanagement/payments/create?InvoiceId={invoiceId}"); + } +} \ 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 index 02faf70..4b01c7d 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Create.razor @@ -1,12 +1,329 @@ @page "/propertymanagement/maintenance/create/{PropertyId:guid?}" -@using Aquiis.UI.Shared.Features.PropertyManagement.MaintenanceRequests + +@using System.ComponentModel.DataAnnotations +@inject MaintenanceService MaintenanceService +@inject PropertyService PropertyService +@inject LeaseService LeaseService + +@inject UserContextService UserContextService +@inject NavigationManager NavigationManager + @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer - +
+
+

Record Repair

+ +
+ + @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) + { + + } + + +
+
+ +
+
+ + + +
+
+ +
+
+ + +
+
+ + + +
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
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; + + private ApplicationUser currentUser = default!; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + currentUser = await UserContextService.GetCurrentUserAsync() ?? throw new InvalidOperationException("User context is required"); + } + + 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 = currentUser.Id, + RequestedByEmail = currentUser.Email!, + RequestedByPhone = currentUser.PhoneNumber!, + RequestedOn = maintenanceRequest.CompletedOn, + ScheduledOn = maintenanceRequest.CompletedOn, + CompletedOn = maintenanceRequest.CompletedOn, + ActualCost = maintenanceRequest.ActualCost, + 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 CompletedOn { get; set; } = DateTime.Today; + + public DateTime? ScheduledOn { get; set; } + + public decimal ActualCost { 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 index fb4c298..de894b1 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Edit.razor @@ -5,7 +5,7 @@ Edit Maintenance Request - + @code { [Parameter] diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor index 396f7d3..8e16b70 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/Index.razor @@ -3,4 +3,4 @@ @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer - + diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor index 585827c..87a17e0 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/MaintenanceRequests/Pages/View.razor @@ -5,7 +5,7 @@ Maintenance Request Details - + @code { [Parameter] diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor index 210ea29..f0a7f13 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Create.razor @@ -2,4 +2,4 @@ @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer - + diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Delete.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Delete.razor new file mode 100644 index 0000000..413b7d6 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Delete.razor @@ -0,0 +1,327 @@ +@page "/propertymanagement/properties/delete/{PropertyId:guid}" + +@inject PropertyService PropertyService +@inject LeaseService LeaseService +@inject InspectionService InspectionService +@inject InvoiceService InvoiceService +@inject NavigationManager Navigation + +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@rendermode InteractiveServer + +Delete Property - Aquiis + +
+ @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (property == null) + { +
+ Property not found. +
+ } + else + { +
+
+
+ +

Delete Property

+
+ +
+
+
@property.Address
+

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

+ + @if (hasBlockers) + { +
+
Cannot Delete Property
+

The following items must be resolved before this property can be deleted:

+
+ + @if (activeLeases.Any() || upcomingLeases.Any()) + { +
+
+
Active or Upcoming Leases (@(activeLeases.Count + upcomingLeases.Count))
+
+
+

Terminate or decline these leases before deleting the property:

+
    + @foreach (var lease in activeLeases) + { +
  • +
    +
    + @lease.Tenant?.FullName +
    + + Status: @lease.Status | + @lease.StartDate.ToString("MM/dd/yyyy") - @lease.EndDate.ToString("MM/dd/yyyy") + +
    + + View + +
    +
  • + } + @foreach (var lease in upcomingLeases) + { +
  • +
    +
    + @lease.Tenant?.FullName +
    + + Status: @lease.Status | + @lease.StartDate.ToString("MM/dd/yyyy") - @lease.EndDate.ToString("MM/dd/yyyy") + +
    + + View + +
    +
  • + } +
+
+
+ } + + @if (outstandingInspections.Any()) + { +
+
+
Inspections with Action Items (@outstandingInspections.Count)
+
+
+

Complete or resolve these action items:

+
    + @foreach (var inspection in outstandingInspections) + { +
  • +
    +
    + @inspection.InspectionType Inspection +
    + + Completed: @inspection.CompletedOn.ToString("MM/dd/yyyy") | + Condition: @inspection.OverallCondition + + @if (!string.IsNullOrWhiteSpace(inspection.ActionItemsRequired)) + { +
    + Action Items: +

    @inspection.ActionItemsRequired

    +
    + } +
    + + View + +
    +
  • + } +
+
+
+ } + + @if (unpaidInvoices.Any()) + { +
+
+
Unpaid Invoices (@unpaidInvoices.Count)
+
+
+

Collect payment, void, or cancel these invoices:

+
    + @foreach (var invoice in unpaidInvoices) + { +
  • +
    +
    + @invoice.InvoiceNumber - @invoice.Description +
    + + Status: @invoice.Status | + Amount: @invoice.Amount.ToString("C") | + Balance: @invoice.BalanceDue.ToString("C") | + Due: @invoice.DueOn.ToString("MM/dd/yyyy") + @if (invoice.IsOverdue) + { + (@invoice.DaysOverdue days overdue) + } + +
    + + View + +
    +
  • + } +
+
+
+ } + +
+ +
+ } + else + { +
+
Confirm Deletion
+

Are you sure you want to delete this property? This action cannot be undone.

+
    +
  • All property information will be permanently removed
  • +
  • Historical lease and inspection records will be preserved
  • +
  • This action is irreversible
  • +
+
+ +
+ + +
+ } +
+
+
+
+ } +
+ +@code { + [Parameter] + public Guid PropertyId { get; set; } + + private Property? property; + private List activeLeases = new(); + private List upcomingLeases = new(); + private List outstandingInspections = new(); + private List unpaidInvoices = new(); + + private bool isLoading = true; + private bool isDeleting = false; + private bool hasBlockers = false; + + protected override async Task OnInitializedAsync() + { + await LoadPropertyAndBlockers(); + } + + private async Task LoadPropertyAndBlockers() + { + isLoading = true; + + try + { + property = await PropertyService.GetByIdAsync(PropertyId); + + if (property != null) + { + // Check for active/upcoming leases + var allLeases = await LeaseService.GetAllAsync(); + var propertyLeases = allLeases.Where(l => l.PropertyId == PropertyId).ToList(); + + activeLeases = propertyLeases + .Where(l => l.Status == ApplicationConstants.LeaseStatuses.Active) + .ToList(); + + upcomingLeases = propertyLeases + .Where(l => l.StartDate > DateTime.Today && + l.Status != ApplicationConstants.LeaseStatuses.Terminated && + l.Status != ApplicationConstants.LeaseStatuses.Declined && + l.Status != ApplicationConstants.LeaseStatuses.Expired) + .ToList(); + + // Check for inspections with action items + var allInspections = await InspectionService.GetAllAsync(); + outstandingInspections = allInspections + .Where(i => i.PropertyId == PropertyId && + !string.IsNullOrWhiteSpace(i.ActionItemsRequired)) + .ToList(); + + // Check for unpaid invoices (through leases) + var allInvoices = await InvoiceService.GetAllAsync(); + var propertyLeaseIds = propertyLeases.Select(l => l.Id).ToHashSet(); + unpaidInvoices = allInvoices + .Where(inv => propertyLeaseIds.Contains(inv.LeaseId) && + inv.Status != ApplicationConstants.InvoiceStatuses.Paid && + inv.Status != ApplicationConstants.InvoiceStatuses.Cancelled && + inv.Status != ApplicationConstants.InvoiceStatuses.Voided) + .ToList(); + + hasBlockers = activeLeases.Any() || upcomingLeases.Any() || + outstandingInspections.Any() || unpaidInvoices.Any(); + } + } + finally + { + isLoading = false; + } + } + + private async Task ConfirmDelete() + { + if (hasBlockers) + { + return; // Should not happen due to UI hiding the button + } + + isDeleting = true; + + try + { + await PropertyService.DeleteAsync(PropertyId); + Navigation.NavigateTo("/propertymanagement/properties"); + } + catch (Exception) + { + // Handle error (could add error message display) + isDeleting = false; + } + } + + private void Cancel() + { + Navigation.NavigateTo("/propertymanagement/properties"); + } + + private string GetInvoiceStatusBadgeClass(string status) + { + return status switch + { + ApplicationConstants.InvoiceStatuses.Paid => "bg-success", + ApplicationConstants.InvoiceStatuses.Pending => "bg-warning", + ApplicationConstants.InvoiceStatuses.Overdue => "bg-danger", + ApplicationConstants.InvoiceStatuses.PaidPartial => "bg-info", + ApplicationConstants.InvoiceStatuses.Cancelled => "bg-secondary", + ApplicationConstants.InvoiceStatuses.Voided => "bg-secondary", + _ => "bg-secondary" + }; + } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor index 2edf4b2..32e746c 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Edit.razor @@ -1,8 +1,9 @@ @page "/propertymanagement/properties/{PropertyId:guid}/edit" @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer +@using Aquiis.UI.Shared.Features.PropertyManagement.Properties - + @code { [Parameter] diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor index 62a0547..47a0f40 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor @@ -1,5 +1,177 @@ @page "/propertymanagement/properties" +@using Aquiis.Core.Entities + +@inject PropertyService PropertyService +@inject NavigationManager NavigationManager @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager", "User")] @rendermode InteractiveServer - + +
+

Properties

+
+
+ + +
+ @if (Properties.Count < maxProperties) + { + + } +
+
+
+ +
+ + +@code { + private List Properties = new List(); + private List filteredProperties = new List(); + private PropertyListViewMode viewMode = PropertyListViewMode.Table; + private string sortColumn = nameof(Property.Address); + private bool sortAscending = true; + private string statusFilter = string.Empty; + + private string searchTerm = string.Empty; + + private int availableProperties = 0; + + private int maxProperties = 9; + + private int occupiedProperties = 0; + + private decimal totalMonthlyRent = 0.00m; + + protected override async Task OnInitializedAsync() + { + Properties = await PropertyService.GetAllAsync(); + + availableProperties = Properties.Where(p => p.IsAvailable).Count(); + occupiedProperties = Properties.Where(p => !p.IsAvailable).Count(); + totalMonthlyRent = Properties.Where(p => !p.IsAvailable).Sum(p => p.MonthlyRent); + + ApplySort(); + } + + private void HandleSort((string Column, bool Ascending) sortInfo) + { + sortColumn = sortInfo.Column; + sortAscending = sortInfo.Ascending; + ApplySort(); + } + + private void ApplySort() + { + // Apply status filter first + var propertiesToSort = string.IsNullOrEmpty(statusFilter) + ? Properties + : Properties.Where(p => p.Status == statusFilter).ToList(); + + // Then apply sorting + filteredProperties = sortColumn switch + { + nameof(Property.Address) => sortAscending + ? propertiesToSort.OrderBy(p => p.Address).ToList() + : propertiesToSort.OrderByDescending(p => p.Address).ToList(), + nameof(Property.City) => sortAscending + ? propertiesToSort.OrderBy(p => p.City).ToList() + : propertiesToSort.OrderByDescending(p => p.City).ToList(), + nameof(Property.PropertyType) => sortAscending + ? propertiesToSort.OrderBy(p => p.PropertyType).ToList() + : propertiesToSort.OrderByDescending(p => p.PropertyType).ToList(), + nameof(Property.SquareFeet) => sortAscending + ? propertiesToSort.OrderBy(p => p.SquareFeet).ToList() + : propertiesToSort.OrderByDescending(p => p.SquareFeet).ToList(), + nameof(Property.Status) => sortAscending + ? propertiesToSort.OrderBy(p => p.Status).ToList() + : propertiesToSort.OrderByDescending(p => p.Status).ToList(), + nameof(Property.MonthlyRent) => sortAscending + ? propertiesToSort.OrderBy(p => p.MonthlyRent).ToList() + : propertiesToSort.OrderByDescending(p => p.MonthlyRent).ToList(), + _ => propertiesToSort.OrderBy(p => p.Address).ToList() + }; + } + + private void SetViewMode(PropertyListViewMode mode) + { + viewMode = mode; + } + + private void StatusFilterChanged(string status) + { + statusFilter = status; + ApplySort(); + } + + private void ClearFilters() + { + StatusFilterChanged(string.Empty); + SearchChanged(string.Empty); + } + + private void SearchChanged(string term) + { + searchTerm = term; + if (string.IsNullOrWhiteSpace(searchTerm)) + { + ApplySort(); + } + else + { + filteredProperties = filteredProperties + .Where(p => p.Address.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + p.City.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + p.PropertyType.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + } + + private void CreateProperty() + { + NavigationManager.NavigateTo("/propertymanagement/properties/create"); + } + + private void EditProperty(Guid propertyId) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/{propertyId}/edit"); + } + + private void ViewPropertyDetails(Guid propertyId) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/{propertyId}"); + } + + private void DeleteProperty(Guid propertyId) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/delete/{propertyId}"); + } +} \ 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 index 0748e9f..336bc25 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor @@ -1,10 +1,46 @@ @page "/propertymanagement/properties/{PropertyId:guid}" + +@using Aquiis.UI.Shared.Features.PropertyManagement.Properties + +@inject PropertyService PropertyService +@inject NavigationManager Navigation + @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer - + + @code { [Parameter] public Guid PropertyId { get; set; } + + private Property property = new Property(); + + protected override async Task OnInitializedAsync() + { + property = await PropertyService.GetByIdAsync(PropertyId) ?? new Property(); + } + + private void EditProperty() + { + Navigation.NavigateTo($"/propertymanagement/properties/{PropertyId}/edit"); + } + + private void CreateRequest() + { + Navigation.NavigateTo($"/propertymanagement/maintenance/create/{PropertyId}"); + } + + private void ViewRequest(Guid requestId) + { + Navigation.NavigateTo($"/propertymanagement/maintenance/{requestId}"); + } + + private void ViewAllRequests() + { + Navigation.NavigateTo($"/propertymanagement/maintenance/?PropertyId={PropertyId}"); + } } diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor index c02c25b..50ad8f6 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Tenants/Pages/View.razor @@ -1,4 +1,4 @@ -@page "/propertymanagement/tenants/view/{Id:guid}" +@page "/propertymanagement/tenants/{Id:guid}" @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor index e7e8001..2a9ca49 100644 --- a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Manage/Email.razor @@ -42,11 +42,11 @@
}
- +
- +
diff --git a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor index 04df98c..a13f436 100644 --- a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor +++ b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor @@ -21,7 +21,6 @@
} + @if(ShowTotal){ +
+
+
+
Total Properties
+

@TotalPropertiesCount

+
+
+
+ } + @if(ShowActiveLeases){ +
+
+
+
Active Leases
+

@ActiveLeasesCount

+
+
+
+ } + @if(ShowActiveTenants){ +
+
+
+
Active Tenants
+

@ActiveTenantsCount

+
+
+
+ } + @if(ShowOccupancyRate){ +
+
+
+
Occupancy Rate
+

@OccupancyRate.ToString("P")

+
+
+
+ }
@code { + [Parameter] + public bool ShowTotal { get; set; } = true; + [Parameter] public bool ShowAvailable { get; set; } = true; [Parameter] - public bool ShowPending { get; set; } = true; + public bool ShowPending { get; set; } = false; [Parameter] public bool ShowOccupied { get; set; } = true; @@ -56,6 +99,24 @@ [Parameter] public bool ShowMonthlyRent { get; set; } = true; + [Parameter] + public bool ShowActiveTenants { get; set; } = false; + + [Parameter] + public bool ShowActiveLeases { get; set; } = false; + + [Parameter] + public bool ShowOccupancyRate { get; set; } = false; + + [Parameter] + public int TotalPropertiesCount { get; set; } + + [Parameter] + public int ActiveTenantsCount { get; set; } + + [Parameter] + public int ActiveLeasesCount { get; set; } + [Parameter] public int AvailableCount { get; set; } @@ -67,4 +128,7 @@ [Parameter] public decimal TotalMonthlyRent { get; set; } + + [Parameter] + public decimal OccupancyRate { get; set; } } \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyOccupancyMetricsCard.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyOccupancyMetricsCard.razor new file mode 100644 index 0000000..e5490a4 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyOccupancyMetricsCard.razor @@ -0,0 +1,70 @@ +
+
+ @if(ShowAvailable){ +
+
+
+
Available
+

@AvailableCount

+
+
+
+ } + @if(ShowPending){ +
+
+
+
Pending Lease
+

@PendingCount

+
+
+
+ } + @if(ShowOccupied){ +
+
+
+
Occupied
+

@OccupiedCount

+
+
+
+ } + @if(ShowMonthlyRent){ +
+
+
+
Total Rent/Month
+

@TotalMonthlyRent.ToString("C")

+
+
+
+ } +
+
+ + @code { + [Parameter] + public bool ShowAvailable { get; set; } = true; + + [Parameter] + public bool ShowPending { get; set; } = true; + + [Parameter] + public bool ShowOccupied { get; set; } = true; + + [Parameter] + public bool ShowMonthlyRent { get; set; } = true; + + [Parameter] + public int AvailableCount { get; set; } + + [Parameter] + public int PendingCount { get; set; } + + [Parameter] + public int OccupiedCount { get; set; } + + [Parameter] + public decimal TotalMonthlyRent { get; set; } + } \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/Repairs/RepairsListView.razor b/3-Aquiis.UI.Shared/Components/Entities/Repairs/RepairsListView.razor new file mode 100644 index 0000000..1943b3f --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Repairs/RepairsListView.razor @@ -0,0 +1,163 @@ +@using Aquiis.Core.Entities + +
+
+
+ Repairs +
+ @if (ShowAddButton && OnAddRepair.HasDelegate) + { + + } +
+
+ @if (Repairs == null) + { +
+
+ Loading... +
+
+ } + else if (!Repairs.Any()) + { +

No repairs recorded for this property.

+ } + else + { +
+ @foreach (var repair in Repairs.OrderByDescending(r => r.CompletedOn ?? r.CreatedOn)) + { +
+
+
+
+ @if (ShowLinks && OnViewRepair.HasDelegate) + { + + @repair.Description + + } + else + { + @repair.Description + } +
+
+ + @repair.RepairType + @if (!string.IsNullOrEmpty(repair.ContractorName)) + { + @repair.ContractorName + } + +
+ @if (!string.IsNullOrEmpty(repair.PartsReplaced)) + { + + Parts: @repair.PartsReplaced + + } +
+
+ @if (repair.CompletedOn.HasValue) + { + Completed +
+ @repair.CompletedOn.Value.ToString("MMM dd, yyyy") + } + else + { + In Progress +
+ Started @repair.CreatedOn.ToString("MMM dd, yyyy") + } + @if (repair.Cost > 0) + { +
+ @repair.Cost.ToString("C") +
+ } +
+
+ @if (repair.DurationMinutes > 0 || repair.WarrantyApplies) + { +
+ + @if (repair.DurationMinutes > 0) + { + + Duration: @FormatDuration(repair.DurationMinutes) + + } + @if (repair.WarrantyApplies && repair.WarrantyExpiresOn.HasValue) + { + + Warranty until @repair.WarrantyExpiresOn.Value.ToString("MMM dd, yyyy") + + } + +
+ } +
+ } +
+ + @if (ShowTotalCost) + { + + } + } +
+
+ +@code { + [Parameter] + public List? Repairs { get; set; } + + [Parameter] + public bool ShowAddButton { get; set; } = true; + + [Parameter] + public bool ShowLinks { get; set; } = true; + + [Parameter] + public bool ShowTotalCost { get; set; } = true; + + [Parameter] + public EventCallback OnAddRepair { get; set; } + + [Parameter] + public EventCallback OnViewRepair { get; set; } + + [Parameter] + public EventCallback OnViewAllRepairs { get; set; } + + private string FormatDuration(int minutes) + { + if (minutes < 60) + return $"{minutes} min"; + + var hours = minutes / 60; + var remainingMinutes = minutes % 60; + + if (remainingMinutes == 0) + return $"{hours} hr"; + + return $"{hours} hr {remainingMinutes} min"; + } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor index a2c897e..12f1f5c 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Leases/Pages/View.razor @@ -3,6 +3,10 @@ @using Aquiis.UI.Shared.Components.Entities.Leases @inject LeaseService LeaseService +@inject DocumentService DocumentService +@inject InvoiceService InvoiceService +@inject IJSRuntime JSRuntime +@inject LeasePdfGenerator LeasePdfGenerator @inject NavigationManager Navigation @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @@ -38,7 +42,8 @@ OnDownloadDocument="DownloadDocument" OnViewDocuments="ViewDocuments" OnViewInvoices="ViewInvoices" - OnEdit="EditLease" /> + OnEdit="EditLease" + IsGenerating="isGenerating" /> @@ -58,7 +63,6 @@
-@* *@ @code { [Parameter] @@ -66,9 +70,15 @@ private Lease lease = new Lease(); + private Document? document = null; + private List recentInvoices = new List(); private List LeaseDocuments = new List(); + + private List payments = new List(); + + private bool isGenerating = false; // flag to indicate if pdf document generation is in progress protected override async Task OnInitializedAsync() { @@ -79,6 +89,40 @@ LeaseDocuments = lease.Documents! .OrderByDescending(d => d.CreatedOn) .ToList(); + + payments = lease.Invoices + .SelectMany(i => i.Payments) + .OrderBy(p => p.PaidOn) + .ToList(); + + // Load the document if it exists + if (lease.DocumentId != null) + { + document = await DocumentService.GetByIdAsync(lease.DocumentId.Value); + } + } + + private async Task LoadLease() + { + + lease = await LeaseService.GetByIdAsync(lease.Id) ?? new Lease(); + + if (lease == null) + { + return; + } + + var invoices = await InvoiceService.GetInvoicesByLeaseIdAsync(lease.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 void BackToProperty() @@ -106,20 +150,75 @@ // Implement document generation logic here } - private void ViewDocument(Guid leaseId) + private async Task ViewDocument() { - // Implement document viewing logic here - // Navigate to the lease document or open in viewer - if (lease.DocumentId.HasValue) + if (document != null) { - Navigation.NavigateTo($"/propertymanagement/documents/{lease.DocumentId}"); + var base64Data = Convert.ToBase64String(document.FileData); + await JSRuntime.InvokeVoidAsync("viewFile", base64Data, document.FileType); } } - private void DownloadDocument(Guid leaseId) + private async Task GenerateLeaseDocument() { - // Implement document downloading logic here - // Trigger download for the lease document + 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(); + } + } + + 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 ViewDocuments(Guid leaseId) diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor index 336bc25..9ce50fb 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor @@ -1,27 +1,234 @@ @page "/propertymanagement/properties/{PropertyId:guid}" +@using Aquiis.SimpleStart.Features.PropertyManagement.SecurityDeposits.Pages +@using Aquiis.SimpleStart.Features.PropertyManagement.Inspections +@using Aquiis.UI.Shared.Components.Common +@using Aquiis.UI.Shared.Components.Entities.Documents +@using Aquiis.UI.Shared.Components.Entities.Inspections +@using Aquiis.UI.Shared.Components.Entities.Repairs @using Aquiis.UI.Shared.Features.PropertyManagement.Properties +@using Aquiis.UI.Shared.Features.PropertyManagement.Leases @inject PropertyService PropertyService +@inject InspectionService InspectionService +@inject LeaseService LeaseService +@inject RepairService RepairService @inject NavigationManager Navigation +@inject IJSRuntime JSRuntime @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @rendermode InteractiveServer - +
+

Property Details

+
+ + +
+
+
+
+ + +
+ +
+
+ +
+ +
+
+ +
+
+
Quick Actions
+
+
+
+ + @if (property.IsAvailable) + { + + } + else + { + + } + + +
+
+
+ +
+ +
+
+
+ + + + + @if (selectedLease != null) + { +
+
+
+ Tenant: @selectedLease.Tenant?.FullName +
+
+ Status: @selectedLease.Status +
+
+
+
+ Start Date: @selectedLease.StartDate.ToShortDateString() +
+
+ End Date: @selectedLease.EndDate.ToShortDateString() +
+
+
+
+ Monthly Rent: @selectedLease.MonthlyRent.ToString("C") +
+
+ Security Deposit: @selectedLease.SecurityDeposit.ToString("C") +
+
+
+ } +
+ + + + +
+ + + + @if (selectedRepair != null) + { +
+
+
+ Repair Type: @selectedRepair.RepairType +
+
+ Contractor: @selectedRepair.ContractorName +
+
+
+
+ Description: +

@selectedRepair.Description

+
+
+
+
+ Completed On: @(selectedRepair.CompletedOn?.ToShortDateString() ?? "In Progress") +
+
+ Cost: @selectedRepair.Cost.ToString("C") +
+
+ @if (selectedRepair.DurationMinutes > 0) + { +
+
+ Duration: @selectedRepair.DurationMinutes minutes +
+
+ } + @if (!string.IsNullOrEmpty(selectedRepair.ContractorPhone)) + { +
+
+ Contact: @selectedRepair.ContractorPhone +
+
+ } +
+ } +
+ + + + +
+ + + + @if (selectedInspection != null) + { + + } + + + + + + @code { [Parameter] public Guid PropertyId { get; set; } private Property property = new Property(); + private Inspection lastInspection = new Inspection(); + + // Modal state + private bool showLeaseModal = false; + private bool showRepairModal = false; + private bool showInspectionModal = false; + + // Selected entities for modals + private Lease? selectedLease; + private Repair? selectedRepair; + private Inspection? selectedInspection; protected override async Task OnInitializedAsync() { - property = await PropertyService.GetByIdAsync(PropertyId) ?? new Property(); + property = await PropertyService.GetPropertyWithRelationsAsync(PropertyId) ?? new Property(); + lastInspection = await InspectionService.GetLastRoutineInspectionAsync(PropertyId) ?? new Inspection(); } private void EditProperty() @@ -29,18 +236,108 @@ Navigation.NavigateTo($"/propertymanagement/properties/{PropertyId}/edit"); } - private void CreateRequest() + private void CreateRepair() + { + Navigation.NavigateTo($"/propertymanagement/repairs/create/{PropertyId}"); + } + + private async Task ViewRepair(Guid repairId) + { + selectedRepair = await RepairService.GetByIdAsync(repairId); + if (selectedRepair != null) + { + showRepairModal = true; + } + } + + private void ViewAllRepairs() + { + Navigation.NavigateTo($"/propertymanagement/repairs/?PropertyId={PropertyId}"); + } + + private async Task ViewInspection(Guid inspectionId) + { + selectedInspection = await InspectionService.GetByIdAsync(inspectionId); + if (selectedInspection != null) + { + showInspectionModal = true; + } + } + + private void CompleteInspection() + { + Navigation.NavigateTo($"/propertymanagement/inspections/create/?PropertyId={PropertyId}"); + } + + private void CreateLease() + { + Navigation.NavigateTo($"/propertymanagement/leases/create/?PropertyId={PropertyId}"); + } + + private async Task ViewLease() + { + var lease = property.Leases.FirstOrDefault(l => l.IsActive); + if (lease != null) + { + selectedLease = await LeaseService.GetLeaseWithRelationsAsync(lease.Id); + if (selectedLease != null) + { + showLeaseModal = true; + } + } + } + + private void ViewDocuments() + { + Navigation.NavigateTo($"/propertymanagement/documents/?PropertyId={PropertyId}"); + } + + private async Task ViewDocument(Guid documentId) + { + var doc = property.Documents.FirstOrDefault(d => d.Id == documentId); + if (doc == null) return; + var base64Data = Convert.ToBase64String(doc.FileData); + await JSRuntime.InvokeVoidAsync("viewFile", base64Data, doc.FileType); + @* Navigation.NavigateTo($"/propertymanagement/documents/{documentId}"); *@ + } + + private void BackToList() + { + Navigation.NavigateTo("/propertymanagement/properties"); + } + + // Modal close methods + private void CloseLeaseModal() + { + showLeaseModal = false; + selectedLease = null; + } + + private void CloseRepairModal() + { + showRepairModal = false; + selectedRepair = null; + } + + private void CloseInspectionModal() + { + showInspectionModal = false; + selectedInspection = null; + } + + // View Full Page methods + private void ViewLeaseFullPage(Guid leaseId) { - Navigation.NavigateTo($"/propertymanagement/maintenance/create/{PropertyId}"); + Navigation.NavigateTo($"/propertymanagement/leases/{leaseId}"); } - private void ViewRequest(Guid requestId) + private void ViewRepairFullPage(Guid repairId) { - Navigation.NavigateTo($"/propertymanagement/maintenance/{requestId}"); + Navigation.NavigateTo($"/propertymanagement/repairs/{repairId}"); } - private void ViewAllRequests() + private void ViewInspectionFullPage(Guid inspectionId) { - Navigation.NavigateTo($"/propertymanagement/maintenance/?PropertyId={PropertyId}"); + Navigation.NavigateTo($"/propertymanagement/inspections/{inspectionId}"); } } diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Create.razor new file mode 100644 index 0000000..215a218 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Create.razor @@ -0,0 +1,276 @@ +@page "/propertymanagement/repairs/create" +@page "/propertymanagement/repairs/create/{PropertyId:guid}" + + +@using Aquiis.Core.Entities +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject RepairService RepairService +@inject PropertyService PropertyService +@inject NavigationManager Navigation +@inject ToastService ToastService +@rendermode InteractiveServer + +New Repair - Aquiis + +
+
+

+ New Repair +

+ +
+ +
+
+
+
+
Repair Information
+
+
+ + + + +
+
+ +
+ + + + @foreach (var property in properties) + { + + } + + +
+ + +
+ + + + @foreach (var type in ApplicationConstants.RepairTypes.AllRepairTypes) + { + + } + + +
+
+ +
+ +
+ + + +
+
+ +
+ +
+ + + e.g., "ABC Plumbing" or "John Doe" +
+ + +
+ + + e.g., "Jane Smith" at company +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + + Leave blank if work is still in progress +
+ + +
+ + + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+ $ + +
+ +
+ + +
+
+ + +
+
+ + @if (model.WarrantyApplies) + { +
+ + +
+ } +
+ +
+ +
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ +
+
+
+
About Repairs
+

+ Use this form to log work performed on your properties. This is ideal for: +

+
    +
  • Quick repairs that don't need workflow tracking
  • +
  • Historical work completed before using this system
  • +
  • Work currently in progress (leave Completed On blank)
  • +
  • Contractor work with warranty tracking
  • +
+
+

+ Tip: Leave "Completed On" blank if the work is still in progress, or hasn't been started yet. + You can mark it complete later by editing the repair. +

+
+
+
+
+
+ +@code { + [Parameter] + [SupplyParameterFromQuery] + public Guid? PropertyId { get; set; } + private RepairFormModel model = new(); + private List properties = new(); + private bool isSubmitting = false; + + protected override async Task OnInitializedAsync() + { + try + { + properties = await PropertyService.GetAllAsync(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading properties: {ex.Message}"); + } + if (PropertyId.HasValue && PropertyId != Guid.Empty) + { + model.PropertyId = PropertyId.Value; + } + } + + private async Task HandleSubmit() + { + isSubmitting = true; + try + { + // Map form model to entity - service will set tracking fields + var repair = new Repair + { + PropertyId = model.PropertyId, + Description = model.Description, + RepairType = model.RepairType, + ContractorName = model.ContractorName, + ContactPerson = model.ContactPerson, + ContractorPhone = model.ContractorPhone, + CompletedOn = model.CompletedOn, + Cost = model.Cost, + DurationMinutes = model.DurationMinutes, + PartsReplaced = model.PartsReplaced, + Notes = model.Notes, + WarrantyApplies = model.WarrantyApplies, + WarrantyExpiresOn = model.WarrantyExpiresOn, + MaintenanceRequestId = model.MaintenanceRequestId, + LeaseId = model.LeaseId + }; + + await RepairService.CreateAsync(repair); + ToastService.ShowSuccess("Repair logged successfully."); + Navigation.NavigateTo("/propertymanagement/repairs"); + } + catch (Exception ex) + { + ToastService.ShowError($"Error saving repair: {ex.Message}"); + isSubmitting = false; + } + } + + private void Cancel() => Navigation.NavigateTo("/propertymanagement/repairs"); +} + diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Edit.razor new file mode 100644 index 0000000..ea8831a --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Edit.razor @@ -0,0 +1,358 @@ +@page "/propertymanagement/repairs/edit/{id:guid}" +@using Aquiis.Core.Entities +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] +@inject RepairService RepairService +@inject PropertyService PropertyService +@inject NavigationManager Navigation +@inject ToastService ToastService +@rendermode InteractiveServer + +Edit Repair - Aquiis + +
+
+

+ Edit Repair +

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

Loading repair...

+
+ } + else if (repair == null) + { +
+ + Repair not found. +
+ } + else + { +
+
+
+
+
Repair Information
+
+
+ + + + +
+ +
+ + + + @foreach (var property in properties) + { + + } + + +
+ + +
+ + + + @foreach (var type in ApplicationConstants.RepairTypes.AllRepairTypes) + { + + } + + +
+ + +
+ + + +
+ + +
+ + + e.g., "ABC Plumbing" or "John Doe" +
+ + +
+ + + e.g., "Jane Smith" at company +
+ +
+ +
+ + +
+
+ + +
+ + + Leave blank if work is still in progress +
+ + +
+ + + +
+ + +
+ + +
+ + +
+ +
+ $ + +
+ +
+ + +
+
+ + +
+
+ + @if (model.WarrantyApplies) + { +
+ + +
+ } + + +
+ + +
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+
+
+ +
+
+
+
Audit Information
+
+
+
+
Created:
+
@repair.CreatedOn.ToShortDateString()
+ +
Created By:
+
@repair.CreatedBy
+ + @if (repair.LastModifiedOn.HasValue) + { +
Last Modified:
+
@repair.LastModifiedOn.Value.ToShortDateString()
+ +
Modified By:
+
@repair.LastModifiedBy
+ } +
+
+
+ + @if (!repair.IsCompleted) + { +
+
+
Quick Actions
+ + + This will set the completion date to today + +
+
+ } +
+
+ } +
+ +@code { + [Parameter] public Guid Id { get; set; } + + private Repair? repair; + private RepairFormModel model = new(); + private List properties = new(); + private bool isLoading = true; + private bool isSubmitting = false; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + isLoading = true; + try + { + repair = await RepairService.GetByIdAsync(Id); + if (repair != null) + { + // Map entity to form model + model = new RepairFormModel + { + PropertyId = repair.PropertyId, + Description = repair.Description, + RepairType = repair.RepairType, + ContractorName = repair.ContractorName, + ContactPerson = repair.ContactPerson, + ContractorPhone = repair.ContractorPhone, + CompletedOn = repair.CompletedOn, + Cost = repair.Cost, + DurationMinutes = repair.DurationMinutes, + PartsReplaced = repair.PartsReplaced, + Notes = repair.Notes, + WarrantyApplies = repair.WarrantyApplies, + WarrantyExpiresOn = repair.WarrantyExpiresOn, + MaintenanceRequestId = repair.MaintenanceRequestId, + LeaseId = repair.LeaseId + }; + } + properties = await PropertyService.GetAllAsync(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading repair: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task HandleSubmit() + { + if (repair == null) return; + + isSubmitting = true; + try + { + // Map form model back to entity, preserving ID and tracking fields + repair.PropertyId = model.PropertyId; + repair.Description = model.Description; + repair.RepairType = model.RepairType; + repair.ContractorName = model.ContractorName; + repair.ContactPerson = model.ContactPerson; + repair.ContractorPhone = model.ContractorPhone; + repair.CompletedOn = model.CompletedOn; + repair.Cost = model.Cost; + repair.DurationMinutes = model.DurationMinutes; + repair.PartsReplaced = model.PartsReplaced; + repair.Notes = model.Notes; + repair.WarrantyApplies = model.WarrantyApplies; + repair.WarrantyExpiresOn = model.WarrantyExpiresOn; + repair.MaintenanceRequestId = model.MaintenanceRequestId; + repair.LeaseId = model.LeaseId; + + await RepairService.UpdateAsync(repair); + ToastService.ShowSuccess("Repair updated successfully."); + Navigation.NavigateTo("/propertymanagement/repairs"); + } + catch (Exception ex) + { + ToastService.ShowError($"Error updating repair: {ex.Message}"); + isSubmitting = false; + } + } + + private async Task MarkComplete() + { + if (repair == null) return; + + try + { + await RepairService.CompleteRepairAsync(repair.Id); + ToastService.ShowSuccess("Repair marked as completed."); + await LoadData(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error completing repair: {ex.Message}"); + } + } + + private async Task DeleteRepair() + { + try + { + await RepairService.DeleteAsync(Id); + ToastService.ShowSuccess("Repair deleted successfully."); + Navigation.NavigateTo("/propertymanagement/repairs"); + } + catch (Exception ex) + { + ToastService.ShowError($"Error deleting repair: {ex.Message}"); + } + } + + private void Cancel() => Navigation.NavigateTo("/propertymanagement/repairs"); +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Index.razor new file mode 100644 index 0000000..802645f --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Index.razor @@ -0,0 +1,288 @@ +@page "/propertymanagement/repairs" +@using Aquiis.Core.Entities +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + +@inject RepairService RepairService +@inject PropertyService PropertyService +@inject NavigationManager Navigation +@inject ToastService ToastService +@rendermode InteractiveServer + +Repairs - Aquiis + +
+
+

+ Repairs +

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

Loading repairs...

+
+ } + else if (!repairs.Any()) + { +
+ + No repairs have been logged yet. Click "Log New Repair" to record work performed on your properties. +
+ } + else + { +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + @foreach (var repair in FilteredRepairs) + { + + + + + + + + + + + } + +
PropertyDescriptionTypeCompletedCostDurationStatusActions
+ @repair.Property?.Address + @if (repair.MaintenanceRequestId.HasValue) + { +
+ Part of Maintenance Request + + } +
+ @repair.Description + @if (!string.IsNullOrEmpty(repair.ContractorName)) + { +
By: @repair.ContractorName + } +
+ @repair.RepairType + + @if (repair.CompletedOn.HasValue) + { + @repair.CompletedOn.Value.ToShortDateString() + } + else + { + - + } + + @if (repair.Cost > 0) + { + @repair.Cost.ToString("C") + } + else + { + - + } + + @if (repair.DurationMinutes > 0) + { + @repair.DurationDisplay + } + else + { + - + } + + @if (repair.IsCompleted) + { + + Completed + + } + else + { + + In Progress + + } + @if (repair.IsUnderWarranty) + { +
+ + Under Warranty + + } +
+
+ + + +
+
+
+
+ + +
+ } +
+ +@code { + private List repairs = new(); + private List properties = new(); + private bool isLoading = true; + + private string searchTerm = string.Empty; + private string selectedPropertyId = string.Empty; + private string selectedRepairType = string.Empty; + private bool showInProgressOnly = false; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + isLoading = true; + try + { + repairs = await RepairService.GetAllAsync(); + properties = await PropertyService.GetAllAsync(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading repairs: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private IEnumerable FilteredRepairs + { + get + { + var filtered = repairs.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(searchTerm)) + { + filtered = filtered.Where(r => + r.Description.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + r.ContractorName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + r.ContactPerson.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + r.Property?.Address?.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) == true); + } + + if (!string.IsNullOrEmpty(selectedPropertyId) && Guid.TryParse(selectedPropertyId, out var propId)) + { + filtered = filtered.Where(r => r.PropertyId == propId); + } + + if (!string.IsNullOrEmpty(selectedRepairType)) + { + filtered = filtered.Where(r => r.RepairType == selectedRepairType); + } + + if (showInProgressOnly) + { + filtered = filtered.Where(r => !r.IsCompleted); + } + + return filtered.OrderByDescending(r => r.CompletedOn ?? r.CreatedOn); + } + } + + private void NavigateToCreate() => Navigation.NavigateTo("/propertymanagement/repairs/create"); + private void NavigateToView(Guid id) => Navigation.NavigateTo($"/propertymanagement/repairs/view/{id}"); + private void NavigateToEdit(Guid id) => Navigation.NavigateTo($"/propertymanagement/repairs/edit/{id}"); + + private async Task DeleteRepair(Guid id) + { + try + { + await RepairService.DeleteAsync(id); + ToastService.ShowSuccess("Repair deleted successfully."); + await LoadData(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error deleting repair: {ex.Message}"); + } + } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/RepairFormModel.cs b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/RepairFormModel.cs new file mode 100644 index 0000000..0d40976 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/RepairFormModel.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; + +namespace Aquiis.SimpleStart.Features.PropertyManagement.Repairs; + +/// +/// Form model for repair create/edit operations. +/// Contains only user-provided fields, not tracking fields. +/// +public class RepairFormModel +{ + [Required(ErrorMessage = "Property is required")] + public Guid PropertyId { get; set; } + + [Required(ErrorMessage = "Description is required")] + [StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")] + public string Description { get; set; } = string.Empty; + + [Required(ErrorMessage = "Repair Type is required")] + [StringLength(50)] + public string RepairType { get; set; } = string.Empty; + + [StringLength(200)] + public string ContractorName { get; set; } = string.Empty; + + [StringLength(200)] + public string ContactPerson { get; set; } = string.Empty; + + [StringLength(50)] + public string ContractorPhone { get; set; } = string.Empty; + + public DateTime? CompletedOn { get; set; } + + [Range(0, double.MaxValue, ErrorMessage = "Cost cannot be negative")] + public decimal Cost { get; set; } = 0; + + [Range(0, int.MaxValue, ErrorMessage = "Duration cannot be negative")] + public int DurationMinutes { get; set; } = 0; + + [StringLength(500)] + public string PartsReplaced { get; set; } = string.Empty; + + [StringLength(2000)] + public string Notes { get; set; } = string.Empty; + + public bool WarrantyApplies { get; set; } = false; + + public DateTime? WarrantyExpiresOn { get; set; } + + public Guid? MaintenanceRequestId { get; set; } + + public Guid? LeaseId { get; set; } +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/View.razor new file mode 100644 index 0000000..b3a7fb9 --- /dev/null +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/View.razor @@ -0,0 +1,356 @@ +@page "/propertymanagement/repairs/{id:guid}" +@attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] + +@inject RepairService RepairService +@inject NavigationManager Navigation +@inject ToastService ToastService +@rendermode InteractiveServer + +Repair Details - Aquiis + +
+
+

+ Repair Details +

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

Loading repair details...

+
+ } + else if (repair == null) + { +
+ + Repair not found. +
+ } + else + { +
+
+ + @if (!repair.IsCompleted) + { +
+
+
+ + In Progress - This repair has not been marked as completed yet. +
+ +
+
+ } + else if (repair.IsUnderWarranty) + { +
+ + Under Warranty - Expires @repair.WarrantyExpiresOn?.ToShortDateString() +
+ } + + +
+
+
Basic Information
+
+
+
+
Property:
+
+ @repair.Property?.Address +
@repair.Property?.Address +
+ +
Repair Type:
+
+ @repair.RepairType +
+ +
Description:
+
@repair.Description
+ + @if (repair.CompletedOn.HasValue) + { +
Completed On:
+
@repair.CompletedOn.Value.ToLongDateString()
+ } + + @if (!string.IsNullOrEmpty(repair.ContractorName)) + { +
Contractor/Company:
+
@repair.ContractorName
+ } +
+
+
+ + +
+
+
Cost & Duration
+
+
+
+
+
Total Cost
+

+ @repair.Cost.ToString("C") +

+
+
+
Duration
+

+ @if (repair.DurationMinutes > 0) + { + @repair.DurationDisplay + } + else + { + Not specified + } +

+
+
+
+
+ + + @if (!string.IsNullOrEmpty(repair.ContractorName) || !string.IsNullOrEmpty(repair.ContactPerson) || !string.IsNullOrEmpty(repair.ContractorPhone)) + { +
+
+
Contractor Information
+
+
+
+ @if (!string.IsNullOrEmpty(repair.ContractorName)) + { +
Company/Name:
+
@repair.ContractorName
+ } + + @if (!string.IsNullOrEmpty(repair.ContactPerson)) + { +
Contact Person:
+
@repair.ContactPerson
+ } + + @if (!string.IsNullOrEmpty(repair.ContractorPhone)) + { +
Phone:
+
+ @repair.ContractorPhone +
+ } +
+
+
+ } + + + @if (!string.IsNullOrEmpty(repair.PartsReplaced)) + { +
+
+
Parts & Materials
+
+
+

@repair.PartsReplaced

+
+
+ } + + + @if (repair.WarrantyApplies) + { +
+
+
+ Warranty Information +
+
+
+
+
Status:
+
+ @if (repair.IsUnderWarranty) + { + Active + } + else + { + Expired + } +
+ + @if (repair.WarrantyExpiresOn.HasValue) + { +
Expires On:
+
@repair.WarrantyExpiresOn.Value.ToLongDateString()
+ } +
+
+
+ } + + + @if (!string.IsNullOrEmpty(repair.Notes)) + { +
+
+
Additional Notes
+
+
+

@repair.Notes

+
+
+ } + + + @if (repair.MaintenanceRequestId.HasValue) + { +
+
+
+ Part of Maintenance Request +
+

+ This repair is associated with a maintenance request workflow. +

+
+
+ } +
+ +
+ +
+
+
Audit Trail
+
+
+
+
Created:
+
@repair.CreatedOn.ToShortDateString()
+ +
Created By:
+
@repair.CreatedBy
+ + @if (repair.LastModifiedOn.HasValue) + { +
Last Modified:
+
@repair.LastModifiedOn.Value.ToShortDateString()
+ +
Modified By:
+
@repair.LastModifiedBy
+ } +
+
+
+ + +
+
+
Actions
+
+
+
+ + @if (!repair.IsCompleted) + { + + } + +
+
+
+
+
+ } +
+ +@code { + [Parameter] public Guid Id { get; set; } + + private Repair? repair; + private bool isLoading = true; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + isLoading = true; + try + { + repair = await RepairService.GetByIdAsync(Id); + } + catch (Exception ex) + { + ToastService.ShowError($"Error loading repair: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task MarkComplete() + { + if (repair == null) return; + + try + { + await RepairService.CompleteRepairAsync(repair.Id); + ToastService.ShowSuccess("Repair marked as completed."); + await LoadData(); + } + catch (Exception ex) + { + ToastService.ShowError($"Error completing repair: {ex.Message}"); + } + } + + private async Task DeleteRepair() + { + try + { + await RepairService.DeleteAsync(Id); + ToastService.ShowSuccess("Repair deleted successfully."); + Navigation.NavigateTo("/propertymanagement/repairs"); + } + catch (Exception ex) + { + ToastService.ShowError($"Error deleting repair: {ex.Message}"); + } + } + + private void NavigateToEdit() => Navigation.NavigateTo($"/propertymanagement/repairs/edit/{Id}"); + private void Back() => Navigation.NavigateTo("/propertymanagement/repairs"); +} diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor index 85f4160..cc0b77e 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/IncomeStatementReport.razor @@ -6,7 +6,7 @@ @using Aquiis.Application.Services.PdfGenerators @using Microsoft.AspNetCore.Authorization @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] -@inject FinancialReportService FinancialReportService +@inject SimpleStartFinancialReportService FinancialReportService @inject PropertyService PropertyService @inject FinancialReportPdfGenerator PdfGenerator @inject AuthenticationStateProvider AuthenticationStateProvider diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor index 734a268..0889066 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor @@ -5,7 +5,7 @@ @using Aquiis.Application.Services.PdfGenerators @using Microsoft.AspNetCore.Authorization @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] -@inject FinancialReportService FinancialReportService +@inject SimpleStartFinancialReportService FinancialReportService @inject FinancialReportPdfGenerator PdfGenerator @inject AuthenticationStateProvider AuthenticationStateProvider diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor index 803ae8a..67bac97 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor @@ -5,7 +5,7 @@ @using Aquiis.Application.Services.PdfGenerators @using Microsoft.AspNetCore.Authorization @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] -@inject FinancialReportService FinancialReportService +@inject SimpleStartFinancialReportService FinancialReportService @inject FinancialReportPdfGenerator PdfGenerator @inject AuthenticationStateProvider AuthenticationStateProvider diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor index 27e7b80..265eb69 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/TaxReport.razor @@ -6,7 +6,7 @@ @using Aquiis.Application.Services.PdfGenerators @using Microsoft.AspNetCore.Authorization @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] -@inject FinancialReportService FinancialReportService +@inject SimpleStartFinancialReportService FinancialReportService @inject PropertyService PropertyService @inject FinancialReportPdfGenerator PdfGenerator @inject AuthenticationStateProvider AuthenticationStateProvider diff --git a/4-Aquiis.SimpleStart/Program.cs b/4-Aquiis.SimpleStart/Program.cs index 37ac256..dcf6599 100644 --- a/4-Aquiis.SimpleStart/Program.cs +++ b/4-Aquiis.SimpleStart/Program.cs @@ -161,7 +161,8 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); // Professional edition uses MaintenanceRequests +builder.Services.AddScoped(); // SimpleStart uses Repairs builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor b/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor index cff06f9..97e7b56 100644 --- a/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor +++ b/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor @@ -1,5 +1,6 @@ @page "/" @using Aquiis.Application.Services +@using Aquiis.UI.Shared.Components.Entities.Properties @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Forms @using Microsoft.EntityFrameworkCore @@ -13,7 +14,7 @@ @inject PropertyService PropertyService @inject TenantService TenantService @inject LeaseService LeaseService -@inject MaintenanceService MaintenanceService +@inject RepairService RepairService @inject InvoiceService InvoiceService @inject UserContextService UserContextService @inject ApplicationDbContext DbContext @@ -110,8 +111,9 @@
+ -
@@ -150,28 +152,40 @@
-
-
Pending Leases
+
+
Outstanding Invoices
+ View All
- @if (pendingLeases.Any()) + @if (recentInvoices.Any()) {
- @foreach (var lease in pendingLeases) + @foreach (var invoice in recentInvoices) {
- - @lease.Property.Address + + @invoice.InvoiceNumber
- @lease.CreatedOn.ToString("MMM dd, yyyy") + @invoice.InvoicedOn.ToString("MMM dd, yyyy")
-

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

+

@invoice.Lease?.Tenant?.FullName

- Start: @lease.StartDate.ToString("MMM dd, yyyy") - Pending +
+ Due: @invoice.DueOn.ToString("MMM dd, yyyy") + @if (invoice.IsOverdue) + { +
+ @invoice.DaysOverdue days overdue + } +
+
+ @invoice.Status +
+ @invoice.Amount.ToString("C") +
} @@ -179,53 +193,51 @@ } else { -

No pending leases found.

+

No outstanding invoices.

}
-
-
-
- -
-
Open Maintenance Requests
- View All +
Outstanding Repairs
+ View All
- @if (openMaintenanceRequests.Any()) + @if (recentRepairs.Any()) {
- @foreach (var request in openMaintenanceRequests) + @foreach (var repair in recentRepairs) {
- - @request.Title + + @repair.Description
- @request.Property?.Address - @request.RequestType + @repair.Property?.Address - @repair.RepairType
- @request.Priority - @if (request.IsOverdue) + In Progress + @if (repair.Cost > 0) {
- Overdue + @repair.Cost.ToString("C") }
- @request.RequestedOn.ToString("MMM dd, yyyy") - @request.Status + Started: @repair.CreatedOn.ToString("MMM dd, yyyy") + @if (!string.IsNullOrEmpty(repair.ContractorName)) + { + @repair.ContractorName + }
} @@ -233,60 +245,14 @@ } else { -

No open maintenance requests.

+

No outstanding repairs.

}
-
-
-
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.

- } -
-
+
-
} @@ -346,8 +312,7 @@ private int activeLeases = 0; private List availablePropertiesList = new(); - private List pendingLeases = new List(); - private List openMaintenanceRequests = new List(); + private List recentRepairs = new List(); private List recentInvoices = new List(); private List properties = new List(); @@ -404,26 +369,20 @@ .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) + // Load outstanding repairs (incomplete only - CompletedOn is null) + var allRepairs = await RepairService.GetAllAsync(); + recentRepairs = allRepairs + .Where(r => r.OrganizationId == organizationId && !r.CompletedOn.HasValue) + .OrderByDescending(r => r.CreatedOn) .Take(5) .ToList(); - // Load recent invoices + // Load outstanding invoices (unpaid and overdue only) var allInvoices = await InvoiceService.GetAllAsync(); recentInvoices = allInvoices .Where(i => i.Status != "Paid" && i.Status != "Cancelled") - .OrderByDescending(i => i.InvoicedOn) + .OrderByDescending(i => i.IsOverdue) + .ThenBy(i => i.DueOn) .Take(5) .ToList(); } diff --git a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor index a13f436..14c11e6 100644 --- a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor +++ b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor @@ -51,7 +51,7 @@
From 3f4e028bfc20af814d234943590ce4bbe79ad217 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Mon, 26 Jan 2026 21:19:09 -0600 Subject: [PATCH 5/8] simple-start-refactor --- .../Services/PropertyService.cs | 19 +- .../Components/Common/MetricsCard.razor | 32 + .../Entities/Documents/DocumentListView.razor | 5 +- .../Entities/Invoices/InvoiceList.razor | 74 +++ .../Entities/Leases/LeaseList.razor | 178 ++++++ .../Entities/Leases/LeaseListView.razor | 2 +- .../Entities/Leases/LeaseRenewalList.razor | 189 ++++++ .../MaintenanceRequestListView.razor | 5 +- .../Entities/Properties/PropertyDetails.razor | 5 +- .../Entities/Properties/PropertyList.razor | 69 ++ .../Properties/PropertyMetricsCard.razor | 219 +++---- ...RepairsListView.razor => RepairList.razor} | 30 +- .../Components/Layout/SharedMainLayout.razor | 11 +- .../Layout/SharedMainLayout.razor.css | 2 +- .../Invoices/ListForm.razor | 2 +- .../Payments/ListForm.razor | 2 +- .../Payments/ViewForm.razor | 4 +- 3-Aquiis.UI.Shared/wwwroot/js/theme.js | 112 +++- .../Administration/Users/Manage.razor | 2 +- .../Features/Calendar/Calendar.razor | 4 +- .../Properties/Pages/Index.razor | 2 +- .../Properties/Pages/View.razor | 4 +- .../PropertyManagement/Repairs/Edit.razor | 6 +- .../PropertyManagement/Repairs/Index.razor | 10 +- .../PropertyManagement/Repairs/View.razor | 14 +- .../Reports/Pages/IncomeStatementReport.razor | 4 +- .../Pages/PropertyPerformanceReport.razor | 12 +- .../Reports/Pages/RentRollReport.razor | 8 +- .../Reports/Pages/Reports.razor | 2 +- 4-Aquiis.SimpleStart/Shared/App.razor | 2 +- .../Components/LeaseRenewalWidget.razor | 9 +- .../Shared/Components/Pages/Home.razor | 303 +++------ .../Components/Pages/TestColorPalette.razor | 599 ++++++++++++++++++ .../Shared/Layout/MainLayout.razor | 6 +- .../Shared/Layout/MainLayout.razor.css | 11 +- .../Shared/Layout/NavMenu.razor | 173 ++++- .../Shared/Layout/NavMenu.razor.css | 141 ++++- .../Shared/Services/ThemeService.cs | 22 + .../wwwroot/android-chrome-192x192.png | Bin 0 -> 8474 bytes .../wwwroot/android-chrome-512x512.png | Bin 0 -> 22504 bytes 4-Aquiis.SimpleStart/wwwroot/app.css | 274 ++++++++ .../wwwroot/apple-touch-icon.png | Bin 0 -> 8119 bytes .../wwwroot/favicon-16x16.png | Bin 0 -> 659 bytes .../wwwroot/favicon-32x32.png | Bin 0 -> 1467 bytes 4-Aquiis.SimpleStart/wwwroot/favicon.ico | Bin 0 -> 15406 bytes 5-Aquiis.Professional/Shared/App.razor | 2 +- .../Shared/Layout/MainLayout.razor.css | 2 +- .../Shared/Layout/NavMenu.razor.css | 4 +- .../wwwroot/android-chrome-192x192.png | Bin 0 -> 8474 bytes .../wwwroot/android-chrome-512x512.png | Bin 0 -> 22504 bytes 5-Aquiis.Professional/wwwroot/app.css | 103 ++- .../wwwroot/apple-touch-icon.png | Bin 0 -> 8119 bytes .../wwwroot/favicon-16x16.png | Bin 0 -> 659 bytes .../wwwroot/favicon-32x32.png | Bin 0 -> 1467 bytes 5-Aquiis.Professional/wwwroot/favicon.ico | Bin 0 -> 15406 bytes 55 files changed, 2246 insertions(+), 433 deletions(-) create mode 100644 3-Aquiis.UI.Shared/Components/Common/MetricsCard.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Invoices/InvoiceList.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseList.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseRenewalList.razor create mode 100644 3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyList.razor rename 3-Aquiis.UI.Shared/Components/Entities/Repairs/{RepairsListView.razor => RepairList.razor} (90%) create mode 100644 4-Aquiis.SimpleStart/Shared/Components/Pages/TestColorPalette.razor create mode 100644 4-Aquiis.SimpleStart/wwwroot/android-chrome-192x192.png create mode 100644 4-Aquiis.SimpleStart/wwwroot/android-chrome-512x512.png create mode 100644 4-Aquiis.SimpleStart/wwwroot/apple-touch-icon.png create mode 100644 4-Aquiis.SimpleStart/wwwroot/favicon-16x16.png create mode 100644 4-Aquiis.SimpleStart/wwwroot/favicon-32x32.png create mode 100644 4-Aquiis.SimpleStart/wwwroot/favicon.ico create mode 100644 5-Aquiis.Professional/wwwroot/android-chrome-192x192.png create mode 100644 5-Aquiis.Professional/wwwroot/android-chrome-512x512.png create mode 100644 5-Aquiis.Professional/wwwroot/apple-touch-icon.png create mode 100644 5-Aquiis.Professional/wwwroot/favicon-16x16.png create mode 100644 5-Aquiis.Professional/wwwroot/favicon-32x32.png create mode 100644 5-Aquiis.Professional/wwwroot/favicon.ico diff --git a/2-Aquiis.Application/Services/PropertyService.cs b/2-Aquiis.Application/Services/PropertyService.cs index ef80bf9..aa29d2f 100644 --- a/2-Aquiis.Application/Services/PropertyService.cs +++ b/2-Aquiis.Application/Services/PropertyService.cs @@ -238,7 +238,7 @@ public async Task CalculateOccupancyRateAsync() var organizationId = await _userContext.GetActiveOrganizationIdAsync(); var totalProperties = await _context.Properties - .CountAsync(p => !p.IsDeleted && p.IsAvailable && p.OrganizationId == organizationId); + .CountAsync(p => !p.IsDeleted && p.OrganizationId == organizationId); if (totalProperties == 0) { @@ -247,7 +247,6 @@ public async Task CalculateOccupancyRateAsync() var occupiedProperties = await _context.Properties .CountAsync(p => !p.IsDeleted && - p.IsAvailable && p.OrganizationId == organizationId && _context.Leases.Any(l => l.PropertyId == p.Id && @@ -354,7 +353,21 @@ public async Task CalculatePortfolioOccupancyRateAsync(DateTime? period } // Calculate total occupied days and total available days across all properties - var startDate = periodStart ?? new DateTime(DateTime.Today.Year, 4, 1); + // Default to current fiscal year (April 1 - March 31) + // If today is Jan-Mar, use April 1 of previous year + // If today is Apr-Dec, use April 1 of current year + DateTime startDate; + if (periodStart.HasValue) + { + startDate = periodStart.Value; + } + else + { + var today = DateTime.Today; + startDate = today.Month < 4 + ? new DateTime(today.Year - 1, 4, 1) + : new DateTime(today.Year, 4, 1); + } var endDate = startDate.AddYears(1).AddDays(-1); var totalDays = (endDate - startDate).Days + 1; diff --git a/3-Aquiis.UI.Shared/Components/Common/MetricsCard.razor b/3-Aquiis.UI.Shared/Components/Common/MetricsCard.razor new file mode 100644 index 0000000..301a165 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Common/MetricsCard.razor @@ -0,0 +1,32 @@ +
+
+
+
+
+

@Count

+

@Title

+
+
+ +
+
+
+
+
+@code { + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string Title { get; set; } = string.Empty; + + [Parameter] + public string Count { get; set; } = string.Empty; + + [Parameter] + public string? IconCssClass { get; set; } + + [Parameter] + public string? ColorClass { get; set; } + +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/Documents/DocumentListView.razor b/3-Aquiis.UI.Shared/Components/Entities/Documents/DocumentListView.razor index eb5abdf..382dde5 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Documents/DocumentListView.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Documents/DocumentListView.razor @@ -1,7 +1,7 @@ @using Aquiis.Core.Entities
-
+
Documents
@@ -68,6 +68,9 @@ [Parameter] public bool ShowUploadButton { get; set; } = true; + [Parameter] + public string HeaderCssClass { get; set; } = ""; + [Parameter] public bool ShowGroupByType { get; set; } = false; diff --git a/3-Aquiis.UI.Shared/Components/Entities/Invoices/InvoiceList.razor b/3-Aquiis.UI.Shared/Components/Entities/Invoices/InvoiceList.razor new file mode 100644 index 0000000..c72d669 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Invoices/InvoiceList.razor @@ -0,0 +1,74 @@ +
+
+
@Title
+ +
+
+ @if (Invoices.Any()) + { +
+ @foreach (var invoice in Invoices) + { +
+
+
+ + @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 outstanding invoices.

+ } +
+
+@code { + [Parameter, EditorRequired] + + public List Invoices { get; set; } = new(); + + [Parameter] + public string Title { get; set; } = "Invoices"; + + [Parameter] + public string HeaderCssClass { get; set; } = ""; + + [Parameter] + public EventCallback OnViewAll { get; set; } + + [Parameter] + public EventCallback OnViewItem { get; set; } + + private string GetInvoiceStatusBadgeClass(string status) + { + return status switch + { + ApplicationConstants.InvoiceStatuses.Paid => "bg-success", + ApplicationConstants.InvoiceStatuses.PaidPartial => "bg-warning text-dark", + ApplicationConstants.InvoiceStatuses.Overdue => "bg-danger", + _ => "bg-secondary" + }; + } +} \ No newline at end of file diff --git a/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseList.razor b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseList.razor new file mode 100644 index 0000000..8843e91 --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseList.razor @@ -0,0 +1,178 @@ +
+
+
+ @Title +
+ + 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 { + [Parameter] + public string Title { get; set; } = "Leases"; + + [Parameter] + public string HeaderCssClass { get; set; } = ""; + + [Parameter] + public List Leases { get; set; } = new(); + + private List expiringLeases = new(); + private List leases30Days = new(); + private List leases60Days = new(); + private List leases90Days = new(); + private bool isLoading = true; + private int selectedFilter = 30; + + + private async Task LoadExpiringLeases() + { + try + { + isLoading = true; + var today = DateTime.Today; + + expiringLeases = Leases + .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/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseListView.razor b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseListView.razor index bad8f24..9000c82 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseListView.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseListView.razor @@ -68,7 +68,7 @@ {
- + diff --git a/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseRenewalList.razor b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseRenewalList.razor new file mode 100644 index 0000000..26b938b --- /dev/null +++ b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseRenewalList.razor @@ -0,0 +1,189 @@ +@namespace Aquiis.UI.Shared.Components.Entities.Leases +
+
+
+ @Title +
+ +
+
+ @if (isLoading) + { +
+
+ Loading... +
+
+ } + else if (Leases == null || !Leases.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 { + + [Parameter] + public string Title { get; set; } = "Upcoming Lease Renewals"; + + [Parameter] + public string HeaderCssClass { get; set; } = ""; + + [Parameter] + public List Leases { get; set; } = new(); + + private List leases30Days = new(); + private List leases60Days = new(); + private List leases90Days = new(); + private bool isLoading = true; + private int selectedFilter = 60; + + [Parameter] + public EventCallback OnViewAll { get; set; } + + [Parameter] + public EventCallback OnViewLease { get; set; } + + [Parameter] + public EventCallback OnFilterChanged { get; set; } + + protected override async Task OnInitializedAsync() + { + await LoadLeases(); + } + + private async Task LoadLeases() + { + try + { + isLoading = true; + var today = DateTime.Today; + + leases30Days = Leases + .Where(l => l.EndDate <= today.AddDays(30)) + .ToList(); + + leases60Days = Leases + .Where(l => l.EndDate <= today.AddDays(60)) + .ToList(); + + leases90Days = Leases + .Where(l => l.EndDate <= today.AddDays(90)) + .ToList(); + } + catch (Exception ex) + { + // Log error + Console.WriteLine($"Error loading expiring leases: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private async Task FilterLeases(int days) + { + selectedFilter = days; + await OnFilterChanged.InvokeAsync(days); + StateHasChanged(); + } + + private List GetFilteredLeases() + { + return selectedFilter switch + { + 30 => leases30Days, + 60 => leases60Days, + 90 => leases90Days, + _ => Leases + }; + } + + private string GetStatusBadgeClass(string status) + { + return status switch + { + "Pending" => "secondary", + "Offered" => "info", + "Accepted" => "success", + "Declined" => "danger", + "Expired" => "dark", + _ => "secondary" + }; + } +} diff --git a/3-Aquiis.UI.Shared/Components/Entities/MaintenanceRequests/MaintenanceRequestListView.razor b/3-Aquiis.UI.Shared/Components/Entities/MaintenanceRequests/MaintenanceRequestListView.razor index 2b3b1e9..decfb2c 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/MaintenanceRequests/MaintenanceRequestListView.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/MaintenanceRequests/MaintenanceRequestListView.razor @@ -4,7 +4,7 @@ @using Microsoft.AspNetCore.Http
-
+
Maintenance Requests
- } +
+ + + @if (ShowAddButton && OnAddRepair.HasDelegate) + { + + } +
@if (Repairs == null) @@ -129,6 +131,12 @@ [Parameter] public List? Repairs { get; set; } + [Parameter] + public string Title { get; set; } = "Repairs"; + + [Parameter] + public string HeaderCssClass { get; set; } = ""; + [Parameter] public bool ShowAddButton { get; set; } = true; diff --git a/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor b/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor index 039975d..411ab22 100644 --- a/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor +++ b/3-Aquiis.UI.Shared/Components/Layout/SharedMainLayout.razor @@ -1,14 +1,15 @@ @inherits LayoutComponentBase @namespace Aquiis.UI.Shared.Components.Layout @using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.EntityFrameworkCore.Metadata.Internal -
-
Tenant Start Date
- + diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor index 678e376..7cb8196 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ListForm.razor @@ -132,7 +132,7 @@ else {
Invoice # Tenant
- + diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor index c630f64..d3fae63 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Payments/ViewForm.razor @@ -161,7 +161,7 @@ else

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

@@ -169,7 +169,7 @@ else

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

diff --git a/3-Aquiis.UI.Shared/wwwroot/js/theme.js b/3-Aquiis.UI.Shared/wwwroot/js/theme.js index 168c1bf..c82e64c 100644 --- a/3-Aquiis.UI.Shared/wwwroot/js/theme.js +++ b/3-Aquiis.UI.Shared/wwwroot/js/theme.js @@ -25,27 +25,92 @@ window.themeManager = { initTheme: function () { const savedTheme = this.getTheme(); this.setTheme(savedTheme); + const savedBrandTheme = this.getBrandTheme(); + this.setBrandTheme(savedBrandTheme); return savedTheme; }, + + setBrandTheme: function (brandTheme) { + console.log("setBrandTheme called with:", brandTheme); + document.documentElement.setAttribute("data-brand-theme", brandTheme); + localStorage.setItem("brandTheme", brandTheme); + console.log( + "Brand theme set. DOM attribute:", + document.documentElement.getAttribute("data-brand-theme"), + ); + console.log("localStorage value:", localStorage.getItem("brandTheme")); + + // Force browser to recalculate CSS custom properties + document.documentElement.style.display = "none"; + void document.documentElement.offsetHeight; // Trigger reflow + document.documentElement.style.display = ""; + }, + + getBrandTheme: function () { + const brandTheme = localStorage.getItem("brandTheme") || "bootstrap"; + return brandTheme; + }, }; // Initialize theme IMMEDIATELY (before DOMContentLoaded) to prevent flash if (typeof localStorage !== "undefined") { const savedTheme = localStorage.getItem("theme") || "light"; - console.log("Initial theme load:", savedTheme); + const savedBrandTheme = localStorage.getItem("brandTheme") || "bootstrap"; + console.log("Initial theme load:", savedTheme, "Brand:", savedBrandTheme); document.documentElement.setAttribute("data-bs-theme", savedTheme); + document.documentElement.setAttribute("data-brand-theme", savedBrandTheme); + + // Force multiple reflows to ensure CSS is applied + // Using requestAnimationFrame to ensure it happens after browser paint + document.documentElement.style.display = "none"; + void document.documentElement.offsetHeight; // Trigger reflow + document.documentElement.style.display = ""; + + // Double-check on next frame + requestAnimationFrame(() => { + if ( + document.documentElement.getAttribute("data-brand-theme") !== + savedBrandTheme + ) { + document.documentElement.setAttribute( + "data-brand-theme", + savedBrandTheme, + ); + } + console.log( + "Initial theme applied with reflow, verified:", + document.documentElement.getAttribute("data-brand-theme"), + ); + }); // 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); + let currentTheme = document.documentElement.getAttribute("data-bs-theme"); + let currentBrandTheme = + document.documentElement.getAttribute("data-brand-theme"); + + // If attributes are missing, restore from localStorage + if (!currentTheme) { + currentTheme = localStorage.getItem("theme") || "light"; + document.documentElement.setAttribute("data-bs-theme", currentTheme); + console.log("Restored theme attribute:", currentTheme); + } + + if (!currentBrandTheme) { + currentBrandTheme = localStorage.getItem("brandTheme") || "bootstrap"; + document.documentElement.setAttribute( + "data-brand-theme", + currentBrandTheme, + ); + console.log("Restored brand theme attribute:", currentBrandTheme); } + + // 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, currentBrandTheme); }); // Start observing after a short delay to let Blazor initialize @@ -55,4 +120,35 @@ if (typeof localStorage !== "undefined") { subtree: true, }); }, 1000); + + // Also ensure theme is applied after DOM is fully loaded + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + const currentTheme = + document.documentElement.getAttribute("data-bs-theme") || + localStorage.getItem("theme") || + "light"; + const currentBrandTheme = + document.documentElement.getAttribute("data-brand-theme") || + localStorage.getItem("brandTheme") || + "bootstrap"; + + document.documentElement.setAttribute("data-bs-theme", currentTheme); + document.documentElement.setAttribute( + "data-brand-theme", + currentBrandTheme, + ); + + // Force reflow + document.documentElement.style.display = "none"; + void document.documentElement.offsetHeight; + document.documentElement.style.display = ""; + + console.log( + "DOMContentLoaded - Theme re-applied:", + currentTheme, + currentBrandTheme, + ); + }); + } } diff --git a/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor b/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor index f76bc80..cf18dd5 100644 --- a/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor +++ b/4-Aquiis.SimpleStart/Features/Administration/Users/Manage.razor @@ -171,7 +171,7 @@ else {
Payment Date Amount
- + diff --git a/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor b/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor index 154c546..c2e9127 100644 --- a/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor +++ b/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor @@ -131,7 +131,7 @@ @if (viewMode == "day") {
-
+
@currentDate.ToString("dddd, MMMM dd, yyyy")
@@ -1144,7 +1144,7 @@ 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 + 1, "class", isCurrentMonth ? "align-top" : "align-top bg-body-tertiary text-muted"); builder.AddAttribute(cellIndex + 2, "style", "min-height: 100px; width: 14.28%;"); builder.OpenElement(cellIndex + 10, "div"); diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor index 47a0f40..5160bf0 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/Index.razor @@ -33,7 +33,7 @@ AvailableCount="@availableProperties" OccupiedCount="@occupiedProperties" TotalMonthlyRent="@totalMonthlyRent" - ShowPending="false" /> + ShowTotalProperties="false" ShowAvailableProperties="true" ShowOccupiedProperties="true" ShowTotalMonthlyRent="true" />
-
+ Documents="@property.Documents.ToList()" OnViewDocument="ViewDocument" />
diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Edit.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Edit.razor index ea8831a..5c80608 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Edit.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Edit.razor @@ -1,4 +1,4 @@ -@page "/propertymanagement/repairs/edit/{id:guid}" +@page "/propertymanagement/repairs/{id:guid}/edit" @using Aquiis.Core.Entities @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @inject RepairService RepairService @@ -40,7 +40,7 @@
-
+
Repair Information
@@ -193,7 +193,7 @@
-
+
Audit Information
diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Index.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Index.razor index 802645f..28d3426 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Index.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Index.razor @@ -39,7 +39,7 @@ else {
-
+
User Email
- + @@ -187,7 +187,7 @@ -
Property Description
- + @@ -108,7 +108,7 @@ } - + @@ -140,7 +140,7 @@
-
+
Top Performing Properties (by Net Income)
@@ -162,7 +162,7 @@
-
+
Highest Occupancy
diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor index 67bac97..07f6d24 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/RentRollReport.razor @@ -48,16 +48,16 @@ else if (rentRollItems.Any()) {
-
+
Rent Roll as of @asOfDate.ToString("MMM dd, yyyy")
-
Property Address
TOTALS @performanceItems?.Sum(p => p.TotalIncome).ToString("C")
- + @@ -111,7 +111,7 @@ } - + diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor index 67c6d56..3821e7c 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/Reports.razor @@ -187,7 +187,7 @@ else
-
+
Report Features
diff --git a/4-Aquiis.SimpleStart/Shared/App.razor b/4-Aquiis.SimpleStart/Shared/App.razor index dee388b..b26a79e 100644 --- a/4-Aquiis.SimpleStart/Shared/App.razor +++ b/4-Aquiis.SimpleStart/Shared/App.razor @@ -11,7 +11,7 @@ - + diff --git a/4-Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor b/4-Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor index ce2c286..bc0b689 100644 --- a/4-Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor +++ b/4-Aquiis.SimpleStart/Shared/Components/LeaseRenewalWidget.razor @@ -39,13 +39,13 @@
@@ -106,7 +106,7 @@ private List leases60Days = new(); private List leases90Days = new(); private bool isLoading = true; - private int selectedFilter = 30; + private int selectedFilter = 60; protected override async Task OnInitializedAsync() { @@ -118,7 +118,7 @@ try { isLoading = true; - var allLeases = await LeaseService.GetAllAsync(); + var allLeases = await LeaseService.GetLeasesWithRelationsAsync(); var today = DateTime.Today; expiringLeases = allLeases @@ -152,6 +152,7 @@ private void FilterLeases(int days) { selectedFilter = days; + StateHasChanged(); } private List GetFilteredLeases() diff --git a/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor b/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor index 97e7b56..2a2f938 100644 --- a/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor +++ b/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor @@ -1,6 +1,9 @@ @page "/" @using Aquiis.Application.Services +@using Aquiis.UI.Shared.Components.Entities.Invoices @using Aquiis.UI.Shared.Components.Entities.Properties +@using Aquiis.UI.Shared.Components.Entities.Repairs +@using Aquiis.UI.Shared.Components.Entities.Leases @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Forms @using Microsoft.EntityFrameworkCore @@ -49,210 +52,30 @@ } 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.

- } -
-
-
-
-
-
-
Outstanding 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 outstanding invoices.

- } -
-
-
- -
-
-
-
Outstanding Repairs
- View All -
-
- @if (recentRepairs.Any()) - { -
- @foreach (var repair in recentRepairs) - { -
-
-
-
- - @repair.Description - -
- - @repair.Property?.Address - @repair.RepairType - -
-
- In Progress - @if (repair.Cost > 0) - { -
- @repair.Cost.ToString("C") - } -
-
-
- Started: @repair.CreatedOn.ToString("MMM dd, yyyy") - @if (!string.IsNullOrEmpty(repair.ContractorName)) - { - @repair.ContractorName - } -
-
- } -
- } - else - { -

No outstanding repairs.

- } -
-
+
+
+ +
-
- +
+ +
+
} @@ -311,8 +134,16 @@ private int totalTenants = 0; private int activeLeases = 0; + private decimal occupancyRate = 0m; + + private decimal totalMonthlyIncome = 0m; + + private decimal totalRevenueOpportunity = 0m; + private List availablePropertiesList = new(); private List recentRepairs = new List(); + private List outstandingInvoices = new List(); + private List recentInvoices = new List(); private List properties = new List(); @@ -385,6 +216,22 @@ .ThenBy(i => i.DueOn) .Take(5) .ToList(); + + outstandingInvoices = allInvoices + .Where(i => i.Status == ApplicationConstants.InvoiceStatuses.Overdue + || i.Status == ApplicationConstants.InvoiceStatuses.PaidPartial + || i.Status == ApplicationConstants.InvoiceStatuses.Pending) + .OrderBy(i => i.DueOn) + .ToList(); + + occupancyRate = await PropertyService.CalculatePortfolioOccupancyRateAsync(); + Console.WriteLine($"Calculated occupancy rate: {occupancyRate}%"); + totalMonthlyIncome = leases + .Where(l => !l.IsDeleted && l.Status == ApplicationConstants.LeaseStatuses.Active) + .Sum(l => l.MonthlyRent); + totalRevenueOpportunity = properties + .Where(p => !p.IsDeleted) + .Sum(p => p.MonthlyRent); } catch (InvalidOperationException) { @@ -393,6 +240,56 @@ } } + private void ViewAllRepairs() + { + NavigationManager.NavigateTo("/propertymanagement/repairs"); + } + + private void ViewRepair(Guid repairId) + { + NavigationManager.NavigateTo($"/propertymanagement/repairs/{repairId}"); + } + + private void AddRepair() + { + NavigationManager.NavigateTo("/propertymanagement/repairs/create"); + } + + private void ViewAllProperties() + { + NavigationManager.NavigateTo("/propertymanagement/properties"); + } + + private void ViewProperty(Guid propertyId) + { + NavigationManager.NavigateTo($"/propertymanagement/properties/{propertyId}"); + } + + private void ViewAllLeases() + { + NavigationManager.NavigateTo("/propertymanagement/leases"); + } + + private void ViewLease(Guid leaseId) + { + NavigationManager.NavigateTo($"/propertymanagement/leases/{leaseId}"); + } + + private void ViewAllInvoices() + { + NavigationManager.NavigateTo("/propertymanagement/invoices"); + } + + private void ViewInvoice(Guid invoiceId) + { + NavigationManager.NavigateTo($"/propertymanagement/invoices/{invoiceId}"); + } + + private void NavigateToCalendar() + { + NavigationManager.NavigateTo("/calendar"); + } + private string GetInvoiceStatusBadgeClass(string status) { return status switch @@ -431,8 +328,4 @@ }; } - private void NavigateToCalendar() - { - NavigationManager.NavigateTo("/calendar"); - } } diff --git a/4-Aquiis.SimpleStart/Shared/Components/Pages/TestColorPalette.razor b/4-Aquiis.SimpleStart/Shared/Components/Pages/TestColorPalette.razor new file mode 100644 index 0000000..7b74de1 --- /dev/null +++ b/4-Aquiis.SimpleStart/Shared/Components/Pages/TestColorPalette.razor @@ -0,0 +1,599 @@ +@page "/test/color-palette" +@rendermode InteractiveServer +Proposed Color Palette Test + + + + +
+ + +
+ +
+ + + +
+
+
+

+ Proposed Color Palette +

+

Visual reference for the new Aquiis color scheme

+
+
+ + +
+
+

Color Reference

+
+
+
+
+ Primary +
+
+ #2C5F8D +
+
+
+
+
+
+ Primary Dark +
+
+ #1E3A5F +
+
+
+
+
+
+ Secondary +
+
+ #F59E42 +
+
+
+
+
+
+ Success +
+
+ #388E3C +
+
+
+
+
+
+ Info +
+
+ #5A8FBF +
+
+
+
+
+
+ Danger +
+
+ #dc3545 +
+
+
+
+ + +
+
+

Neutral Colors

+
+
+
+
+ Neutral Light +
+
+ #F5F7FA +
+
+
+
+
+
+ Gray +
+
+ #E0E7EE +
+
+
+
+
+
+ Gray Dark +
+
+ #8A9BA8 +
+
+
+
+
+
+ Neutral Dark +
+
+ #333333 +
+
+
+
+
+
+ Light Charcoal +
+
+ #282a2e +
+
+
+
+
+
+ Charcoal +
+
+ #1a1f2e +
+
+
+
+ + +
+
+

Dashboard Metrics

+
+
+
+
+
+ +
+

2

+

Total Properties

+
+
+
+
+
+
+
+ +
+

1

+

Available Properties

+
+
+
+
+
+
+
+ +
+

45.8%

+

Occupancy Rate

+
+
+
+
+
+
+
+ +
+

$1,800.00

+

Total Rent/Month

+
+
+
+
+ + +
+
+

Buttons

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

Status Badges

+
+
+
+ + Available + + + Occupied + + + Pending + + + Overdue + + + Active + + + Warning + + + Cancelled + +
+
+
+ + +
+
+

Cards

+
+
+
+
+
+
354 Maple Avenue
+ Available +
+
+
+

Address: Los Angeles, CA 90210

+

Type: House

+

Beds: 4 | Baths: 2.5

+

$2,700.00 /month

+
+ +
+
+ +
+
+
+
+
+
INV-202601-00001
+

Trinity Anderson

+
+ Overdue +
+

Due: Feb 20, 2026

+

Amount: $1,800.00

+

Status: Paid Partial

+
+ +
+
+ +
+
+
+
Card with Header
+
+
+

This card demonstrates the primary color header style with white text.

+

Perfect for modal headers and important sections.

+
+ +
+
+
+ + +
+
+

Data Table

+
+
+
Property Address
TOTALS @rentRollItems.Sum(r => r.MonthlyRent).ToString("C")
+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyAddressStatusRentActions
354 Maple AvenueLos Angeles, CA 90210 + Available + $2,700.00 + + +
333 Forest CreekLos Angeles, CA 90210 + Occupied + $1,800.00 + + +
+
+
+ + +
+
+

Alerts

+
+
+
+
+ +
+ Success! Property has been added successfully. +
+
+
+
+
+
+
+ +
+ Info: This property has an active lease through February 2026. +
+
+
+
+
+
+
+ +
+ Warning: Lease expires in 30 days. +
+
+
+
+
+
+
+ +
+ Error! Unable to save property. Please check all required fields. +
+
+
+
+
+ + +
+
+

Form Example

+
+
+
+
+
Add New Property
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+
+
+
+ +@code { + private string selectedPalette = "standard"; + + private string GetPaletteClass() + { + return $"palette-{selectedPalette}"; + } +} diff --git a/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor b/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor index 7b3ff61..a1ce3cc 100644 --- a/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor +++ b/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor @@ -7,7 +7,7 @@ - + @@ -15,7 +15,7 @@ - + @@ -36,10 +36,12 @@ protected override void OnInitialized() { ThemeService.OnThemeChanged += StateHasChanged; + ThemeService.OnBrandThemeChanged += StateHasChanged; } public void Dispose() { ThemeService.OnThemeChanged -= StateHasChanged; + ThemeService.OnBrandThemeChanged -= StateHasChanged; } } diff --git a/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor.css b/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor.css index 393ac82..d22eff3 100644 --- a/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor.css +++ b/4-Aquiis.SimpleStart/Shared/Layout/MainLayout.razor.css @@ -8,13 +8,10 @@ main { flex: 1; } -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - .top-row { - background-color: var(--bs-body-bg) !important; - border-bottom: 1px solid var(--bs-border-color) !important; + /* background-color: #0f172a !important; */ + color: white !important; + border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important; justify-content: flex-end; height: 3.5rem; display: flex; @@ -26,7 +23,7 @@ main { white-space: nowrap; margin-left: 1.5rem; text-decoration: none; - color: var(--bs-body-color) !important; + color: white !important; } .top-row ::deep a:hover, diff --git a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor index 14c11e6..f3c6eed 100644 --- a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor +++ b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor @@ -9,9 +9,11 @@ @@ -97,6 +99,31 @@
+ + @@ -122,14 +174,24 @@ @code { private string? currentUrl; private string currentTheme = "light"; + private bool showBrandThemeDropdown = false; private string OrganizationId = string.Empty; + // Available brand themes - add new themes here + private readonly List AvailableBrandThemes = new() + { + "bootstrap", + "obsidian", + "teal" + }; + protected override async Task OnInitializedAsync() { currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); NavigationManager.LocationChanged += OnLocationChanged; ThemeService.OnThemeChanged += OnThemeChanged; + ThemeService.OnBrandThemeChanged += OnBrandThemeChanged; currentTheme = ThemeService.CurrentTheme; var orgId = await UserContext.GetActiveOrganizationIdAsync(); @@ -143,24 +205,45 @@ { try { - // Load saved theme from localStorage and sync with service - var savedTheme = await JSRuntime.InvokeAsync("eval", "localStorage.getItem('theme') || 'light'"); + // Read current DOM attributes that were set by theme.js initialization + var domTheme = await JSRuntime.InvokeAsync("eval", + "document.documentElement.getAttribute('data-bs-theme') || localStorage.getItem('theme') || 'light'"); + var domBrandTheme = await JSRuntime.InvokeAsync("eval", + "document.documentElement.getAttribute('data-brand-theme') || localStorage.getItem('brandTheme') || 'bootstrap'"); + + Console.WriteLine($"NavMenu OnAfterRenderAsync - DOM theme: {domTheme}, brand: {domBrandTheme}"); + Console.WriteLine($"NavMenu OnAfterRenderAsync - Service BEFORE sync: {ThemeService.CurrentTheme}, brand: {ThemeService.CurrentBrandTheme}"); + + // Sync the service with the DOM state (which was set by theme.js) + if (ThemeService.CurrentTheme != domTheme) + { + Console.WriteLine($"Syncing theme service: {ThemeService.CurrentTheme} -> {domTheme}"); + ThemeService.SetTheme(domTheme); + } - // Only update if different from current - if (ThemeService.CurrentTheme != savedTheme) + if (ThemeService.CurrentBrandTheme != domBrandTheme) { - ThemeService.SetTheme(savedTheme); + Console.WriteLine($"Syncing brand theme service: {ThemeService.CurrentBrandTheme} -> {domBrandTheme}"); + ThemeService.SetBrandTheme(domBrandTheme); } - // Always ensure the DOM has the correct theme attribute - await JSRuntime.InvokeVoidAsync("themeManager.setTheme", savedTheme); + Console.WriteLine($"NavMenu OnAfterRenderAsync - Service AFTER sync: {ThemeService.CurrentTheme}, brand: {ThemeService.CurrentBrandTheme}"); + + // Force UI refresh to reflect synced theme StateHasChanged(); + + // Also ensure DOM attributes are still set (in case Blazor cleared them) + await JSRuntime.InvokeVoidAsync("themeManager.setTheme", ThemeService.CurrentTheme); + await JSRuntime.InvokeVoidAsync("themeManager.setBrandTheme", ThemeService.CurrentBrandTheme); } - catch + catch (Exception ex) { + Console.WriteLine($"Error loading themes: {ex.Message}"); // Fallback if localStorage not available ThemeService.SetTheme("light"); + ThemeService.SetBrandTheme("bootstrap"); await JSRuntime.InvokeVoidAsync("themeManager.setTheme", "light"); + await JSRuntime.InvokeVoidAsync("themeManager.setBrandTheme", "bootstrap"); } } } @@ -173,7 +256,10 @@ currentTheme = ThemeService.CurrentTheme; // Save to localStorage and update DOM immediately await JSRuntime.InvokeVoidAsync("themeManager.setTheme", ThemeService.CurrentTheme); - StateHasChanged(); + + // Force a complete page refresh to apply theme changes immediately + NavigationManager.Refresh(forceReload: false); + Console.WriteLine($"Theme toggled to: {ThemeService.CurrentTheme}"); } catch (Exception ex) @@ -182,6 +268,51 @@ } } + private async Task SelectBrandTheme(string brandTheme) + { + try + { + Console.WriteLine($"SelectBrandTheme called with: {brandTheme}"); + + // Close dropdown first + showBrandThemeDropdown = false; + StateHasChanged(); + + // Update service and JavaScript + ThemeService.SetBrandTheme(brandTheme); + await JSRuntime.InvokeVoidAsync("themeManager.setBrandTheme", brandTheme); + + Console.WriteLine($"Brand theme changed to: {ThemeService.CurrentBrandTheme}"); + + // Force UI refresh + NavigationManager.Refresh(forceReload: false); + } + catch (Exception ex) + { + Console.WriteLine($"Error changing brand theme: {ex.Message}"); + showBrandThemeDropdown = false; + StateHasChanged(); + } + } + + private void ToggleBrandThemeDropdown() + { + showBrandThemeDropdown = !showBrandThemeDropdown; + Console.WriteLine($"Dropdown toggled: {showBrandThemeDropdown}"); + StateHasChanged(); + } + + private string FormatBrandThemeName(string theme) + { + return theme switch + { + "bootstrap" => "Bootstrap", + "obsidian" => "Obsidian", + "teal" => "Teal", + _ => theme.Substring(0, 1).ToUpper() + theme.Substring(1) + }; + } + private string GetThemeIcon() { return currentTheme == "light" ? "bi-moon-fill" : "bi-sun-fill"; @@ -202,14 +333,29 @@ }); } + private void OnBrandThemeChanged() + { + InvokeAsync(async () => + { + try + { + // Re-apply brand theme when service notifies of change + await JSRuntime.InvokeVoidAsync("themeManager.setBrandTheme", ThemeService.CurrentBrandTheme); + } + catch { } + StateHasChanged(); + }); + } + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) { currentUrl = NavigationManager.ToBaseRelativePath(e.Location); - // Re-apply current theme after navigation + // Re-apply current theme and brand theme after navigation try { await JSRuntime.InvokeVoidAsync("themeManager.setTheme", ThemeService.CurrentTheme); + await JSRuntime.InvokeVoidAsync("themeManager.setBrandTheme", ThemeService.CurrentBrandTheme); } catch { } @@ -220,6 +366,7 @@ { NavigationManager.LocationChanged -= OnLocationChanged; ThemeService.OnThemeChanged -= OnThemeChanged; + ThemeService.OnBrandThemeChanged -= OnBrandThemeChanged; } } diff --git a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor.css b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor.css index 01486ae..202219c 100644 --- a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor.css +++ b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor.css @@ -14,16 +14,43 @@ } .navbar-toggler:checked { - background-color: rgba(255, 255, 255, 0.5); + background-color: rgba(255, 255, 255, 0.1); +} + +[data-bs-theme="light"] .navbar-toggler { + border: 1px solid rgba(0, 0, 0, 0.5) !important; + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 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(0, 0, 0, 0.5) !important; +} + +[data-bs-theme="light"] .navbar-toggler:checked { + background-color: rgba(0, 0, 0, 0.25) !important; +} + +[data-bs-theme="light"] .top-row { + min-height: 3.5rem; + border-bottom: 1px solid var(--bs-border-color); + /* background-color: #0f172a !important; */ } -.top-row { +[data-bs-theme="dark"] .top-row { min-height: 3.5rem; - background-color: rgba(0, 0, 0, 0.4); + border-bottom: 1px solid var(--bs-border-color); + /* background-color: #0a0f1a !important; */ } .navbar-brand { font-size: 1.1rem; + color: var(--bs-body-color) !important; +} + +.top-row .btn-link { + color: var(--bs-body-color) !important; + text-decoration: none; +} + +.top-row .btn-link:hover { + color: var(--bs-link-hover-color) !important; } .bi { @@ -36,6 +63,19 @@ background-size: cover; } +/* Light mode - invert white icons to dark */ +[data-bs-theme="light"] .bi-house-door-fill-nav-menu, +[data-bs-theme="light"] .bi-plus-square-fill-nav-menu, +[data-bs-theme="light"] .bi-list-nested-nav-menu, +[data-bs-theme="light"] .bi-lock-nav-menu, +[data-bs-theme="light"] .bi-person-nav-menu, +[data-bs-theme="light"] .bi-person-badge-nav-menu, +[data-bs-theme="light"] .bi-person-fill-nav-menu, +[data-bs-theme="light"] .bi-arrow-bar-left-nav-menu, +[data-bs-theme="light"] .bi-palette-fill-nav-menu { + filter: invert(1); +} + .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"); } @@ -68,6 +108,10 @@ 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"); } +.bi-palette-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-palette-fill' viewBox='0 0 16 16'%3E%3Cpath d='M12.433 10.07C14.133 10.585 16 11.15 16 8a8 8 0 1 0-8 8c1.996 0 1.826-1.504 1.649-3.08-.124-1.101-.252-2.237.351-2.92.465-.527 1.42-.237 2.433.07zM8 5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm4.5 3a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM5 6.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm.5 6.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z'/%3E%3C/svg%3E"); +} + .nav-item { font-size: 0.9rem; padding-bottom: 0.5rem; @@ -82,7 +126,7 @@ } .nav-item ::deep .nav-link { - color: #d7d7d7; + color: var(--bs-body-color); background: none; border: none; border-radius: 4px; @@ -94,18 +138,24 @@ } .nav-item ::deep a.active { - background-color: rgba(255, 255, 255, 0.37); + background-color: var(--bs-primary); color: white; } .nav-item ::deep .nav-link:hover { + background-color: var(--bs-secondary-bg); + color: var(--bs-body-color); +} + +/* Obsidian brand - dark sidebar with lighter hover effect (both modes) */ +[data-brand-theme="obsidian"] .nav-item ::deep .nav-link:hover { background-color: rgba(255, 255, 255, 0.1); color: white; } .nav-separator { border: 0; - border-top: 1px solid rgba(255, 255, 255, 0.2); + border-top: 1px solid var(--bs-border-color); margin: 0.5rem 1rem; } @@ -114,8 +164,14 @@ justify-content: space-between; } - .nav-scrollable { + [data-bs-theme="light"] .nav-scrollable { display: none; + /* background-color: #0f172a; */ + } + + [data-bs-theme="dark"] .nav-scrollable { + display: none; + /* background-color: #0a0f1a; */ } .navbar-toggler:checked ~ .nav-scrollable { @@ -128,12 +184,81 @@ display: none; } - .nav-scrollable { + [data-bs-theme="light"] .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; + /* background-color: #0f172a; */ + } + + [data-bs-theme="dark"] .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; + /* background-color: #0a0f1a; */ } } + +/* Brand Theme Dropdown Styling */ +.dropdown { + width: 100%; +} + +.dropdown-toggle { + width: 100%; + text-align: left; + border: none; + background: transparent; +} + +.dropdown-toggle::after { + float: right; + margin-top: 0.5rem; +} + +.dropdown-menu { + width: 100%; + background-color: var(--bs-secondary-bg); + border: 1px solid var(--bs-border-color); + margin-top: 0; +} + +.dropdown-item { + color: var(--bs-body-color); + padding: 0.5rem 1rem; + border: none; + width: 100%; + text-align: left; + background: transparent; +} + +.dropdown-item:hover { + background-color: var(--bs-secondary-bg); + color: var(--bs-body-color); +} + +.dropdown-item.active { + background-color: var(--bs-primary); + color: white; +} + +/* Obsidian brand - dropdown styling (both modes) */ +[data-brand-theme="obsidian"] .dropdown-menu { + background-color: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.15); +} + +[data-brand-theme="obsidian"] .dropdown-item { + color: rgba(255, 255, 255, 0.85); +} + +[data-brand-theme="obsidian"] .dropdown-item:hover { + background-color: rgba(255, 255, 255, 0.1); + color: white; +} diff --git a/4-Aquiis.SimpleStart/Shared/Services/ThemeService.cs b/4-Aquiis.SimpleStart/Shared/Services/ThemeService.cs index 43884f3..a1a30b6 100644 --- a/4-Aquiis.SimpleStart/Shared/Services/ThemeService.cs +++ b/4-Aquiis.SimpleStart/Shared/Services/ThemeService.cs @@ -3,10 +3,13 @@ namespace Aquiis.SimpleStart.Shared.Services; public class ThemeService { private string _currentTheme = "light"; + private string _currentBrandTheme = "bootstrap"; public event Action? OnThemeChanged; + public event Action? OnBrandThemeChanged; public string CurrentTheme => _currentTheme; + public string CurrentBrandTheme => _currentBrandTheme; public void SetTheme(string theme) { @@ -28,4 +31,23 @@ public string GetNextTheme() { return _currentTheme == "light" ? "dark" : "light"; } + + // Valid brand themes - add new themes here when implementing them + private readonly HashSet _validBrandThemes = new() + { + "bootstrap", + "obsidian", + "teal" + }; + + public void SetBrandTheme(string brandTheme) + { + if (!_validBrandThemes.Contains(brandTheme)) + { + throw new ArgumentException($"Brand theme must be one of: {string.Join(", ", _validBrandThemes)}", nameof(brandTheme)); + } + + _currentBrandTheme = brandTheme; + OnBrandThemeChanged?.Invoke(); + } } diff --git a/4-Aquiis.SimpleStart/wwwroot/android-chrome-192x192.png b/4-Aquiis.SimpleStart/wwwroot/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..aa0e77478140b6e68bdd871feb14177eda6cf806 GIT binary patch literal 8474 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 IM6N<$f@$PaVE_OC literal 0 HcmV?d00001 diff --git a/4-Aquiis.SimpleStart/wwwroot/android-chrome-512x512.png b/4-Aquiis.SimpleStart/wwwroot/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..013f46d314cc5de9d1af92e0293d6d87f8811b5a GIT binary patch literal 22504 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 DW6`=3 literal 0 HcmV?d00001 diff --git a/4-Aquiis.SimpleStart/wwwroot/app.css b/4-Aquiis.SimpleStart/wwwroot/app.css index ee4a87b..e2a3344 100644 --- a/4-Aquiis.SimpleStart/wwwroot/app.css +++ b/4-Aquiis.SimpleStart/wwwroot/app.css @@ -1,3 +1,277 @@ +/* ======================================== + BRAND THEME SYSTEM + For documentation on adding new themes, see: + Theming-Support-Implementation.md + ======================================== */ + +/* ======================================== + BRAND THEME: OBSIDIAN + Override Bootstrap CSS variables with Obsidian colors + Same colors apply to both light and dark modes + (Bootstrap controls background/text via data-bs-theme) + ======================================== */ + +/* Obsidian Light Mode */ +:root[data-bs-theme="light"][data-brand-theme="obsidian"], +[data-bs-theme="light"][data-brand-theme="obsidian"] .page { + --bs-primary: #25344d !important; + --bs-primary-rgb: 37, 52, 77 !important; + --bs-secondary: #d17315 !important; + --bs-secondary-rgb: 209, 115, 21 !important; + --bs-success: #1a531d !important; + --bs-success-rgb: 26, 83, 29 !important; + --bs-info: #1b60b4 !important; + --bs-info-rgb: 27, 96, 180 !important; + --bs-warning: #d29e00 !important; + --bs-warning-rgb: 210, 158, 0 !important; + --bs-danger: #712028 !important; + --bs-danger-rgb: 113, 32, 40 !important; + + /* Additional Bootstrap variables to ensure consistent theming */ + --bs-link-color: #25344d !important; + --bs-link-hover-color: #1a2435 !important; + --bs-btn-color: #25344d !important; + --bs-emphasis-color: #25344d !important; +} + +/* Obsidian Dark Mode - Identical colors */ +:root[data-bs-theme="dark"][data-brand-theme="obsidian"], +[data-bs-theme="dark"][data-brand-theme="obsidian"] .page { + --bs-primary: #25344d !important; + --bs-primary-rgb: 37, 52, 77 !important; + --bs-secondary: #d17315 !important; + --bs-secondary-rgb: 209, 115, 21 !important; + --bs-success: #1a531d !important; + --bs-success-rgb: 26, 83, 29 !important; + --bs-info: #1b60b4 !important; + --bs-info-rgb: 27, 96, 180 !important; + --bs-warning: #d29e00 !important; + --bs-warning-rgb: 210, 158, 0 !important; + --bs-danger: #712028 !important; + --bs-danger-rgb: 113, 32, 40 !important; + + /* Additional Bootstrap variables to ensure consistent theming */ + --bs-link-color: #25344d !important; + --bs-link-hover-color: #1a2435 !important; + --bs-btn-color: #25344d !important; + --bs-emphasis-color: #25344d !important; +} + +/* ======================================== + BRAND THEME: TEAL + Override Bootstrap CSS variables with Teal colors + Same colors apply to both light and dark modes + (Bootstrap controls background/text via data-bs-theme) + ======================================== */ + +/* Teal Light Mode */ +:root[data-bs-theme="light"][data-brand-theme="teal"], +[data-bs-theme="light"][data-brand-theme="teal"] .page { + --bs-primary: #0d9488 !important; + --bs-primary-rgb: 13, 148, 136 !important; + --bs-secondary: #ff6b6b !important; + --bs-secondary-rgb: 255, 107, 107 !important; + --bs-success: #388e3c !important; + --bs-success-rgb: 56, 142, 60 !important; + --bs-info: #14b8a6 !important; + --bs-info-rgb: 20, 184, 166 !important; + --bs-warning: #ffc107 !important; + --bs-warning-rgb: 255, 193, 7 !important; + --bs-danger: #dc3545 !important; + --bs-danger-rgb: 220, 53, 69 !important; + + /* Additional Bootstrap variables to ensure consistent theming */ + --bs-link-color: #0d9488 !important; + --bs-link-hover-color: #0a7169 !important; + --bs-btn-color: #0d9488 !important; + --bs-emphasis-color: #0d9488 !important; +} + +/* Teal Dark Mode - Identical colors */ +:root[data-bs-theme="dark"][data-brand-theme="teal"], +[data-bs-theme="dark"][data-brand-theme="teal"] .page { + --bs-primary: #0d9488 !important; + --bs-primary-rgb: 13, 148, 136 !important; + --bs-secondary: #ff6b6b !important; + --bs-secondary-rgb: 255, 107, 107 !important; + --bs-success: #388e3c !important; + --bs-success-rgb: 56, 142, 60 !important; + --bs-info: #14b8a6 !important; + --bs-info-rgb: 20, 184, 166 !important; + --bs-warning: #ffc107 !important; + --bs-warning-rgb: 255, 193, 7 !important; + --bs-danger: #dc3545 !important; + --bs-danger-rgb: 220, 53, 69 !important; + + /* Additional Bootstrap variables to ensure consistent theming */ + --bs-link-color: #0d9488 !important; + --bs-link-hover-color: #0a7169 !important; + --bs-btn-color: #0d9488 !important; + --bs-emphasis-color: #0d9488 !important; +} + +/* Layout Overrides for Obsidian Theme */ + +[data-bs-theme="light"] main .top-row, +[data-bs-theme="light"] .page main .top-row, +[data-bs-theme="dark"] main .top-row, +[data-bs-theme="dark"] .page main .top-row { + border-bottom: 1px solid var(--bs-border-color) !important; +} + +/* Sidebar background and text styling for Obsidian brand (both modes) */ +/* Higher specificity selector to prevent Bootstrap dark from flashing through */ +[data-bs-theme][data-brand-theme="obsidian"] .sidebar, +[data-brand-theme="obsidian"] .sidebar { + background-color: #1a1a1a !important; + color: whitesmoke !important; +} + +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .navbar-brand, +[data-brand-theme="obsidian"] .sidebar .navbar-brand { + color: whitesmoke !important; +} + +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .top-row .btn-link, +[data-brand-theme="obsidian"] .sidebar .top-row .btn-link { + color: whitesmoke !important; +} + +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .nav-separator, +[data-brand-theme="obsidian"] .sidebar .nav-separator { + border-top-color: rgba(255, 255, 255, 0.15) !important; +} + +/* Override all text colors in sidebar for Obsidian brand */ +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .nav-link, +[data-bs-theme][data-brand-theme="obsidian"] .sidebar a, +[data-bs-theme][data-brand-theme="obsidian"] .sidebar button, +[data-bs-theme][data-brand-theme="obsidian"] .sidebar div, +[data-brand-theme="obsidian"] .sidebar .nav-link, +[data-brand-theme="obsidian"] .sidebar a, +[data-brand-theme="obsidian"] .sidebar button, +[data-brand-theme="obsidian"] .sidebar div { + color: rgba(255, 255, 255, 100) !important; +} + +/* Keep navigation icons white in dark sidebar for Obsidian brand */ +[data-bs-theme][data-brand-theme="obsidian"] + .sidebar + .bi-house-door-fill-nav-menu, +[data-bs-theme][data-brand-theme="obsidian"] + .sidebar + .bi-plus-square-fill-nav-menu, +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .bi-list-nested-nav-menu, +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .bi-lock-nav-menu, +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .bi-person-nav-menu, +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .bi-person-badge-nav-menu, +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .bi-person-fill-nav-menu, +[data-bs-theme][data-brand-theme="obsidian"] + .sidebar + .bi-arrow-bar-left-nav-menu, +[data-bs-theme][data-brand-theme="obsidian"] .sidebar .bi-palette-fill-nav-menu, +[data-brand-theme="obsidian"] .sidebar .bi-house-door-fill-nav-menu, +[data-brand-theme="obsidian"] .sidebar .bi-plus-square-fill-nav-menu, +[data-brand-theme="obsidian"] .sidebar .bi-list-nested-nav-menu, +[data-brand-theme="obsidian"] .sidebar .bi-lock-nav-menu, +[data-brand-theme="obsidian"] .sidebar .bi-person-nav-menu, +[data-brand-theme="obsidian"] .sidebar .bi-person-badge-nav-menu, +[data-brand-theme="obsidian"] .sidebar .bi-person-fill-nav-menu, +[data-brand-theme="obsidian"] .sidebar .bi-arrow-bar-left-nav-menu, +[data-brand-theme="obsidian"] .sidebar .bi-palette-fill-nav-menu { + filter: none !important; +} + +/* Background Colors */ +.bg-obsidian-primary { + background-color: #25344d !important; + color: white !important; +} +.bg-obsidian-primary-dark { + background-color: #1a1a1a !important; + color: white !important; +} +.bg-obsidian-secondary { + background-color: #d17315 !important; + color: white !important; +} +.bg-obsidian-success { + background-color: #1a531d !important; + color: white !important; +} +.bg-obsidian-info { + background-color: #1b60b4 !important; + color: white !important; +} +.bg-obsidian-warning { + background-color: #d29e00 !important; + color: #2a3749 !important; +} +.bg-obsidian-danger { + background-color: #712028 !important; + color: white !important; +} +.bg-obsidian-light { + background-color: #faf9f6 !important; + color: #2a3749 !important; +} +.bg-obsidian-dark { + background-color: #2a3749 !important; + color: white !important; +} + +/* Text Colors */ +.text-obsidian-primary { + color: #25344d !important; +} +.text-obsidian-primary-dark { + color: #1a1a1a !important; +} +.text-obsidian-secondary { + color: #d17315 !important; +} +.text-obsidian-success { + color: #1a531d !important; +} +.text-obsidian-info { + color: #1b60b4 !important; +} +.text-obsidian-warning { + color: #d29e00 !important; +} +.text-obsidian-danger { + color: #712028 !important; +} +.text-obsidian-light { + color: #faf9f6 !important; +} +.text-obsidian-dark { + color: #2a3749 !important; +} + +/* Border Colors */ +.border-obsidian-primary { + border-color: #25344d !important; +} +.border-obsidian-primary-dark { + border-color: #1a1a1a !important; +} +.border-obsidian-secondary { + border-color: #d17315 !important; +} +.border-obsidian-success { + border-color: #1a531d !important; +} +.border-obsidian-info { + border-color: #1b60b4 !important; +} +.border-obsidian-warning { + border-color: #d29e00 !important; +} +.border-obsidian-danger { + border-color: #712028 !important; +} + html, body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/4-Aquiis.SimpleStart/wwwroot/apple-touch-icon.png b/4-Aquiis.SimpleStart/wwwroot/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f62ca1f61dcbcb48f9145923121220cda019aba1 GIT binary patch literal 8119 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 literal 0 HcmV?d00001 diff --git a/4-Aquiis.SimpleStart/wwwroot/favicon-32x32.png b/4-Aquiis.SimpleStart/wwwroot/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..7ae694588adaa30bdf0f15cdd855185f8ff7f9ed GIT binary patch literal 1467 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 - + diff --git a/5-Aquiis.Professional/Shared/Layout/MainLayout.razor.css b/5-Aquiis.Professional/Shared/Layout/MainLayout.razor.css index 393ac82..b4325c1 100644 --- a/5-Aquiis.Professional/Shared/Layout/MainLayout.razor.css +++ b/5-Aquiis.Professional/Shared/Layout/MainLayout.razor.css @@ -9,7 +9,7 @@ main { } .sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); + background-color: #0f172a !important; } .top-row { diff --git a/5-Aquiis.Professional/Shared/Layout/NavMenu.razor.css b/5-Aquiis.Professional/Shared/Layout/NavMenu.razor.css index 01486ae..698cad9 100644 --- a/5-Aquiis.Professional/Shared/Layout/NavMenu.razor.css +++ b/5-Aquiis.Professional/Shared/Layout/NavMenu.razor.css @@ -19,7 +19,7 @@ .top-row { min-height: 3.5rem; - background-color: rgba(0, 0, 0, 0.4); + background-color: #0f172a; } .navbar-brand { @@ -116,6 +116,7 @@ .nav-scrollable { display: none; + background-color: #0f172a; } .navbar-toggler:checked ~ .nav-scrollable { @@ -135,5 +136,6 @@ /* Allow sidebar to scroll for tall menus */ height: calc(100vh - 3.5rem); overflow-y: auto; + background-color: #0f172a; } } diff --git a/5-Aquiis.Professional/wwwroot/android-chrome-192x192.png b/5-Aquiis.Professional/wwwroot/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..aa0e77478140b6e68bdd871feb14177eda6cf806 GIT binary patch literal 8474 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 IM6N<$f@$PaVE_OC literal 0 HcmV?d00001 diff --git a/5-Aquiis.Professional/wwwroot/android-chrome-512x512.png b/5-Aquiis.Professional/wwwroot/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..013f46d314cc5de9d1af92e0293d6d87f8811b5a GIT binary patch literal 22504 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 DW6`=3 literal 0 HcmV?d00001 diff --git a/5-Aquiis.Professional/wwwroot/app.css b/5-Aquiis.Professional/wwwroot/app.css index 1127dd4..8d9eb15 100644 --- a/5-Aquiis.Professional/wwwroot/app.css +++ b/5-Aquiis.Professional/wwwroot/app.css @@ -1,9 +1,103 @@ +/* Obsidian Palette - Utility Classes */ + +/* Background Colors */ +.bg-obsidian-primary { + background-color: #25344d !important; + color: white !important; +} +.bg-obsidian-primary-dark { + background-color: #0f172a !important; + color: white !important; +} +.bg-obsidian-secondary { + background-color: #d17315 !important; + color: white !important; +} +.bg-obsidian-success { + background-color: #1a531d !important; + color: white !important; +} +.bg-obsidian-info { + background-color: #1b60b4 !important; + color: white !important; +} +.bg-obsidian-warning { + background-color: #d29e00 !important; + color: #2a3749 !important; +} +.bg-obsidian-danger { + background-color: #712028 !important; + color: white !important; +} +.bg-obsidian-light { + background-color: #faf9f6 !important; + color: #2a3749 !important; +} +.bg-obsidian-dark { + background-color: #2a3749 !important; + color: white !important; +} + +/* Text Colors */ +.text-obsidian-primary { + color: #25344d !important; +} +.text-obsidian-primary-dark { + color: #0f172a !important; +} +.text-obsidian-secondary { + color: #d17315 !important; +} +.text-obsidian-success { + color: #1a531d !important; +} +.text-obsidian-info { + color: #1b60b4 !important; +} +.text-obsidian-warning { + color: #d29e00 !important; +} +.text-obsidian-danger { + color: #712028 !important; +} +.text-obsidian-light { + color: #faf9f6 !important; +} +.text-obsidian-dark { + color: #2a3749 !important; +} + +/* Border Colors */ +.border-obsidian-primary { + border-color: #25344d !important; +} +.border-obsidian-primary-dark { + border-color: #0f172a !important; +} +.border-obsidian-secondary { + border-color: #d17315 !important; +} +.border-obsidian-success { + border-color: #1a531d !important; +} +.border-obsidian-info { + border-color: #1b60b4 !important; +} +.border-obsidian-warning { + border-color: #d29e00 !important; +} +.border-obsidian-danger { + border-color: #712028 !important; +} + html, body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; background-color: var(--bs-body-bg) !important; color: var(--bs-body-color) !important; - transition: background-color 0.3s ease, color 0.3s ease; + transition: + background-color 0.3s ease, + color 0.3s ease; } /* Ensure color-scheme is set for proper dark mode rendering */ @@ -61,7 +155,9 @@ a, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; + box-shadow: + 0 0 0 0.1rem white, + 0 0 0 0.25rem #258cfb; } .content { @@ -85,7 +181,8 @@ h1:focus { } .blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) + background: + url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; padding: 1rem 1rem 1rem 3.7rem; diff --git a/5-Aquiis.Professional/wwwroot/apple-touch-icon.png b/5-Aquiis.Professional/wwwroot/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f62ca1f61dcbcb48f9145923121220cda019aba1 GIT binary patch literal 8119 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 literal 0 HcmV?d00001 diff --git a/5-Aquiis.Professional/wwwroot/favicon-32x32.png b/5-Aquiis.Professional/wwwroot/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..7ae694588adaa30bdf0f15cdd855185f8ff7f9ed GIT binary patch literal 1467 zcmV;s1w{IZP)0$G6kYe3mlqR_FBBEO(hq60L9|M%(yEij`T<(MW&tNoivK`qM}-cYHaHZE;Lw+W zkXkZnjf$0mkW?*}mZqRkCy`duB-q=v&OY~?b6=iXFMFS}_u6akv+ud*-uEg1D)sun zXFWXw52*R`wB9q2_tdGHrKv5yg}R&;IM;ZfHMO=DKb>Xxg3bzR3(1IO$T->{gX#dz z&5{EYhZ@C3l#;PRYqE*c6&fe;!b|0(MIn;qa%w50QIe1+=YCEi~|Z^2jn4&*63;Mj43?z`Hjh=3QBq;qZ~2*tc&hIy-BO=V2pMg{j1vB@ci&a@-Ec zFpUKB5sLDr5MMB0`}TgUS-q5tcVO?{t&CC9bT&vN3S*S$dn+8m7>rx#IW`q7n}=h^ zcG0q|BS&P@ZQ>BIzGW~*k(J&Dd;?j$Q>J7_^JCc)xO{mJcI?ylDEKo17zfA5l)Nb*tGF2JAC9)aR2@vn3(ttNHUKe^{-^Q<*=*O3mzNbS}KZA zj6hPPR;yxmb`Hg*sO;h4F-<{t?|u)97cWdQL&zINrn#{J`dL_*JW7o^_{QH_%aQ$Nv4>t&#whDk(uq0hJnXRhkwqh$~mV20i1=o8QLf zcd44GeSN*Sabp5n(H(^haEB4RXmgw|3EtGee6Emn>(=1bt)DSAHbEW7*fr{y=~d3> z&W(r$HKG33u3d>uo7MsXlDkzby4J=2YM{UW4cxu^yKNB4m&)9`H;FqxO|#h9E*WB9w&#Q+aucXyY^xERHXz$*BU#7i5{ zPwg1D*{uy-s>In(v|QaQS8nkz&d7yUsI|A@z=01CE)|e6&%Tf}7;MpiSd6ercXua_ zAOF~W^@NnPpb?b$9@Jb@Msv3vAIqE}VZfYB+P|Q>PsqP~`3A&4|MiH?%zgA|%Kdk&wpWepM_WEC z{tuvZ&CSi>;>A%64K;#Zy7WCB^GC=L-9uyviVlF@f(sYE4cwujVQ+K(s^NaR?JF9a zHj95sUBOoww5Vl+`)G>iT_k(1P4t_ndIS$0)*Ng09f^ahr*00006NklvLAc8Gmj2-SZOeCDd>ZLdgNeF>}3I$VwHX+agGGG+TrBH(mglk0x zq=ghLPCKCtZK=f+MNzO^X1E6ef#j=yLdSgCKF{uXcAvAC^PV?|UutH~?4I4{@_T;! zoIQK?-Q;rje?s>=0LkS6l?=3LU$H#hSHv+)9ykc%cwE#y^Ni>Q-yK)mMfH96 z89lOr`fw2zv_thez|gXBll1+;)~&Iw8h`0e`=&PJ&+sv8)F{z@<+9|x;n`=kUCJSi znS4qdh8OHVdazY|{;#9bH{l|l6lV%Q#-Okx2E)&bU^Ha*-4*=(C!1*;|OY=NZG3=>IO- zi5o1b?(r)*(EZ6dAPsX>|Ndg*Yp;o2Yu4!NorVVSLTRa)gMK9bedv((i@wjFIU^rLO^X*>zT4l1CKzMl9Qp6O(IH*{ z{`9z7xk^0i{uC^jKT=NTvWwxwKh^tOVgUa*?qu8b1+k>qB0VbWpMF~4yO!>gWh5~F zdF)un^_cX_{m6Pck4f7m&dp1gir1%36^(dR$7OtfXzyO-KhFD8pL`O)YV7&^AYPX% zrL!E~`9FK)5t)ma6Lighwi}pt(s!3E*DqL*K3<%Y#JHO|%EnPOaG+T6^2?6hruurr zJ@9_x`gJjN=unVTJlk>Fc6Gl8lP$aEl~Zr`qC5^<&Tt}z5KV?Sc4 zg*EgqpMPH4uOB;B`tE|bDgS6KZDUt9isZx-!|FM6wEvdoX5}}|yD!_?bf1YSYwQK& z;_+#$u7>x}a9BSM6z!LB9o#J_ez}(C;~1LfxD4am$5}FtNWKeJLLMRy`29h=F1 z1*C1@nfXAwg>9So-8E_47u)>J?AhYj;ltWDV))axZP~adTvQgo=EFU6=EKZ-a>vC$#*?GwQqwE%V zC+*+7S;nsV2rHIUH-3`(s$a&>pHq$7)wl7zIDEML9=F?*emwqTA0atuk7S^9s+t)L9C_4o?<36x1 z{g{{uHoxhDr4 z-dmQ>&w;t+G}QlVacbpIkb5>?1#(r7U$6TS+i8V0e=mea*muFWf0K@zd|LQ3$7`~jn^#k1r zcRANGDRwk|E#ulzGFH2mzw@!e_2i@h1A>>_0|Ob2mBx%=kTziNFwKsy5A__o9{WKV zkevqK{P+#@)u*3||9tjY;`+yd0|LL@Lwj*#7xy=DE#Lb7`{JwP#}hVen|OZ$enY|i zO|=~^?p?n={zij68MZ$_ zb=)g`Yx;E9S1k!+58!lSWOC?x$EsEEZQLbVcklM^T{8KQttC}e;>yK~UhG}BZwuUy z_UDcDjHLJqjSI$b1af8*>}iK&6z;IEC6gwJD;F*Re$KzDqwPuXoP=UG?P2?cWaj;J zoY)BazkZI=bF!jOiQND1ym`~%|0e9~E}Sp{uxG)RJ!efkN(Y}s+|R*1c)PFAJ?i{@ z|9%HMj>XZ|*7WDKKwP1G;O$sP-rugP6LUt7E*gIw?CV{-a>c=atFu#VZES@3Anlpu z4P zfd?|jfOGlf^XI+z{|0+bm6@K~B-cDQc$}&|$+_uQ?Z_8C$EID`<~Dg9$BX88yTm!w zwiCKf$CiwbbMCX((!4>`JocEF@$kc`SIwi3ij&8VdGTYrZ!Qbr9LEsLcF+s!?Q1; z_YUq!`*H`@A^TvD5`QmnFnY%#opWU)`xueWMdju4w>!GG%jL6XIeY3j4)`8;YsL&0 zyEVqPJ$|qG)H=oENB;f@zxPPH9^Trl-@Yk_PQiM-xOaH**v$Nsu8Y5$;8{7&4c6=V z@3+=kc^j<9ZF$8f_yQg{x4dra(0vvelC{gp@QmU-B)`StvBYl!{LPX0=H=B(m*jkF z>{$GG#?i?7p976Qq{}(s^%yVycE*28uZ8n;=T4n+=1h#EW#dMN`$G6q`$L%Zb7947 z$z$v|2d%EHm1{73AL03o&W;X;fBc5Sa{;M%LVgRj^Ks<$(0jqi;5@&2cCB0kPrd(s z@z*_j9PG$FexuNNpR6(H+_3P6+OyiFIiKt-gYVY5F=ND>C!P>jFS*}{_}yZE`;cyS zpH?ndF`*r0({P*B_VkAy65HNkzXT?r(cpe4M zH}Uy~>IdPwznC*uWYaB+=hn61AIFy-d{FMM5N2M=v(%a&j)B+xb{+rPPDXi8o;fyS z89dX?abq3NqtNpb(ku8Z+zDo_TbL_6%fi2N@l3%gct?kDJMBgIa&(K}Ii!p8iM* z9I~-{^~om%&X?`5*1`V|Fuv36e%o>(oVwi_3u?zbiofjK3IE5?CaMPyNvtO|MoTx; zPByV$*F(CDLv7D)16^&$?qm0O<_y Date: Mon, 26 Jan 2026 21:34:45 -0600 Subject: [PATCH 6/8] simple-start-refactor --- .../PropertyManagement/Repairs/Create.razor | 4 ++-- .../PropertyManagement/Repairs/View.razor | 2 +- 4-Aquiis.SimpleStart/wwwroot/app.css | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Create.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Create.razor index 215a218..2becaec 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Create.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/Create.razor @@ -25,7 +25,7 @@
-
+
Repair Information
@@ -188,7 +188,7 @@
-
+
About Repairs

diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/View.razor index 8da231d..a72da1f 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Repairs/View.razor @@ -351,6 +351,6 @@ } } - private void NavigateToEdit() => Navigation.NavigateTo($"/propertymanagement/repairs/edit/{Id}"); + private void NavigateToEdit() => Navigation.NavigateTo($"/propertymanagement/repairs/{Id}/edit"); private void Back() => Navigation.NavigateTo("/propertymanagement/repairs"); } diff --git a/4-Aquiis.SimpleStart/wwwroot/app.css b/4-Aquiis.SimpleStart/wwwroot/app.css index e2a3344..56eb65e 100644 --- a/4-Aquiis.SimpleStart/wwwroot/app.css +++ b/4-Aquiis.SimpleStart/wwwroot/app.css @@ -57,6 +57,25 @@ --bs-emphasis-color: #25344d !important; } +/* Table text color for Obsidian dark theme */ +[data-bs-theme="dark"][data-brand-theme="obsidian"] table, +[data-bs-theme="dark"][data-brand-theme="obsidian"] .table, +[data-bs-theme="dark"][data-brand-theme="obsidian"] table thead, +[data-bs-theme="dark"][data-brand-theme="obsidian"] table tbody, +[data-bs-theme="dark"][data-brand-theme="obsidian"] table th, +[data-bs-theme="dark"][data-brand-theme="obsidian"] table td { + color: whitesmoke !important; +} + +/* Override Bootstrap's table text color more specifically */ +[data-bs-theme="dark"][data-brand-theme="obsidian"] + .table + > :not(caption) + > * + > * { + color: whitesmoke !important; +} + /* ======================================== BRAND THEME: TEAL Override Bootstrap CSS variables with Teal colors From f47bc652e27e7e0b712125469ca24b47db523129 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Tue, 27 Jan 2026 20:29:39 -0600 Subject: [PATCH 7/8] simple start refactor --- .../Utilities/CalendarEventRouter.cs | 6 +- ...TestEntityAndPropertyTestField.Designer.cs | 4314 +++++++++++++++++ ...84357_AddTestEntityAndPropertyTestField.cs | 76 + ...TestEntityAndPropertyTestField.Designer.cs | 4245 ++++++++++++++++ ...38_RemoveTestEntityAndPropertyTestField.cs | 76 + .../ApplicationDbContextModelSnapshot.cs | 8 +- .../Services/CalendarEventService.cs | 35 +- .../Services/PaymentService.cs | 6 +- .../Services/SchemaValidationService.cs | 2 +- .../Inspections/RoutineInspectionCard.razor | 10 +- .../Entities/Invoices/InvoiceList.razor | 2 +- .../Entities/Leases/LeaseListView.razor | 2 +- .../Leases/LeaseTransactionsCard.razor | 2 +- .../Entities/Properties/PropertyDetails.razor | 13 +- .../Properties/PropertyMetricsCard.razor | 2 +- .../Invoices/ViewForm.razor | 9 +- .../Features/Calendar/Calendar.razor | 157 +- .../Features/Calendar/CalendarListView.razor | 2 +- .../Leases/Pages/Index.razor | 12 +- .../Properties/Pages/View.razor | 6 +- .../Pages/PropertyPerformanceReport.razor | 26 +- .../Services/WebPathService.cs | 21 +- .../Shared/Components/Pages/Home.razor | 3 +- .../Shared/Layout/NavMenu.razor | 8 - .../appsettings.Development.json | 3 +- 4-Aquiis.SimpleStart/appsettings.json | 2 +- .../Features/Calendar/Calendar.razor | 151 +- .../Features/Calendar/CalendarListView.razor | 2 +- .../Services/WebPathService.cs | 21 +- .../Components/Account/Pages/Login.razor | 4 +- .../Account/Pages/RegisterConfirmation.razor | 6 +- 31 files changed, 9088 insertions(+), 144 deletions(-) create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260127184357_AddTestEntityAndPropertyTestField.Designer.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260127184357_AddTestEntityAndPropertyTestField.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260127190738_RemoveTestEntityAndPropertyTestField.Designer.cs create mode 100644 1-Aquiis.Infrastructure/Data/Migrations/20260127190738_RemoveTestEntityAndPropertyTestField.cs diff --git a/0-Aquiis.Core/Utilities/CalendarEventRouter.cs b/0-Aquiis.Core/Utilities/CalendarEventRouter.cs index 05050c5..e283663 100644 --- a/0-Aquiis.Core/Utilities/CalendarEventRouter.cs +++ b/0-Aquiis.Core/Utilities/CalendarEventRouter.cs @@ -19,9 +19,9 @@ public static class CalendarEventRouter 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}", + nameof(Tour) => $"/PropertyManagement/Tours/{evt.SourceEntityId}", + nameof(Inspection) => $"/PropertyManagement/Inspections/{evt.SourceEntityId}", + nameof(MaintenanceRequest) => $"/PropertyManagement/Maintenance/{evt.SourceEntityId}", // Add new schedulable entity routes here as they are created _ => null }; diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260127184357_AddTestEntityAndPropertyTestField.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260127184357_AddTestEntityAndPropertyTestField.Designer.cs new file mode 100644 index 0000000..afd2770 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260127184357_AddTestEntityAndPropertyTestField.Designer.cs @@ -0,0 +1,4314 @@ +// +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("20260127184357_AddTestEntityAndPropertyTestField")] + partial class AddTestEntityAndPropertyTestField + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + 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.OrganizationUser", 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("OrganizationUsers"); + }); + + 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.Property("PaymentNumber") + .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("TestField") + .HasMaxLength(100) + .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.Repair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("ContactId") + .HasColumnType("TEXT"); + + b.Property("ContactPerson") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorId") + .HasColumnType("TEXT"); + + b.Property("ContractorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Cost") + .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(200) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .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("MaintenanceRequestId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PartsReplaced") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RepairType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("WarrantyApplies") + .HasColumnType("INTEGER"); + + b.Property("WarrantyExpiresOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("LeaseId"); + + b.HasIndex("MaintenanceRequestId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RepairType"); + + b.ToTable("Repairs"); + }); + + 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.TestEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .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("TestDate") + .HasColumnType("TEXT"); + + b.Property("TestDescription") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("TestFlag") + .HasColumnType("INTEGER"); + + b.Property("TestName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("TestNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("TestName"); + + b.ToTable("TestEntities"); + }); + + 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.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActiveOrganizationId"); + + b.HasIndex("Email"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserProfiles"); + }); + + 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("MaintenanceRequests") + .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.OrganizationUser", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("OrganizationUsers") + .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.Repair", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.MaintenanceRequest", "MaintenanceRequest") + .WithMany("Repairs") + .HasForeignKey("MaintenanceRequestId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Repairs") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("MaintenanceRequest"); + + b.Navigation("Property"); + }); + + 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.TestEntity", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .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("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.MaintenanceRequest", b => + { + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + + b.Navigation("MaintenanceRequests"); + + b.Navigation("Repairs"); + }); + + 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/20260127184357_AddTestEntityAndPropertyTestField.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260127184357_AddTestEntityAndPropertyTestField.cs new file mode 100644 index 0000000..281ef18 --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260127184357_AddTestEntityAndPropertyTestField.cs @@ -0,0 +1,76 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Infrastructure.Data.Migrations +{ + /// + public partial class AddTestEntityAndPropertyTestField : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TestField", + table: "Properties", + type: "TEXT", + maxLength: 100, + nullable: true); + + migrationBuilder.CreateTable( + name: "TestEntities", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + TestName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + TestDescription = table.Column(type: "TEXT", maxLength: 500, nullable: true), + TestDate = table.Column(type: "TEXT", nullable: true), + TestNumber = table.Column(type: "INTEGER", nullable: false), + TestFlag = 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_TestEntities", x => x.Id); + table.ForeignKey( + name: "FK_TestEntities_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TestEntities_IsDeleted", + table: "TestEntities", + column: "IsDeleted"); + + migrationBuilder.CreateIndex( + name: "IX_TestEntities_OrganizationId", + table: "TestEntities", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_TestEntities_TestName", + table: "TestEntities", + column: "TestName"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TestEntities"); + + migrationBuilder.DropColumn( + name: "TestField", + table: "Properties"); + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260127190738_RemoveTestEntityAndPropertyTestField.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260127190738_RemoveTestEntityAndPropertyTestField.Designer.cs new file mode 100644 index 0000000..da1b16c --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260127190738_RemoveTestEntityAndPropertyTestField.Designer.cs @@ -0,0 +1,4245 @@ +// +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("20260127190738_RemoveTestEntityAndPropertyTestField")] + partial class RemoveTestEntityAndPropertyTestField + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + 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.OrganizationUser", 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("OrganizationUsers"); + }); + + 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.Property("PaymentNumber") + .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.Repair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedOn") + .HasColumnType("TEXT"); + + b.Property("ContactId") + .HasColumnType("TEXT"); + + b.Property("ContactPerson") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorId") + .HasColumnType("TEXT"); + + b.Property("ContractorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ContractorPhone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Cost") + .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(200) + .HasColumnType("TEXT"); + + b.Property("DurationMinutes") + .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("MaintenanceRequestId") + .HasColumnType("TEXT"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PartsReplaced") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("PropertyId") + .HasColumnType("TEXT"); + + b.Property("RepairType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("WarrantyApplies") + .HasColumnType("INTEGER"); + + b.Property("WarrantyExpiresOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CompletedOn"); + + b.HasIndex("LeaseId"); + + b.HasIndex("MaintenanceRequestId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("PropertyId"); + + b.HasIndex("RepairType"); + + b.ToTable("Repairs"); + }); + + 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.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActiveOrganizationId") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LastModifiedOn") + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActiveOrganizationId"); + + b.HasIndex("Email"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserProfiles"); + }); + + 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("MaintenanceRequests") + .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.OrganizationUser", b => + { + b.HasOne("Aquiis.Core.Entities.Organization", "Organization") + .WithMany("OrganizationUsers") + .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.Repair", b => + { + b.HasOne("Aquiis.Core.Entities.Lease", "Lease") + .WithMany() + .HasForeignKey("LeaseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.MaintenanceRequest", "MaintenanceRequest") + .WithMany("Repairs") + .HasForeignKey("MaintenanceRequestId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Aquiis.Core.Entities.Property", "Property") + .WithMany("Repairs") + .HasForeignKey("PropertyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Lease"); + + b.Navigation("MaintenanceRequest"); + + b.Navigation("Property"); + }); + + 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("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.MaintenanceRequest", b => + { + b.Navigation("Repairs"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Organization", b => + { + b.Navigation("Leases"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Properties"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Aquiis.Core.Entities.Property", b => + { + b.Navigation("Documents"); + + b.Navigation("Leases"); + + b.Navigation("MaintenanceRequests"); + + b.Navigation("Repairs"); + }); + + 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/20260127190738_RemoveTestEntityAndPropertyTestField.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260127190738_RemoveTestEntityAndPropertyTestField.cs new file mode 100644 index 0000000..8a1c80e --- /dev/null +++ b/1-Aquiis.Infrastructure/Data/Migrations/20260127190738_RemoveTestEntityAndPropertyTestField.cs @@ -0,0 +1,76 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aquiis.Infrastructure.Data.Migrations +{ + /// + public partial class RemoveTestEntityAndPropertyTestField : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TestEntities"); + + migrationBuilder.DropColumn( + name: "TestField", + table: "Properties"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TestField", + table: "Properties", + type: "TEXT", + maxLength: 100, + nullable: true); + + migrationBuilder.CreateTable( + name: "TestEntities", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: false), + CreatedOn = table.Column(type: "TEXT", nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + LastModifiedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + LastModifiedOn = table.Column(type: "TEXT", nullable: true), + OrganizationId = table.Column(type: "TEXT", maxLength: 100, nullable: false), + TestDate = table.Column(type: "TEXT", nullable: true), + TestDescription = table.Column(type: "TEXT", maxLength: 500, nullable: true), + TestFlag = table.Column(type: "INTEGER", nullable: false), + TestName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + TestNumber = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TestEntities", x => x.Id); + table.ForeignKey( + name: "FK_TestEntities_Organizations_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organizations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TestEntities_IsDeleted", + table: "TestEntities", + column: "IsDeleted"); + + migrationBuilder.CreateIndex( + name: "IX_TestEntities_OrganizationId", + table: "TestEntities", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_TestEntities_TestName", + table: "TestEntities", + column: "TestName"); + } + } +} diff --git a/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs index ae87e0f..095e216 100644 --- a/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/1-Aquiis.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -3919,7 +3919,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.SetNull); b.HasOne("Aquiis.Core.Entities.Property", "Property") - .WithMany() + .WithMany("MaintenanceRequests") .HasForeignKey("PropertyId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); @@ -4048,7 +4048,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.SetNull); b.HasOne("Aquiis.Core.Entities.Property", "Property") - .WithMany() + .WithMany("Repairs") .HasForeignKey("PropertyId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); @@ -4204,6 +4204,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Documents"); b.Navigation("Leases"); + + b.Navigation("MaintenanceRequests"); + + b.Navigation("Repairs"); }); modelBuilder.Entity("Aquiis.Core.Entities.ProspectiveTenant", b => diff --git a/2-Aquiis.Application/Services/CalendarEventService.cs b/2-Aquiis.Application/Services/CalendarEventService.cs index 09aeb29..573b6ee 100644 --- a/2-Aquiis.Application/Services/CalendarEventService.cs +++ b/2-Aquiis.Application/Services/CalendarEventService.cs @@ -142,12 +142,28 @@ public async Task> GetEventsAsync( /// public async Task CreateCustomEventAsync(CalendarEvent calendarEvent) { - calendarEvent.EventType = CalendarEventTypes.Custom; + // Service sets tracking fields - UI should not set these + var organizationId = await _userContextService.GetActiveOrganizationIdAsync(); + var userId = await _userContextService.GetUserIdAsync(); + + calendarEvent.Id = Guid.NewGuid(); + calendarEvent.OrganizationId = organizationId ?? Guid.Empty; + calendarEvent.CreatedBy = userId ?? string.Empty; + calendarEvent.CreatedOn = DateTime.UtcNow; + + // Not linked to a source entity (user-created from calendar UI) calendarEvent.SourceEntityId = null; calendarEvent.SourceEntityType = null; - calendarEvent.Color = CalendarEventTypes.GetColor(CalendarEventTypes.Custom); - calendarEvent.Icon = CalendarEventTypes.GetIcon(CalendarEventTypes.Custom); - calendarEvent.CreatedOn = DateTime.UtcNow; + + // Set color and icon from event type if not already set + if (string.IsNullOrEmpty(calendarEvent.Color)) + { + calendarEvent.Color = CalendarEventTypes.GetColor(calendarEvent.EventType ?? CalendarEventTypes.Custom); + } + if (string.IsNullOrEmpty(calendarEvent.Icon)) + { + calendarEvent.Icon = CalendarEventTypes.GetIcon(calendarEvent.EventType ?? CalendarEventTypes.Custom); + } _context.CalendarEvents.Add(calendarEvent); await _context.SaveChangesAsync(); @@ -160,9 +176,12 @@ public async Task CreateCustomEventAsync(CalendarEvent calendarEv /// public async Task UpdateCustomEventAsync(CalendarEvent calendarEvent) { + var organizationId = await _userContextService.GetActiveOrganizationIdAsync(); + var userId = await _userContextService.GetUserIdAsync(); + var existing = await _context.CalendarEvents .FirstOrDefaultAsync(e => e.Id == calendarEvent.Id - && e.OrganizationId == calendarEvent.OrganizationId + && e.OrganizationId == organizationId && e.SourceEntityType == null && !e.IsDeleted); @@ -176,8 +195,10 @@ public async Task CreateCustomEventAsync(CalendarEvent calendarEv existing.PropertyId = calendarEvent.PropertyId; existing.Location = calendarEvent.Location; existing.Status = calendarEvent.Status; - existing.LastModifiedBy = calendarEvent.LastModifiedBy; - existing.LastModifiedOn = calendarEvent.LastModifiedOn; + + // Service sets tracking fields + existing.LastModifiedBy = userId ?? string.Empty; + existing.LastModifiedOn = DateTime.UtcNow; await _context.SaveChangesAsync(); diff --git a/2-Aquiis.Application/Services/PaymentService.cs b/2-Aquiis.Application/Services/PaymentService.cs index b3d1803..11993e4 100644 --- a/2-Aquiis.Application/Services/PaymentService.cs +++ b/2-Aquiis.Application/Services/PaymentService.cs @@ -77,7 +77,8 @@ protected override async Task ValidateEntityAsync(Payment entity) .SumAsync(p => p.Amount); var totalWithThisPayment = existingPayments + entity.Amount; - var invoiceTotal = invoice.Amount + (invoice.LateFeeAmount ?? 0); + // Invoice amount already includes late fees (added by ScheduledTaskService) + var invoiceTotal = invoice.Amount; if (totalWithThisPayment > invoiceTotal) { @@ -419,7 +420,8 @@ private async Task UpdateInvoiceAfterPaymentChangeAsync(Guid invoiceId) invoice.AmountPaid = totalPaid; - var totalDue = invoice.Amount + (invoice.LateFeeAmount ?? 0); + // Total due is the invoice amount (which includes late fees already added by ScheduledTaskService) + var totalDue = invoice.Amount; // Update invoice status based on payment // Don't change status if invoice is Cancelled or Voided diff --git a/2-Aquiis.Application/Services/SchemaValidationService.cs b/2-Aquiis.Application/Services/SchemaValidationService.cs index 49e2b32..7762c14 100644 --- a/2-Aquiis.Application/Services/SchemaValidationService.cs +++ b/2-Aquiis.Application/Services/SchemaValidationService.cs @@ -52,7 +52,7 @@ public SchemaValidationService( dbVersion); } - _logger.LogInformation("Schema version validated successfully: {Version}", dbVersion); + _logger.LogDebug("Schema version validated successfully: {Version}", dbVersion); return (true, $"Schema version {dbVersion} is valid", dbVersion); } catch (Exception ex) diff --git a/3-Aquiis.UI.Shared/Components/Entities/Inspections/RoutineInspectionCard.razor b/3-Aquiis.UI.Shared/Components/Entities/Inspections/RoutineInspectionCard.razor index 5b3e96d..975c62f 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Inspections/RoutineInspectionCard.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Inspections/RoutineInspectionCard.razor @@ -8,13 +8,13 @@ @if (ShowScheduleButton && OnCompleteInspection.HasDelegate) { }

- @if (LastInspection != null) + @if (LastInspection != null && LastInspection.Id != Guid.Empty) {
@@ -50,12 +50,12 @@
Last Inspection
-

No inspections recorded

+

No inspections recorded.

} -
+
Next Inspection
@if (NextInspectionDate.HasValue) { @@ -92,7 +92,7 @@ @if (ShowScheduleButton && OnCompleteInspection.HasDelegate) { } } diff --git a/3-Aquiis.UI.Shared/Components/Entities/Invoices/InvoiceList.razor b/3-Aquiis.UI.Shared/Components/Entities/Invoices/InvoiceList.razor index c72d669..057863d 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Invoices/InvoiceList.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Invoices/InvoiceList.razor @@ -7,7 +7,7 @@ @if (Invoices.Any()) {
- @foreach (var invoice in Invoices) + @foreach (var invoice in Invoices.OrderByDescending(i => i.InvoiceNumber)) {
diff --git a/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseListView.razor b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseListView.razor index 9000c82..c0480aa 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseListView.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseListView.razor @@ -10,7 +10,7 @@ ActiveCount="@ActiveCount" ExpiringSoonCount="@ExpiringSoonCount" TotalMonthlyRent="@TotalMonthlyRent" - ShowActive="true" ShowTotalCount="true" ShowTotalMonthlyRent="true" /> + ShowActive="true" ShowExpiringSoon="true" ShowTotalMonthlyRent="true" /> } diff --git a/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseTransactionsCard.razor b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseTransactionsCard.razor index a5dce4e..9df95fc 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseTransactionsCard.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Leases/LeaseTransactionsCard.razor @@ -8,7 +8,7 @@
- @foreach (var invoice in LeaseInvoices.OrderByDescending(i => i.InvoicedOn)) + @foreach (var invoice in LeaseInvoices.OrderByDescending(i => i.InvoiceNumber)) {
diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor index ca5d474..26907dc 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyDetails.razor @@ -6,7 +6,7 @@
Property Information
- @(Property.IsAvailable ? "Available" : "Occupied") + @Property.Status @if (OnEdit.HasDelegate) { @@ -100,6 +100,15 @@ private string GetAvailabilityBadgeClass() { - return Property.IsAvailable ? "bg-success" : "bg-warning"; + return Property.Status switch + { + ApplicationConstants.PropertyStatuses.Available => "bg-success", + ApplicationConstants.PropertyStatuses.ApplicationPending => "bg-warning text-dark", + ApplicationConstants.PropertyStatuses.LeasePending => "bg-info", + ApplicationConstants.PropertyStatuses.Occupied => "bg-danger", + ApplicationConstants.PropertyStatuses.UnderRenovation => "bg-secondary", + ApplicationConstants.PropertyStatuses.OffMarket => "bg-dark text-white", + _ => "bg-secondary" + }; } } diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyMetricsCard.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyMetricsCard.razor index 8d5000d..6acfc4b 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyMetricsCard.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyMetricsCard.razor @@ -16,7 +16,7 @@ @if(ShowOccupiedProperties){ } @if(ShowLeasePendingProperties){ diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ViewForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ViewForm.razor index 5e483bf..aec3b71 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ViewForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Invoices/ViewForm.razor @@ -57,7 +57,14 @@ else @if (invoice.IsOverdue) {
- (@invoice.DaysOverdue days overdue) + @if(invoice.DaysOverdue == 1) + { + (1 day overdue) + } + else + { + (@invoice.DaysOverdue days overdue) + } }
diff --git a/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor b/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor index c2e9127..8ef89a0 100644 --- a/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor +++ b/4-Aquiis.SimpleStart/Features/Calendar/Calendar.razor @@ -9,6 +9,7 @@ @using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @inject CalendarEventService CalendarEventService @inject CalendarSettingsService CalendarSettingsService @@ -502,43 +503,43 @@
- + @@ -86,7 +86,7 @@ else if (!leases.Any()) else {

No Leases Found

-

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

+

Get started by completing your first lease agreement.

}
@@ -446,10 +446,10 @@ else { activeCount = filteredLeases.Count(l => l.Status == "Active"); - // Expiring within 30 days - var thirtyDaysFromNow = DateTime.Now.AddDays(30); + // Expiring within 90 days + var ninetyDaysFromNow = DateTime.Now.AddDays(90); expiringSoonCount = filteredLeases.Count(l => - l.Status == "Active" && l.EndDate <= thirtyDaysFromNow); + l.Status == "Active" && l.EndDate <= ninetyDaysFromNow); totalMonthlyRent = filteredLeases .Where(l => l.Status == "Active") diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor index 027e6b2..cf62e2e 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Properties/Pages/View.razor @@ -82,8 +82,11 @@
-
@@ -219,6 +222,7 @@ private bool showLeaseModal = false; private bool showRepairModal = false; private bool showInspectionModal = false; + private bool showInspectionViewButton => lastInspection != null && lastInspection.Id != Guid.Empty && property.LastRoutineInspectionDate.HasValue; // Selected entities for modals private Lease? selectedLease; diff --git a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor index 2787596..be0b56c 100644 --- a/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor +++ b/4-Aquiis.SimpleStart/Features/PropertyManagement/Reports/Pages/PropertyPerformanceReport.razor @@ -187,10 +187,32 @@
@code { - private DateTime startDate = new DateTime(DateTime.Now.Year, 1, 1); - private DateTime endDate = DateTime.Now; + private DateTime startDate = GetFiscalYearStart(); + private DateTime endDate = GetFiscalYearEnd(); private List performanceItems = new(); private bool isLoading = false; + + private static DateTime GetFiscalYearStart() + { + var today = DateTime.Now; + // Fiscal year runs April 1 - March 31 + // If current month is April or later, fiscal year started April 1 of current year + // If current month is before April, fiscal year started April 1 of previous year + return today.Month >= 4 + ? new DateTime(today.Year, 4, 1) + : new DateTime(today.Year - 1, 4, 1); + } + + private static DateTime GetFiscalYearEnd() + { + var today = DateTime.Now; + // Fiscal year ends March 31 + // If current month is April or later, fiscal year ends March 31 of next year + // If current month is before April, fiscal year ends March 31 of current year + return today.Month >= 4 + ? new DateTime(today.Year + 1, 3, 31) + : new DateTime(today.Year, 3, 31); + } [CascadingParameter] private Task? AuthenticationStateTask { get; set; } diff --git a/4-Aquiis.SimpleStart/Services/WebPathService.cs b/4-Aquiis.SimpleStart/Services/WebPathService.cs index ae32fed..580688f 100644 --- a/4-Aquiis.SimpleStart/Services/WebPathService.cs +++ b/4-Aquiis.SimpleStart/Services/WebPathService.cs @@ -31,16 +31,25 @@ public async Task GetConnectionStringAsync(object 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)); + // Extract Data Source from connection string (supports both "Data Source=" and "DataSource=") + var dbPath = connectionString + .Replace("Data Source=", "") + .Replace("DataSource=", "") + .Split(';')[0] + .Trim(); - if (dataSource != null) + if (string.IsNullOrEmpty(dbPath)) { - return dataSource.Split('=')[1].Trim(); + dbPath = "aquiis.db"; // Default } - return "aquiis.db"; // Default + // Make absolute path if relative + if (!Path.IsPathRooted(dbPath)) + { + dbPath = Path.Combine(Directory.GetCurrentDirectory(), dbPath); + } + + return dbPath; } public async Task GetUserDataPathAsync() diff --git a/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor b/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor index 2a2f938..ee3d88e 100644 --- a/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor +++ b/4-Aquiis.SimpleStart/Shared/Components/Pages/Home.razor @@ -69,7 +69,7 @@ OnViewRepair="ViewRepair" ShowTotalCost="true" />
- !l.IsDeleted && l.Status == ApplicationConstants.LeaseStatuses.Active) .Sum(l => l.MonthlyRent); diff --git a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor index f3c6eed..5e4ae1c 100644 --- a/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor +++ b/4-Aquiis.SimpleStart/Shared/Layout/NavMenu.razor @@ -196,7 +196,6 @@ var orgId = await UserContext.GetActiveOrganizationIdAsync(); OrganizationId = orgId?.ToString() ?? string.Empty; - Console.WriteLine($"NavMenu initialized. OrganizationId: {OrganizationId}"); } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -211,24 +210,17 @@ var domBrandTheme = await JSRuntime.InvokeAsync("eval", "document.documentElement.getAttribute('data-brand-theme') || localStorage.getItem('brandTheme') || 'bootstrap'"); - Console.WriteLine($"NavMenu OnAfterRenderAsync - DOM theme: {domTheme}, brand: {domBrandTheme}"); - Console.WriteLine($"NavMenu OnAfterRenderAsync - Service BEFORE sync: {ThemeService.CurrentTheme}, brand: {ThemeService.CurrentBrandTheme}"); - // Sync the service with the DOM state (which was set by theme.js) if (ThemeService.CurrentTheme != domTheme) { - Console.WriteLine($"Syncing theme service: {ThemeService.CurrentTheme} -> {domTheme}"); ThemeService.SetTheme(domTheme); } if (ThemeService.CurrentBrandTheme != domBrandTheme) { - Console.WriteLine($"Syncing brand theme service: {ThemeService.CurrentBrandTheme} -> {domBrandTheme}"); ThemeService.SetBrandTheme(domBrandTheme); } - Console.WriteLine($"NavMenu OnAfterRenderAsync - Service AFTER sync: {ThemeService.CurrentTheme}, brand: {ThemeService.CurrentBrandTheme}"); - // Force UI refresh to reflect synced theme StateHasChanged(); diff --git a/4-Aquiis.SimpleStart/appsettings.Development.json b/4-Aquiis.SimpleStart/appsettings.Development.json index 3cacbdc..9d1ab69 100644 --- a/4-Aquiis.SimpleStart/appsettings.Development.json +++ b/4-Aquiis.SimpleStart/appsettings.Development.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" } }, "SessionTimeout": { diff --git a/4-Aquiis.SimpleStart/appsettings.json b/4-Aquiis.SimpleStart/appsettings.json index 0258005..b7535c3 100644 --- a/4-Aquiis.SimpleStart/appsettings.json +++ b/4-Aquiis.SimpleStart/appsettings.json @@ -7,7 +7,7 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning", "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.EntityFrameworkCore.Database.Command": "Information" + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" } }, "AllowedHosts": "*", diff --git a/5-Aquiis.Professional/Features/Calendar/Calendar.razor b/5-Aquiis.Professional/Features/Calendar/Calendar.razor index 4222083..59c094a 100644 --- a/5-Aquiis.Professional/Features/Calendar/Calendar.razor +++ b/5-Aquiis.Professional/Features/Calendar/Calendar.razor @@ -9,6 +9,7 @@ @using Microsoft.AspNetCore.Authorization +@using System.ComponentModel.DataAnnotations @attribute [OrganizationAuthorize("Owner", "Administrator", "PropertyManager")] @inject CalendarEventService CalendarEventService @inject CalendarSettingsService CalendarSettingsService @@ -502,43 +503,43 @@
- + @code { diff --git a/5-Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor index d8f7061..090151f 100644 --- a/5-Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/RegisterConfirmation.razor @@ -17,9 +17,13 @@ @if (emailConfirmationLink is not null) { -

+ @*

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

*@ +

+ This app does not currently integrate email, this is something we hope to consider in the future. + Click here to confirm your account

} else From 7e531367745de4690a306467f20093deb7af5a32 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Wed, 28 Jan 2026 08:05:09 -0600 Subject: [PATCH 8/8] simple start refactor --- .../Constants/ApplicationSettings.cs | 1 + .../Services/OrganizationService.cs | 26 +++++++++++++++- .../Properties/PropertyListView.razor | 15 +++++++--- .../Properties/PropertyListForm.razor | 4 +++ .../Organizations/Pages/ManageUsers.razor | 15 +++++++++- .../Components/Account/Pages/Register.razor | 25 +++++++--------- 4-Aquiis.SimpleStart/appsettings.json | 3 +- .../Components/Account/Pages/Register.razor | 25 +++++++--------- 5-Aquiis.Professional/appsettings.json | 3 +- .../NewSetupUITests.cs | 30 +++++++++---------- .../Layout/SharedMainLayoutTests.cs | 2 +- .../NewSetupUITests.cs | 22 +++++++------- 12 files changed, 108 insertions(+), 63 deletions(-) diff --git a/0-Aquiis.Core/Constants/ApplicationSettings.cs b/0-Aquiis.Core/Constants/ApplicationSettings.cs index e15454b..80fc7e8 100644 --- a/0-Aquiis.Core/Constants/ApplicationSettings.cs +++ b/0-Aquiis.Core/Constants/ApplicationSettings.cs @@ -9,6 +9,7 @@ public class ApplicationSettings public string Repository { get; set; } = string.Empty; public bool SoftDeleteEnabled { get; set; } public string SchemaVersion { get; set; } = "1.0.0"; + public int MaxOrganizationUsers { get; set; } = 0; // 0 = unlimited (Professional), 3 = SimpleStart limit } // Property & Tenant Lifecycle Enums diff --git a/2-Aquiis.Application/Services/OrganizationService.cs b/2-Aquiis.Application/Services/OrganizationService.cs index 6c7046d..cd6d59d 100644 --- a/2-Aquiis.Application/Services/OrganizationService.cs +++ b/2-Aquiis.Application/Services/OrganizationService.cs @@ -2,6 +2,7 @@ using Aquiis.Core.Constants; using Aquiis.Core.Interfaces.Services; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; namespace Aquiis.Application.Services { @@ -9,11 +10,16 @@ public class OrganizationService { private readonly ApplicationDbContext _dbContext; private readonly IUserContextService _userContext; + private readonly ApplicationSettings _settings; - public OrganizationService(ApplicationDbContext dbContext, IUserContextService userContextService) + public OrganizationService( + ApplicationDbContext dbContext, + IUserContextService userContextService, + IOptions settings) { _dbContext = dbContext; _userContext = userContextService; + _settings = settings.Value; } #region CRUD Operations @@ -289,6 +295,24 @@ public async Task GrantOrganizationAccessAsync(string userId, Guid organiz if (organization == null || organization.IsDeleted) return false; + // Check user limit for SimpleStart (MaxOrganizationUsers > 0) + if (_settings.MaxOrganizationUsers > 0) + { + var currentUserCount = await _dbContext.OrganizationUsers + .Where(ou => ou.OrganizationId == organizationId + && ou.UserId != ApplicationConstants.SystemUser.Id + && ou.IsActive + && !ou.IsDeleted) + .CountAsync(); + + if (currentUserCount >= _settings.MaxOrganizationUsers) + { + throw new InvalidOperationException( + $"User limit reached. SimpleStart allows maximum {_settings.MaxOrganizationUsers} user accounts (including system account). " + + "Upgrade to Aquiis Professional for unlimited users."); + } + } + // Check if user already has access var existing = await _dbContext.OrganizationUsers .FirstOrDefaultAsync(uo => uo.UserId == userId && uo.OrganizationId == organizationId); diff --git a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor index 6daeb2f..d36a091 100644 --- a/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor +++ b/3-Aquiis.UI.Shared/Components/Entities/Properties/PropertyListView.razor @@ -29,10 +29,11 @@ @if (ShowMetrics) { [Parameter] public bool SortAscending { get; set; } = true; + + /// + /// Total properties count (for metrics) + /// + [Parameter] + public int TotalCount { get; set; } /// /// Available properties count (for metrics) diff --git a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyListForm.razor b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyListForm.razor index 31d8513..a33aac1 100644 --- a/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyListForm.razor +++ b/3-Aquiis.UI.Shared/Features/PropertyManagement/Properties/PropertyListForm.razor @@ -59,6 +59,7 @@ else StatusFilter="@selectedPropertyStatus" SortColumn="@sortColumn" SortAscending="@sortAscending" + TotalCount="@totalCount" AvailableCount="@availableCount" PendingCount="@pendingCount" OccupiedCount="@occupiedCount" @@ -131,6 +132,8 @@ else private List pagedProperties = new(); private string searchTerm = string.Empty; private string selectedPropertyStatus = string.Empty; + + private int totalCount = 0; private int availableCount = 0; private int pendingCount = 0; private int occupiedCount = 0; @@ -214,6 +217,7 @@ else private void CalculateMetrics(){ if (filteredProperties != null) { + totalCount = filteredProperties.Count; 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); diff --git a/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor index e759109..164203c 100644 --- a/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor +++ b/4-Aquiis.SimpleStart/Features/Administration/Organizations/Pages/ManageUsers.razor @@ -58,10 +58,19 @@
Users with Access
-
+ @if (!CanAddMoreUsers) + { +
+ + User Limit Reached
+ SimpleStart allows 3 user accounts (1 system + 2 login users).
+ Upgrade to Aquiis Professional for unlimited users and advanced features. +
+ }
@if (!organizationUsers.Any()) { @@ -229,6 +238,10 @@ private string selectedRole = ApplicationConstants.OrganizationRoles.User; private string addUserError = string.Empty; + // SimpleStart: 3-user limit (excluding system account) + private bool CanAddMoreUsers => organizationUsers + .Count(ou => ou.UserId != ApplicationConstants.SystemUser.Id) < 3; + protected override async Task OnInitializedAsync() { await LoadOrganization(); diff --git a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Register.razor b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Register.razor index fcca3fe..bf2da7e 100644 --- a/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Register.razor +++ b/4-Aquiis.SimpleStart/Shared/Components/Account/Pages/Register.razor @@ -1,10 +1,7 @@ @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 @@ -324,21 +321,21 @@ else } } + // Auto-confirm email (no email integration yet) 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) + var confirmResult = await UserManager.ConfirmEmailAsync(user, code); + + if (!confirmResult.Succeeded) + { + Logger.LogWarning("Failed to auto-confirm email for user {UserId}: {Errors}", + userId, string.Join(", ", confirmResult.Errors.Select(e => e.Description))); + } + else { - RedirectManager.RedirectTo( - "Account/RegisterConfirmation", - new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl }); + Logger.LogInformation("Email auto-confirmed for user {UserId}", userId); } + // Sign in the user directly await SignInManager.SignInAsync(user, isPersistent: false); RedirectManager.RedirectTo(ReturnUrl); } diff --git a/4-Aquiis.SimpleStart/appsettings.json b/4-Aquiis.SimpleStart/appsettings.json index b7535c3..68fd678 100644 --- a/4-Aquiis.SimpleStart/appsettings.json +++ b/4-Aquiis.SimpleStart/appsettings.json @@ -20,7 +20,8 @@ "SoftDeleteEnabled": true, "DatabaseFileName": "app_v0.3.0.db", "PreviousDatabaseFileName": "app_v0.0.0.db", - "SchemaVersion": "0.3.0" + "SchemaVersion": "0.3.0", + "MaxOrganizationUsers": 3 }, "SessionTimeout": { "InactivityTimeoutMinutes": 18, diff --git a/5-Aquiis.Professional/Shared/Components/Account/Pages/Register.razor b/5-Aquiis.Professional/Shared/Components/Account/Pages/Register.razor index fcca3fe..bf2da7e 100644 --- a/5-Aquiis.Professional/Shared/Components/Account/Pages/Register.razor +++ b/5-Aquiis.Professional/Shared/Components/Account/Pages/Register.razor @@ -1,10 +1,7 @@ @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 @@ -324,21 +321,21 @@ else } } + // Auto-confirm email (no email integration yet) 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) + var confirmResult = await UserManager.ConfirmEmailAsync(user, code); + + if (!confirmResult.Succeeded) + { + Logger.LogWarning("Failed to auto-confirm email for user {UserId}: {Errors}", + userId, string.Join(", ", confirmResult.Errors.Select(e => e.Description))); + } + else { - RedirectManager.RedirectTo( - "Account/RegisterConfirmation", - new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl }); + Logger.LogInformation("Email auto-confirmed for user {UserId}", userId); } + // Sign in the user directly await SignInManager.SignInAsync(user, isPersistent: false); RedirectManager.RedirectTo(ReturnUrl); } diff --git a/5-Aquiis.Professional/appsettings.json b/5-Aquiis.Professional/appsettings.json index 0258005..40fc2ad 100644 --- a/5-Aquiis.Professional/appsettings.json +++ b/5-Aquiis.Professional/appsettings.json @@ -20,7 +20,8 @@ "SoftDeleteEnabled": true, "DatabaseFileName": "app_v0.3.0.db", "PreviousDatabaseFileName": "app_v0.0.0.db", - "SchemaVersion": "0.3.0" + "SchemaVersion": "0.3.0", + "MaxOrganizationUsers": 0 }, "SessionTimeout": { "InactivityTimeoutMinutes": 18, diff --git a/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs b/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs index 3ec25b8..0a75d07 100644 --- a/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs +++ b/6-Tests/Aquiis.UI.Professional.Tests/NewSetupUITests.cs @@ -31,7 +31,7 @@ public async Task CreateNewAccount() 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.Locator("select[id='Input.State']").SelectOptionAsync(new[] { "LA" }); + await Page.Locator("select[id='Input.State']").SelectOptionAsync(new[] { "TX" }); await Page.Locator("select[id='Input.State']").PressAsync("Tab"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).FillAsync("owner1@aquiis.com"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" }).PressAsync("Tab"); @@ -44,25 +44,25 @@ public async Task CreateNewAccount() 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.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.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.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.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.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.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"); diff --git a/6-Tests/Aquiis.UI.Shared.Tests/Components/Layout/SharedMainLayoutTests.cs b/6-Tests/Aquiis.UI.Shared.Tests/Components/Layout/SharedMainLayoutTests.cs index 0b32488..9dcaa56 100644 --- a/6-Tests/Aquiis.UI.Shared.Tests/Components/Layout/SharedMainLayoutTests.cs +++ b/6-Tests/Aquiis.UI.Shared.Tests/Components/Layout/SharedMainLayoutTests.cs @@ -132,7 +132,7 @@ public void SharedMainLayout_Has_Theme_Attribute() ); // Assert - cut.Markup.Should().Contain("data-theme=\"dark\""); + cut.Markup.Should().Contain("data-bs-theme=\"dark\""); } [Fact] diff --git a/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs b/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs index 8868384..6e84aac 100644 --- a/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs +++ b/6-Tests/Aquiis.UI.SimpleStart.Tests/NewSetupUITests.cs @@ -44,25 +44,25 @@ public async Task CreateNewAccount() 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.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.GetByRole(AriaRole.Link, new() { Name = "Click here to confirm your account" }).ClickAsync(); - await Page.WaitForSelectorAsync("h1:has-text('Confirm email')"); + //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.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.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.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");