import SwiftUI struct DashboardView: View { @StateObject private var viewModel = DashboardViewModel() var body: some View { ZStack { Color.appSecondary.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 } Spacer(minLength: 0) } .padding(.top, 12) } .navigationBarHidden(true) .task { await viewModel.loadDashboard() } .refreshable { await viewModel.loadDashboard() } } // MARK: - Top bar private var topBar: some View { HStack { Circle() .fill(Color.appBackground) .frame(width: 40, height: 40) .overlay { Image(systemName: "person.fill") .foregroundStyle(.white) } Spacer() HStack(spacing: 6) { Image(systemName: "dot.radiowaves.left.and.right") .foregroundStyle(Color.appTextPrimary.opacity(0.55)) Circle() .fill(.green) .frame(width: 8, height: 8) } } .padding(.horizontal, 20) } // MARK: - Patient header private var patientHeader: some View { VStack(spacing: 6) { ZStack { Circle() .stroke(Color.green, lineWidth: 3) .frame(width: 92, height: 92) patientAvatar .frame(width: 78, height: 78) .clipShape(Circle()) } Text(viewModel.patient?.name ?? "Doente") .font(.jakartaBold18) .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") .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) } } .padding(.horizontal, 20) .padding(.bottom, 20) } } 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(.jakartaBold16) .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 } .padding(16) .background(.white) .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() }