// // Copyright 2026 Novaloop AG // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Foundation enum HealthChatProfileEditScreenViewModelAction { case dismiss case displayCameraPicker case displayMediaPicker case displayFilePicker } struct HealthChatProfileEditScreenViewState: BindableState { var initialProfile: HealthChatMember? var loadFailed = false var canEditAvatar = false var selectedAvatarURL: URL? var localMedia: MediaInfo? var bindings = HealthChatProfileEditScreenViewStateBindings() var avatarDidChange: Bool { localMedia != nil || selectedAvatarURL != initialProfile?.avatarURL } /// The changed text fields as a partial update, nil when nothing changed. var fieldsUpdate: HealthChatProfileUpdate? { guard let initialProfile else { return nil } var update = HealthChatProfileUpdate() // nil leaves a field unchanged server-side, an empty string clears it. func diff(_ binding: String, _ initialValue: String?) -> String? { let value = binding.trimmingCharacters(in: .whitespacesAndNewlines) return value == (initialValue ?? "") ? nil : value } update.title = diff(bindings.title, initialProfile.title) update.firstName = diff(bindings.firstName, initialProfile.firstName) update.lastName = diff(bindings.lastName, initialProfile.lastName) update.specialty = diff(bindings.specialty, initialProfile.specialty) update.practiceName = diff(bindings.practiceName, initialProfile.practiceName) update.businessCity = diff(bindings.businessCity, initialProfile.businessCity) update.businessEmail = diff(bindings.businessEmail, initialProfile.businessEmail) update.businessTel = diff(bindings.businessTel, initialProfile.businessTel) update.furtherInfo = diff(bindings.furtherInfo, initialProfile.furtherInfo) update.street = diff(bindings.street, initialProfile.street) update.houseNumber = diff(bindings.houseNumber, initialProfile.houseNumber) update.postalCode = diff(bindings.postalCode, initialProfile.postalCode) update.website = diff(bindings.website, initialProfile.website) return update == HealthChatProfileUpdate() ? nil : update } /// First name, last name, speciality, practice and city are required (web client parity). var requiredFieldsAreValid: Bool { let requiredFields = [bindings.firstName, bindings.lastName, bindings.specialty, bindings.practiceName, bindings.businessCity] guard requiredFields.allSatisfy({ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }) else { return false } let website = bindings.website.trimmingCharacters(in: .whitespacesAndNewlines) return website.isEmpty || URL(string: website) != nil } var canSave: Bool { initialProfile != nil && requiredFieldsAreValid && (avatarDidChange || fieldsUpdate != nil) } var showDeleteImageAction: Bool { localMedia != nil || selectedAvatarURL != nil } } struct HealthChatProfileEditScreenViewStateBindings { var title = "" var firstName = "" var lastName = "" var specialty = "" var practiceName = "" var businessCity = "" var businessEmail = "" var businessTel = "" var furtherInfo = "" var street = "" var houseNumber = "" var postalCode = "" var website = "" var showMediaSheet = false var alertInfo: AlertInfo? } enum HealthChatProfileEditScreenAlertType { case failedProcessingMedia case unsavedChanges case saveError case unknown } enum HealthChatProfileEditScreenViewAction { case cancel case save case retry case presentMediaSource case displayCameraPicker case displayMediaPicker case displayFilePicker case removeImage }