import SwiftUI struct DashboardView: View { @StateObject private var viewModel = DashboardViewModel() @State private var isShowingMonthlyHistory = false @State private var isShowingLogoutConfirmation = false var body: some View { ZStack { BouncingCirclesBackground(numberOfCircles: 6, blurStrength: 60) .opacity(0.3) .ignoresSafeArea() VStack(alignment: .leading, spacing: 20) { topBar patientHeader reminderBanner if viewModel.isLoading && viewModel.medicationCards.isEmpty { ProgressView() .frame(maxWidth: .infinity) .padding(.top, 40) } else if let errorMessage = viewModel.errorMessage { errorState(errorMessage) } else if viewModel.medicationCards.isEmpty { Text("Nao ha medicacao ativa para mostrar.") .font(.jakartaRegular16) .foregroundStyle(Color.appTextPrimary.opacity(0.65)) .padding(.horizontal, 20) } else { medicationCarousel .frame(maxHeight: .infinity) } } .padding(.top, 12) } .navigationBarHidden(true) .task { await viewModel.loadDashboard() } .refreshable { await viewModel.loadDashboard() } .sheet(isPresented: $isShowingMonthlyHistory) { PatientMonthlyHistorySheet() .presentationBackground(.ultraThinMaterial) } .alert("Terminar sessao?", isPresented: $isShowingLogoutConfirmation) { Button("Cancelar", role: .cancel) {} Button("Terminar sessao", role: .destructive) { Task { await viewModel.logout() } } } message: { Text("Terá de iniciar sessao novamente para consultar a sua medicacao.") } } // MARK: - Top bar private var topBar: some View { HStack { Image("timerIcon") .foregroundStyle(Color.appBackground) .frame(width: 32, height: 32) Spacer() HStack(spacing: 6) { Image("bluetoothIcon") .foregroundStyle(Color.appTextPrimary.opacity(0.55)) Circle() .fill(.green) .frame(width: 16, height: 16) } Button { isShowingLogoutConfirmation = true } label: { Image(systemName: "power") .font(.system(size: 15, weight: .semibold)) .foregroundStyle(Color.appTextPrimary.opacity(0.52)) .frame(width: 32, height: 32) .background(.white.opacity(0.45)) .clipShape(Circle()) } .buttonStyle(.plain) .accessibilityLabel("Terminar sessao") } .padding(.horizontal, 32) if let logoutErrorMessage = viewModel.logoutErrorMessage { Text(logoutErrorMessage) .font(.jakartaRegular12) .foregroundStyle(.red) .frame(maxWidth: .infinity) } } // MARK: - Patient header // Mock adherence breakdown until the backend exposes real dose stats. private let mockAdherenceSegments: [RadialRingSegment] = [ RadialRingSegment(percentage: 65, color: .green), RadialRingSegment(percentage: 20, color: .orange), RadialRingSegment(percentage: 15, color: .red) ] private var patientHeader: some View { VStack(spacing: 6) { Button { isShowingMonthlyHistory = true } label: { ZStack { RadialSegmentedRingView(segments: mockAdherenceSegments, lineWidth: 5, gapDegrees: 8) .frame(width: 92, height: 92) patientAvatar .frame(width: 78, height: 78) .clipShape(Circle()) } } .buttonStyle(.plain) .accessibilityLabel("Ver histórico mensal") Text(viewModel.patient?.name ?? "Doente") .font(.lineSeedBold22) .foregroundStyle(Color.appAccent) if let age = viewModel.patientAge { Text("\(age) Anos") .font(.jakartaRegular14) .foregroundStyle(Color.appTextPrimary.opacity(0.55)) } Text("🙂") .font(.system(size: 20)) .padding(.top, 2) } .frame(maxWidth: .infinity) } @ViewBuilder private var patientAvatar: some View { if let photo = viewModel.patient?.photo, let image = decodedImage(from: photo) { image.resizable().scaledToFill() } else { Color.appBorder .overlay { Image(systemName: "person.fill") .resizable() .scaledToFit() .frame(width: 32, height: 32) .foregroundStyle(Color.appTextPrimary.opacity(0.4)) } } } // MARK: - Reminder banner private var reminderBanner: some View { VStack(spacing: 4) { Text("Não se esqueça!") .font(.jakartaBold16) .foregroundStyle(Color.appAccent) Group { Text("Tem ") + Text("\(viewModel.totalDosesToday) Comprimidos") .fontWeight(.bold) + Text(" para tomar hoje.") } .font(.jakartaRegular14) .foregroundStyle(Color.appAccent) } .frame(maxWidth: .infinity) .multilineTextAlignment(.center) } // MARK: - Medication carousel private var medicationCarousel: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { ForEach(viewModel.medicationCards) { card in MedicationCardView(card: card) .frame(width: 260) .frame(maxHeight: .infinity) } } .padding(.horizontal, 20) .padding(.bottom, 20) .frame(maxHeight: .infinity) } } private func errorState(_ message: String) -> some View { VStack(alignment: .leading, spacing: 12) { Text(message) .font(.jakartaRegular16) .foregroundStyle(.red) Button("Tentar novamente") { Task { await viewModel.loadDashboard() } } .font(.jakartaBold16) .foregroundStyle(Color.appBackground) } .padding(.horizontal, 20) } private func decodedImage(from base64String: String) -> Image? { guard let data = Data(base64Encoded: base64String), let uiImage = UIImage(data: data) else { return nil } return Image(uiImage: uiImage) } } private struct MedicationCardView: View { let card: MedicationCard var body: some View { VStack(spacing: 14) { medicationImage .frame(height: 140) .frame(maxWidth: .infinity) .background(Color.appSecondary) .clipShape(RoundedRectangle(cornerRadius: 12)) .overlay(alignment: .topTrailing) { Image(systemName: "info.circle.fill") .foregroundStyle(Color.appBackground) .padding(8) .background(.white) .clipShape(Circle()) .padding(8) } Text(card.name) .font(.lineSeedBold20) .foregroundStyle(Color.appAccent) .multilineTextAlignment(.center) Text(card.tag) .font(.jakartaMedium14) .foregroundStyle(.white) .padding(.horizontal, 14) .padding(.vertical, 6) .background(Color.appBackground) .clipShape(Capsule()) if let nextDoseTime = card.nextDoseTime { Group { Text("Próxima toma às ") + Text("\(nextDoseTime)h").fontWeight(.bold) } .font(.jakartaRegular14) .foregroundStyle(Color.appBackground) } Divider() doseTimesGrid Spacer(minLength: 0) } .padding(16) .frame(maxHeight: .infinity) .background(Color.appBackgroundLight.opacity(0.11)) .clipShape(RoundedRectangle(cornerRadius: 16)) .shadow(color: .black.opacity(0.06), radius: 8, y: 4) } @ViewBuilder private var medicationImage: some View { if let photoBase64 = card.photoBase64, let data = Data(base64Encoded: photoBase64), let uiImage = UIImage(data: data) { Image(uiImage: uiImage) .resizable() .scaledToFill() .clipped() } else { Image(systemName: "pills.fill") .font(.system(size: 40)) .foregroundStyle(Color.appBackground.opacity(0.5)) } } private var doseTimesGrid: some View { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) { ForEach(card.doseTimes, id: \.self) { time in HStack(spacing: 6) { Image(systemName: time == card.nextDoseTime ? "bell.fill" : "clock") .font(.system(size: 13)) Text("\(time)") .font(time == card.nextDoseTime ? .jakartaBold14 : .jakartaRegular14) } .foregroundStyle( time == card.nextDoseTime ? Color.appBackground : Color.appTextPrimary.opacity(0.4) ) } } } } #Preview { DashboardView() }