Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Skinia/Views/AnalysisList/AnalysisDetailSheetState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import SwiftUI

final class AnalysisDetailSheetState: ObservableObject {
@Published var selectedPhoto: SkinLesionPhoto?
@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
}
}
25 changes: 25 additions & 0 deletions Skinia/Views/AnalysisList/AnalysisListEmptyStateView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SwiftUI

struct AnalysisListEmptyStateView: View {
let onLoadSampleData: () -> Void

private let instructionsText = "Capture uma foto da lesão ou importe uma imagem da galeria para iniciar uma nova análise e acompanhar os resultados aqui."

var body: some View {
#if DEBUG
EnhancedEmptyStateView(
icon: "photo.stack",
title: "Nenhuma Análise Encontrada",
subtitle: instructionsText,
actionTitle: "Carregar Dados de Exemplo",
action: onLoadSampleData
)
#else
EnhancedEmptyStateView(
icon: "photo.stack",
title: "Nenhuma Análise Encontrada",
subtitle: instructionsText
)
#endif
}
}
47 changes: 47 additions & 0 deletions Skinia/Views/AnalysisList/AnalysisListFilterSheet.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import SwiftUI

struct AnalysisListFilterSheet: View {
let viewModel: AnalysisListViewModel
@Environment(\.dismiss) private var dismiss

var body: some View {
NavigationView {
List {
Section("Filtrar por Status") {
ForEach([AnalysisStatus?.none] + viewModel.statusFilterOptions.map { Optional($0) }, id: \.self) { status in
HStack {
if let status = status {
StatusBadge(status: status)
} else {
Text("Todos")
.fontWeight(.medium)
}

Spacer()

if viewModel.selectedStatusFilter == status {
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
.contentShape(Rectangle())
.onTapGesture {
viewModel.setStatusFilter(status)
dismiss()
}
}
}
}
.navigationTitle("Filtros")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Concluído") {
dismiss()
}
}
}
}
.presentationDetents([.medium])
}
}
186 changes: 186 additions & 0 deletions Skinia/Views/AnalysisList/AnalysisListHistoryOptionsSheet.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import SwiftUI

struct AnalysisListHistoryOptionsSheet: View {
let viewModel: AnalysisListViewModel
@Binding var activeAlert: AnalysisListAlertContext?
let onExport: (AnalysisExportFormat) -> AnalysisListAlertContext?
let onShareStatistics: () -> AnalysisListAlertContext?
@Environment(\.dismiss) private var dismiss

var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 24) {
statisticsSection
dateRangeSection
riskDistributionSection
exportOptionsSection
}
.padding()
}
.navigationTitle("Estatísticas do Histórico")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Concluído") {
dismiss()
}
}
}
}
.presentationDetents([.large])
}

private var statisticsSection: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Resumo Geral")
.font(.title2)
.fontWeight(.bold)

LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 16) {
AnalysisListStatCard(
title: "Total de Análises",
value: "\(viewModel.getAllPhotos.count)",
icon: "photo.stack",
color: .blue
)

AnalysisListStatCard(
title: "Concluídas",
value: "\(viewModel.getAllPhotos.filter { $0.analysisStatus == .completed }.count)",
icon: "checkmark.circle.fill",
color: .green
)

AnalysisListStatCard(
title: "Em Análise",
value: "\(viewModel.getAllPhotos.filter { $0.analysisStatus == .analyzing }.count)",
icon: "brain.head.profile",
color: .orange
)

AnalysisListStatCard(
title: "Falharam",
value: "\(viewModel.getAllPhotos.filter { $0.analysisStatus == .failed }.count)",
icon: "exclamationmark.triangle.fill",
color: .red
)
}
}
}

private var dateRangeSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Análise por Período")
.font(.headline)

if let oldestPhoto = viewModel.getAllPhotos.min(by: { $0.captureDate < $1.captureDate }),
let newestPhoto = viewModel.getAllPhotos.max(by: { $0.captureDate < $1.captureDate }) {
HStack {
VStack(alignment: .leading) {
Text("Primeira Análise")
.font(.caption)
.foregroundColor(.secondary)
Text(oldestPhoto.formattedCaptureDate)
.font(.subheadline)
.fontWeight(.medium)
}

Spacer()

VStack(alignment: .trailing) {
Text("Última Análise")
.font(.caption)
.foregroundColor(.secondary)
Text(newestPhoto.formattedCaptureDate)
.font(.subheadline)
.fontWeight(.medium)
}
}
.padding()
.background(DesignSystem.Colors.backgroundSecondary)
.cornerRadius(12)
}
}
}

private var riskDistributionSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Distribuição de Risco")
.font(.headline)

let completedPhotos = viewModel.getAllPhotos.filter { $0.analysisResult != nil }

if !completedPhotos.isEmpty {
VStack(spacing: 8) {
ForEach([RiskLevel.low, .moderate, .high, .urgent], id: \.self) { riskLevel in
let count = completedPhotos.filter { $0.analysisResult?.riskLevel == riskLevel }.count
let percentage = count > 0 ? Double(count) / Double(completedPhotos.count) : 0.0

HStack {
RiskBadge(riskLevel: riskLevel)

Spacer()

Text("\(count)")
.font(.subheadline)
.fontWeight(.semibold)

Text("(\(Int(percentage * 100))%)")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
}
.padding()
.background(DesignSystem.Colors.backgroundSecondary)
.cornerRadius(12)
} else {
Text("Nenhuma análise concluída disponível")
.font(.subheadline)
.foregroundColor(.secondary)
.italic()
}
}
}

private var exportOptionsSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Opções de Exportação")
.font(.headline)

VStack(spacing: 8) {
Button("Exportar Relatório Completo (PDF)") {
activeAlert = onExport(.pdf)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(12)

Button("Exportar Dados CSV") {
activeAlert = onExport(.csv)
}
.frame(maxWidth: .infinity)
.padding()
.background(DesignSystem.Colors.backgroundSecondary)
.foregroundColor(.primary)
.cornerRadius(12)

Button("Compartilhar Estatísticas") {
activeAlert = onShareStatistics()
}
.frame(maxWidth: .infinity)
.padding()
.background(DesignSystem.Colors.backgroundSecondary)
.foregroundColor(.primary)
.cornerRadius(12)
}
}
}
}
41 changes: 41 additions & 0 deletions Skinia/Views/AnalysisList/AnalysisListLoadingView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftUI

struct AnalysisListLoadingView: View {
var body: some View {
VStack(spacing: DesignSystem.Spacing.lg) {
PulsatingCircle(
color: DesignSystem.Colors.primary,
size: 60
)

VStack(spacing: DesignSystem.Spacing.sm) {
Text("Carregando análises...")
.font(DesignSystem.Typography.headline)
.foregroundColor(DesignSystem.Colors.text)

LoadingDots()
}

VStack(spacing: DesignSystem.Spacing.md) {
ForEach(0..<3) { _ in
VStack(spacing: DesignSystem.Spacing.sm) {
HStack {
SkeletonLoader(height: 60, cornerRadius: DesignSystem.CornerRadius.sm)
.frame(width: 60)

VStack(spacing: DesignSystem.Spacing.xs) {
SkeletonLoader(height: 16)
SkeletonLoader(height: 12)
SkeletonLoader(height: 10)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.cardStyle()
}
}
.padding(.top, DesignSystem.Spacing.lg)
}
.padding(DesignSystem.Spacing.lg)
}
}
Loading