diff --git a/Skinia/Coordinators/AnalysisListCoordinator.swift b/Skinia/Coordinators/AnalysisListCoordinator.swift index 4fde0bb..d829e25 100644 --- a/Skinia/Coordinators/AnalysisListCoordinator.swift +++ b/Skinia/Coordinators/AnalysisListCoordinator.swift @@ -36,8 +36,7 @@ final class AnalysisListCoordinator: NavigationCoordinator, ObservableObject { // MARK: - Navigation Methods func showAnalysisDetail(for photo: SkinLesionPhoto) { - // This method is now handled directly in AnalysisListView - print("🔍 Navigation handled by AnalysisListView for photo: \(photo.id)") + // Navigation handled directly in AnalysisListView } } diff --git a/Skinia/Views/AnalysisList/AnalysisDetailSheetState.swift b/Skinia/Views/AnalysisList/AnalysisDetailSheetState.swift index 09f28a3..da840a1 100644 --- a/Skinia/Views/AnalysisList/AnalysisDetailSheetState.swift +++ b/Skinia/Views/AnalysisList/AnalysisDetailSheetState.swift @@ -5,13 +5,11 @@ final class AnalysisDetailSheetState: ObservableObject { @Published var isShowing = false func showSheet(with photo: SkinLesionPhoto) { - print("🔍 SheetState: Setting photo \(photo.id) and showing sheet") selectedPhoto = photo isShowing = true } func hideSheet() { - print("🔍 SheetState: Hiding sheet and clearing photo") isShowing = false selectedPhoto = nil } diff --git a/Skinia/Views/AnalysisList/AnalysisDetailSheetView.swift b/Skinia/Views/AnalysisList/AnalysisDetailSheetView.swift new file mode 100644 index 0000000..7da6176 --- /dev/null +++ b/Skinia/Views/AnalysisList/AnalysisDetailSheetView.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct AnalysisDetailSheetView: View { + @ObservedObject var sheetState: AnalysisDetailSheetState + let dependencyContainer: DependencyContainer + + var body: some View { + Group { + if let photo = sheetState.selectedPhoto { + NavigationView { + AnalysisDetailView( + photo: photo, + photoRepository: dependencyContainer.photoRepository, + analysisService: dependencyContainer.analysisService + ) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Fechar") { + sheetState.hideSheet() + } + } + } + } + .navigationViewStyle(StackNavigationViewStyle()) + .environment(\.analysisService, dependencyContainer.analysisService) + .environment(\.notificationManager, dependencyContainer.notificationManager) + } else { + VStack(spacing: 12) { + Text("Error: Photo not found") + .foregroundColor(.red) + Button("Close") { + sheetState.hideSheet() + } + } + .padding() + } + } + } +} diff --git a/Skinia/Views/AnalysisList/AnalysisListSelectionActionsView.swift b/Skinia/Views/AnalysisList/AnalysisListSelectionActionsView.swift new file mode 100644 index 0000000..bf1aa2f --- /dev/null +++ b/Skinia/Views/AnalysisList/AnalysisListSelectionActionsView.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct AnalysisListSelectionActionsView: View { + @Binding var showingDeleteConfirmation: Bool + @Binding var activeAlert: AnalysisListAlertContext? + let onExportSelected: () -> AnalysisListAlertContext? + let onDeleteConfirmed: () -> Void + + var body: some View { + Menu { + Button("Exportar Selecionadas") { + activeAlert = onExportSelected() + } + + Button("Excluir Selecionadas", role: .destructive) { + HapticManager.shared.selection() + showingDeleteConfirmation = true + } + } label: { + Image(systemName: "ellipsis.circle") + } + .confirmationDialog( + "Confirmar exclusão", + isPresented: $showingDeleteConfirmation, + titleVisibility: .visible + ) { + Button("Excluir", role: .destructive) { + HapticManager.shared.impact(.medium) + showingDeleteConfirmation = false + onDeleteConfirmed() + } + + Button("Cancelar", role: .cancel) { + HapticManager.shared.selection() + } + } message: { + Text("Esta ação é irreversível. As análises selecionadas serão removidas permanentemente.") + } + } +} diff --git a/Skinia/Views/AnalysisList/AnalysisListStatisticsFormatter.swift b/Skinia/Views/AnalysisList/AnalysisListStatisticsFormatter.swift new file mode 100644 index 0000000..f39118f --- /dev/null +++ b/Skinia/Views/AnalysisList/AnalysisListStatisticsFormatter.swift @@ -0,0 +1,40 @@ +import Foundation + +enum AnalysisListStatisticsFormatter { + static func statisticsSummary(from photos: [SkinLesionPhoto]) -> String? { + guard !photos.isEmpty else { return nil } + + let total = photos.count + let completed = photos.completedCount + let pending = photos.pendingCount + let failed = photos.failedCount + let highRisk = photos.highRiskCount + + return """ + Estatísticas de Análises + Total de fotos: \(total) + Concluídas: \(completed) + Em andamento: \(pending) + Com erro: \(failed) + Alto ou urgente risco: \(highRisk) + """ + } +} + +private extension Array where Element == SkinLesionPhoto { + var completedCount: Int { + filter { $0.analysisStatus == .completed }.count + } + + var pendingCount: Int { + filter { $0.isPendingAnalysis }.count + } + + var failedCount: Int { + filter { $0.hasError }.count + } + + var highRiskCount: Int { + filter { $0.analysisResult?.riskLevel == .high || $0.analysisResult?.riskLevel == .urgent }.count + } +} diff --git a/Skinia/Views/AnalysisList/AnalysisListToolbarContent.swift b/Skinia/Views/AnalysisList/AnalysisListToolbarContent.swift new file mode 100644 index 0000000..1ccbeca --- /dev/null +++ b/Skinia/Views/AnalysisList/AnalysisListToolbarContent.swift @@ -0,0 +1,63 @@ +import SwiftUI + +struct AnalysisListToolbarContent: ToolbarContent { + @ObservedObject var selectionHelper: AnalysisListSelectionHelper + @Binding var showingSearchField: Bool + @Binding var showingFilterSheet: Bool + @Binding var showingHistoryOptions: Bool + @Binding var showingDeleteConfirmation: Bool + @Binding var activeAlert: AnalysisListAlertContext? + let isFilterActive: Bool + let onExportSelected: () -> AnalysisListAlertContext? + let onExportAll: (AnalysisExportFormat) -> AnalysisListAlertContext? + let onDeleteSelected: () -> Void + + var body: some ToolbarContent { + ToolbarItemGroup(placement: .navigationBarLeading) { + if selectionHelper.isSelectionMode { + Button("Cancelar") { + selectionHelper.clearSelection() + } + } + } + + ToolbarItemGroup(placement: .navigationBarTrailing) { + if selectionHelper.isSelectionMode { + AnalysisListSelectionActionsView( + showingDeleteConfirmation: $showingDeleteConfirmation, + activeAlert: $activeAlert, + onExportSelected: onExportSelected, + onDeleteConfirmed: onDeleteSelected + ) + } else { + Button { + showingSearchField.toggle() + } label: { + Image(systemName: "magnifyingglass") + } + + Button { + showingFilterSheet = true + } label: { + Image(systemName: isFilterActive ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle") + } + + Menu { + Button("Estatísticas") { + showingHistoryOptions = true + } + + Button("Exportar Todas") { + activeAlert = onExportAll(.pdf) + } + + Button("Selecionar") { + selectionHelper.enableSelectionMode() + } + } label: { + Image(systemName: "ellipsis.circle") + } + } + } + } +} diff --git a/Skinia/Views/Screens/AnalysisListView.swift b/Skinia/Views/Screens/AnalysisListView.swift index 1d4c9d5..e41c120 100644 --- a/Skinia/Views/Screens/AnalysisListView.swift +++ b/Skinia/Views/Screens/AnalysisListView.swift @@ -39,7 +39,10 @@ struct AnalysisListView: View { try PreviewPhotoFactory.seed(repository: coordinator.dependencyContainer.photoRepository) viewModel.loadPhotos() } catch { - print("Falha ao carregar dados de exemplo: \(error)") + activeAlert = AnalysisListAlertContext( + title: "Erro", + message: "Falha ao carregar dados de exemplo: \(error.localizedDescription)" + ) } } } else { @@ -48,62 +51,18 @@ struct AnalysisListView: View { } .navigationTitle("Análises") .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - if selectionHelper.isSelectionMode { - Button("Cancelar") { - selectionHelper.clearSelection() - } - } - } - - ToolbarItemGroup(placement: .navigationBarTrailing) { - if selectionHelper.isSelectionMode { - Menu { - Button("Exportar Selecionadas") { - exportSelectedPhotos() - } - - Button("Excluir Selecionadas", role: .destructive) { - HapticManager.shared.selection() - showingDeleteConfirmation = true - } - } label: { - Image(systemName: "ellipsis.circle") - } - } else { - Button { - showingSearchField.toggle() - } label: { - Image(systemName: "magnifyingglass") - } - - Button { - showingFilterSheet = true - } label: { - Image(systemName: viewModel.selectedStatusFilter != nil ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle") - } - - Menu { - Button("Estatísticas") { - showingHistoryOptions = true - } - - Button("Exportar Todas") { - activeAlert = exportAllPhotos() - } - - Button("Selecionar") { - if selectionHelper.isSelectionMode { - selectionHelper.clearSelection() - } else { - selectionHelper.enableSelectionMode() - } - } - } label: { - Image(systemName: "ellipsis.circle") - } - } - } + AnalysisListToolbarContent( + selectionHelper: selectionHelper, + showingSearchField: $showingSearchField, + showingFilterSheet: $showingFilterSheet, + showingHistoryOptions: $showingHistoryOptions, + showingDeleteConfirmation: $showingDeleteConfirmation, + activeAlert: $activeAlert, + isFilterActive: viewModel.selectedStatusFilter != nil, + onExportSelected: handleExportSelected, + onExportAll: handleExportAll(format:), + onDeleteSelected: handleDeleteSelected + ) } .refreshable { HapticManager.shared.impact(.light) @@ -122,7 +81,7 @@ struct AnalysisListView: View { viewModel: viewModel, activeAlert: $activeAlert, onExport: { format in - exportAllPhotos(format: format) + handleExportAll(format: format) }, onShareStatistics: { shareStatisticsSummary() @@ -139,22 +98,6 @@ struct AnalysisListView: View { } ) } - .confirmationDialog( - "Confirmar exclusão", - isPresented: $showingDeleteConfirmation, - titleVisibility: .visible - ) { - Button("Excluir", role: .destructive) { - HapticManager.shared.impact(.medium) - deleteSelectedPhotos() - } - - Button("Cancelar", role: .cancel) { - HapticManager.shared.selection() - } - } message: { - Text("Esta ação é irreversível. As análises selecionadas serão removidas permanentemente.") - } } .onAppear { viewModel.loadPhotos() @@ -180,43 +123,10 @@ struct AnalysisListView: View { } } .sheet(isPresented: $sheetState.isShowing) { - let _ = print("🔍 Sheet building - isShowing: \(sheetState.isShowing), selectedPhoto: \(sheetState.selectedPhoto?.id.uuidString ?? "nil")") - - if let photoToShow = sheetState.selectedPhoto { - let _ = print("🔍 Sheet presenting AnalysisDetailView for photo: \(photoToShow.id)") - NavigationView { - AnalysisDetailView( - photo: photoToShow, - photoRepository: coordinator.dependencyContainer.photoRepository, - analysisService: coordinator.dependencyContainer.analysisService - ) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Fechar") { - print("🔍 Closing sheet") - sheetState.hideSheet() - } - } - } - .onAppear { - print("🔍 Sheet AnalysisDetailView appeared for photo: \(photoToShow.id)") - } - } - .navigationViewStyle(StackNavigationViewStyle()) - .environment(\.analysisService, coordinator.dependencyContainer.analysisService) - .environment(\.notificationManager, coordinator.dependencyContainer.notificationManager) - } else { - let _ = print("🔍 Sheet presenting but selectedPhoto is nil - creating placeholder") - VStack { - Text("Error: Photo not found") - .foregroundColor(.red) - Button("Close") { - sheetState.hideSheet() - } - } - .padding() - } + AnalysisDetailSheetView( + sheetState: sheetState, + dependencyContainer: coordinator.dependencyContainer + ) } } @@ -285,22 +195,21 @@ struct AnalysisListView: View { if selectionHelper.isSelectionMode { selectionHelper.toggleSelection(for: photo.id) } else { - print("🔍 Card tapped - showing detail for photo: \(photo.id)") sheetState.showSheet(with: photo) } } } } - private func exportSelectedPhotos() { - activeAlert = selectionHelper.exportSelectedPhotos( + private func handleExportSelected() -> AnalysisListAlertContext? { + selectionHelper.exportSelectedPhotos( using: viewModel, exportService: exportService, shareSheetPresenter: shareSheetPresenter ) } - private func exportAllPhotos( + private func handleExportAll( format: AnalysisExportFormat = .pdf, emptyMessage: String = "Nenhuma foto disponível para exportação." ) -> AnalysisListAlertContext? { @@ -313,35 +222,18 @@ struct AnalysisListView: View { ) } - private func deleteSelectedPhotos() { + private func handleDeleteSelected() { selectionHelper.deleteSelectedPhotos(using: viewModel) } private func shareStatisticsSummary() -> AnalysisListAlertContext? { - let photos = viewModel.getAllPhotos - - guard !photos.isEmpty else { + guard let summary = AnalysisListStatisticsFormatter.statisticsSummary(from: viewModel.getAllPhotos) else { return AnalysisListAlertContext( title: "Sem dados", message: "Cadastre análises para compartilhar estatísticas." ) } - let total = photos.count - let completed = photos.filter { $0.analysisStatus == .completed }.count - let pending = photos.filter { $0.isPendingAnalysis }.count - let failed = photos.filter { $0.hasError }.count - let highRisk = photos.filter { $0.analysisResult?.riskLevel == .high || $0.analysisResult?.riskLevel == .urgent }.count - - let summary = """ - Estatísticas de Análises - Total de fotos: \(total) - Concluídas: \(completed) - Em andamento: \(pending) - Com erro: \(failed) - Alto ou urgente risco: \(highRisk) - """ - shareSheetPresenter.present(items: [summary]) return nil }