import SwiftUI import ComposeApp struct ResidenceDetailView: View { let residenceId: Int32 @StateObject private var viewModel = ResidenceViewModel() @StateObject private var taskViewModel = TaskViewModel() @ObservedObject private var dataManager = DataManagerObservable.shared // Use TaskViewModel's state instead of local state private var tasksResponse: TaskColumnsResponse? { taskViewModel.tasksResponse } private var isLoadingTasks: Bool { taskViewModel.isLoadingTasks } private var tasksError: String? { taskViewModel.tasksError } @State private var contractors: [ContractorSummary] = [] @State private var isLoadingContractors = false @State private var contractorsError: String? @State private var showAddTask = false @State private var showEditResidence = false @State private var showEditTask = false @State private var showManageUsers = false @State private var selectedTaskForEdit: TaskResponse? @State private var selectedTaskForComplete: TaskResponse? @State private var selectedTaskForArchive: TaskResponse? @State private var showArchiveConfirmation = false @State private var hasAppeared = false @State private var showReportAlert = false @State private var showReportConfirmation = false @State private var showDeleteConfirmation = false @State private var isDeleting = false @State private var showingUpgradePrompt = false @State private var upgradeTriggerKey = "" @StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared @Environment(\.dismiss) private var dismiss // Check if current user is the owner of the residence private func isCurrentUserOwner(of residence: ResidenceResponse) -> Bool { guard let currentUser = dataManager.currentUser else { return false } return Int(residence.ownerId) == Int(currentUser.id) } var body: some View { ZStack { WarmGradientBackground() mainContent } .navigationBarTitleDisplayMode(.inline) .toolbar { leadingToolbar trailingToolbar } // MARK: Alerts .alert(L10n.Residences.generateReport, isPresented: $showReportConfirmation) { Button(L10n.Common.cancel, role: .cancel) { showReportConfirmation = false } Button(L10n.Residences.generate) { viewModel.generateTasksReport(residenceId: residenceId, email: "") showReportConfirmation = false } } message: { Text(L10n.Residences.generateReportMessage) } .alert(L10n.Residences.deleteTitle, isPresented: $showDeleteConfirmation) { Button(L10n.Common.cancel, role: .cancel) { } .accessibilityIdentifier(AccessibilityIdentifiers.Alert.cancelButton) Button(L10n.Common.delete, role: .destructive) { deleteResidence() } .accessibilityIdentifier(AccessibilityIdentifiers.Alert.deleteButton) } message: { if let residence = viewModel.selectedResidence { Text("\(L10n.Residences.deleteConfirmMessage)") } } .alert(L10n.Residences.maintenanceReport, isPresented: $showReportAlert) { Button(L10n.Common.ok, role: .cancel) { } } message: { Text(viewModel.reportMessage ?? "") } // MARK: Sheets .sheet(isPresented: $showAddTask) { AddTaskView(residenceId: residenceId, isPresented: $showAddTask) } .sheet(isPresented: $showEditResidence) { if let residence = viewModel.selectedResidence { EditResidenceView(residence: residence, isPresented: $showEditResidence) } } .sheet(isPresented: $showEditTask) { if let task = selectedTaskForEdit { EditTaskView(task: task, isPresented: $showEditTask) } } .sheet(item: $selectedTaskForComplete) { task in CompleteTaskView(task: task) { updatedTask in print("DEBUG: onComplete callback called") print("DEBUG: updatedTask is nil: \(updatedTask == nil)") if let updatedTask = updatedTask { print("DEBUG: updatedTask.id = \(updatedTask.id)") print("DEBUG: updatedTask.kanbanColumn = \(updatedTask.kanbanColumn ?? "nil")") updateTaskInKanban(updatedTask) } selectedTaskForComplete = nil } } .sheet(isPresented: $showManageUsers) { if let residence = viewModel.selectedResidence { ManageUsersView( residenceId: residence.id, residenceName: residence.name, isPrimaryOwner: isCurrentUserOwner(of: residence), residenceOwnerId: residence.ownerId, residence: residence ) } } .alert("Archive Task", isPresented: $showArchiveConfirmation) { Button("Cancel", role: .cancel) { selectedTaskForArchive = nil } Button("Archive", role: .destructive) { if let task = selectedTaskForArchive { taskViewModel.archiveTask(id: task.id) { _ in loadResidenceTasks() } selectedTaskForArchive = nil } } } message: { if let task = selectedTaskForArchive { Text("Are you sure you want to archive \"\(task.title)\"? You can unarchive it later from archived tasks.") } } .sheet(isPresented: $showingUpgradePrompt) { UpgradePromptView(triggerKey: upgradeTriggerKey.isEmpty ? "add_11th_task" : upgradeTriggerKey, isPresented: $showingUpgradePrompt) } // MARK: onChange & lifecycle .onChange(of: viewModel.reportMessage) { message in if message != nil { showReportAlert = true } } .onChange(of: viewModel.selectedResidence) { residence in if residence != nil { hasAppeared = true } } .onChange(of: showAddTask) { isShowing in if !isShowing { loadResidenceTasks(forceRefresh: true) } } .onChange(of: showEditResidence) { isShowing in if !isShowing { loadResidenceData() } } .onChange(of: showEditTask) { isShowing in if !isShowing { loadResidenceTasks(forceRefresh: true) } } .onAppear { loadResidenceData() } .handleErrors( error: viewModel.errorMessage, onRetry: { loadResidenceData() } ) } } // MARK: - Main Content private extension ResidenceDetailView { @ViewBuilder var mainContent: some View { if !hasAppeared || viewModel.isLoading { loadingView } else if let residence = viewModel.selectedResidence { contentView(for: residence) } } var loadingView: some View { StandardLoadingView(message: L10n.Residences.loadingResidence) } @ViewBuilder func contentView(for residence: ResidenceResponse) -> some View { ScrollView(showsIndicators: false) { VStack(spacing: OrganicSpacing.comfortable) { PropertyHeaderCard(residence: residence) .padding(.horizontal, 16) .padding(.top, 8) tasksSection .padding(.horizontal, 16) contractorsSection .padding(.horizontal, 16) } .padding(.bottom, OrganicSpacing.airy) } } @ViewBuilder var tasksSection: some View { if let tasksResponse = tasksResponse { TasksSectionContainer( tasksResponse: tasksResponse, taskViewModel: taskViewModel, selectedTaskForEdit: $selectedTaskForEdit, showEditTask: $showEditTask, selectedTaskForComplete: $selectedTaskForComplete, selectedTaskForArchive: $selectedTaskForArchive, showArchiveConfirmation: $showArchiveConfirmation, reloadTasks: { loadResidenceTasks(forceRefresh: true) } ) } else if isLoadingTasks { ProgressView(L10n.Residences.loadingTasks) } else if let tasksError = tasksError { Text("\(L10n.Residences.errorLoadingTasks): \(tasksError)") .foregroundColor(Color.appError) .padding() } } @ViewBuilder var contractorsSection: some View { VStack(alignment: .leading, spacing: 16) { // Section Header HStack(alignment: .center, spacing: 12) { ZStack { Circle() .fill(Color.appPrimary.opacity(0.12)) .frame(width: 40, height: 40) Image(systemName: "person.2.fill") .font(.system(size: 16, weight: .semibold)) .foregroundColor(Color.appPrimary) } Text(L10n.Residences.contractors) .font(.system(size: 20, weight: .bold, design: .rounded)) .foregroundColor(Color.appTextPrimary) Spacer() } .padding(.top, 8) if isLoadingContractors { HStack { Spacer() ProgressView() .tint(Color.appPrimary) Spacer() } .padding(OrganicSpacing.cozy) } else if let error = contractorsError { Text("\(L10n.Common.error): \(error)") .foregroundColor(Color.appError) .padding() } else if contractors.isEmpty { // Empty state with organic styling OrganicEmptyState( icon: "person.crop.circle.badge.plus", title: L10n.Residences.noContractors, subtitle: L10n.Residences.addContractorsPrompt, blobVariation: 1 ) } else { // Contractors list VStack(spacing: 12) { ForEach(contractors, id: \.id) { contractor in NavigationLink(destination: ContractorDetailView(contractorId: contractor.id)) { ContractorCard( contractor: contractor, onToggleFavorite: { // Could implement toggle favorite here if needed } ) } .buttonStyle(OrganicCardButtonStyle()) } } } } } } // MARK: - Organic Card Button Style private struct OrganicCardButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.98 : 1.0) .opacity(configuration.isPressed ? 0.9 : 1.0) .animation(.easeOut(duration: 0.15), value: configuration.isPressed) } } // MARK: - Toolbars private extension ResidenceDetailView { @ToolbarContentBuilder var leadingToolbar: some ToolbarContent { ToolbarItem(placement: .navigationBarLeading) { if viewModel.selectedResidence != nil { Button(L10n.Common.edit) { showEditResidence = true } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.editButton) } } } @ToolbarContentBuilder var trailingToolbar: some ToolbarContent { ToolbarItemGroup(placement: .navigationBarTrailing) { if viewModel.selectedResidence != nil { Button { showReportConfirmation = true } label: { if viewModel.isGeneratingReport { ProgressView() } else { Image(systemName: "doc.text") } } .disabled(viewModel.isGeneratingReport) } // Manage Users button (owner only) - includes share code generation and easy share if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) { Button { if subscriptionCache.canShareResidence() { showManageUsers = true } else { upgradeTriggerKey = "share_residence" showingUpgradePrompt = true } } label: { Image(systemName: "person.2") } } Button { // Check LIVE task count before adding let totalTasks = tasksResponse?.columns.reduce(0) { $0 + $1.tasks.count } ?? 0 if subscriptionCache.shouldShowUpgradePrompt(currentCount: totalTasks, limitKey: "tasks") { upgradeTriggerKey = "add_11th_task" showingUpgradePrompt = true } else { showAddTask = true } } label: { Image(systemName: "plus") } .accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton) if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) { Button { showDeleteConfirmation = true } label: { Image(systemName: "trash") .foregroundStyle(Color.appError) } .accessibilityIdentifier(AccessibilityIdentifiers.Residence.deleteButton) } } } } // MARK: - Data Loading private extension ResidenceDetailView { func loadResidenceData() { viewModel.getResidence(id: residenceId) loadResidenceTasks() loadResidenceContractors() } func loadResidenceTasks(forceRefresh: Bool = false) { taskViewModel.loadTasks(residenceId: residenceId, forceRefresh: forceRefresh) } func updateTaskInKanban(_ updatedTask: TaskResponse) { taskViewModel.updateTaskInKanban(updatedTask) } func deleteResidence() { guard TokenStorage.shared.getToken() != nil else { return } isDeleting = true Task { do { let result = try await APILayer.shared.deleteResidence( id: Int32(Int(residenceId)) ) await MainActor.run { self.isDeleting = false if result is ApiResultSuccess { dismiss() } else if let errorResult = result as? ApiResultError { self.viewModel.errorMessage = ErrorMessageParser.parse(errorResult.message) } else { self.viewModel.errorMessage = "Failed to delete residence" } } } catch { await MainActor.run { self.isDeleting = false self.viewModel.errorMessage = ErrorMessageParser.parse(error.localizedDescription) } } } } func loadResidenceContractors() { guard TokenStorage.shared.getToken() != nil else { return } isLoadingContractors = true contractorsError = nil Task { do { let result = try await APILayer.shared.getContractorsByResidence( residenceId: Int32(Int(residenceId)), forceRefresh: false ) await MainActor.run { if let successResult = result as? ApiResultSuccess { self.contractors = (successResult.data as? [ContractorSummary]) ?? [] self.isLoadingContractors = false } else if let errorResult = result as? ApiResultError { self.contractorsError = errorResult.message self.isLoadingContractors = false } else { self.contractorsError = "Failed to load contractors" self.isLoadingContractors = false } } } catch { await MainActor.run { self.contractorsError = ErrorMessageParser.parse(error.localizedDescription) self.isLoadingContractors = false } } } } } private struct TasksSectionContainer: View { let tasksResponse: TaskColumnsResponse @ObservedObject var taskViewModel: TaskViewModel @Binding var selectedTaskForEdit: TaskResponse? @Binding var showEditTask: Bool @Binding var selectedTaskForComplete: TaskResponse? @Binding var selectedTaskForArchive: TaskResponse? @Binding var showArchiveConfirmation: Bool let reloadTasks: () -> Void var body: some View { TasksSection( tasksResponse: tasksResponse, onEditTask: { task in selectedTaskForEdit = task showEditTask = true }, onCancelTask: { task in taskViewModel.cancelTask(id: task.id) { _ in reloadTasks() } }, onUncancelTask: { taskId in taskViewModel.uncancelTask(id: taskId) { _ in reloadTasks() } }, onMarkInProgress: { taskId in taskViewModel.markInProgress(id: taskId) { success in if success { reloadTasks() } } }, onCompleteTask: { task in selectedTaskForComplete = task }, onArchiveTask: { task in selectedTaskForArchive = task showArchiveConfirmation = true }, onUnarchiveTask: { taskId in taskViewModel.unarchiveTask(id: taskId) { _ in reloadTasks() } } ) } } // MARK: - Preview struct ResidenceDetailView_Previews: PreviewProvider { static var previews: some View { NavigationView { ResidenceDetailView(residenceId: 1) } } }