diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index 9f9be00..5611bbf 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -12,8 +12,8 @@ 2DC1718A3F968CF02D7AF0EC /* PatientList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17A06799FC1470D4DDC0D /* PatientList.swift */; }; 2DC171975C15D343ED45A7F3 /* DeviceConnectionListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173E2CB87E7470075E022 /* DeviceConnectionListener.swift */; }; 2DC171B41D4A87E2C861C356 /* ConnectionState+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */; }; + 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177BFA6C401C2C87FCD5C /* TileType.swift */; }; 2DC17337BA4FEF5664BC0D10 /* FinishedSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */; }; - 2DC1734EAF449080A9315932 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1774A86137A5E26720EFA /* ModalView.swift */; }; 2DC17374D5266F13ADD5C002 /* MockDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */; }; 2DC173E02BF55765A906AF4F /* IXNMuseDataPacketType+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */; }; 2DC173E694F96E89EAB63FE0 /* IXNMuseConfiguration+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1785634B3460C4FB953C7 /* IXNMuseConfiguration+Description.swift */; }; @@ -36,6 +36,7 @@ 2DC179F4A6B69C07C1A440D2 /* EEGMeasurementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D48217697F558657E69 /* EEGMeasurementGenerator.swift */; }; 2DC17A1DBD336BC4543CCA81 /* Fit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177545B4C12357C3B2613 /* Fit.swift */; }; 2DC17ABFAC9031FE699864FB /* IXNMuse+EEGDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1768C714141C636C5CFCD /* IXNMuse+EEGDevice.swift */; }; + 2DC17ADF934F839FC66BF7A0 /* TileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177BFA6C401C2C87FCD5C /* TileType.swift */; }; 2DC17B21929D86939F8EB566 /* ConnectionState+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */; }; 2DC17C29AA0382E9F5F2AA4D /* EEGMeasurementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D48217697F558657E69 /* EEGMeasurementGenerator.swift */; }; 2DC17C341155F06C17225169 /* NewPatientModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1739D3D10EFC5B9F67646 /* NewPatientModel.swift */; }; @@ -69,12 +70,6 @@ 2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */; }; 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */; }; 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */; }; - 2FE5DC4E29EDD7FA004B9AB4 /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4829EDD7FA004B9AB4 /* ScheduleView.swift */; }; - 2FE5DC4F29EDD7FA004B9AB4 /* EventContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4929EDD7FA004B9AB4 /* EventContext.swift */; }; - 2FE5DC5029EDD7FA004B9AB4 /* EventContextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4A29EDD7FA004B9AB4 /* EventContextView.swift */; }; - 2FE5DC5129EDD7FA004B9AB4 /* NAMSTaskContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4B29EDD7FA004B9AB4 /* NAMSTaskContext.swift */; }; - 2FE5DC5229EDD7FA004B9AB4 /* NAMSScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4C29EDD7FA004B9AB4 /* NAMSScheduler.swift */; }; - 2FE5DC5329EDD7FA004B9AB4 /* Bundle+Questionnaire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4D29EDD7FA004B9AB4 /* Bundle+Questionnaire.swift */; }; 2FE5DC6429EDD883004B9AB4 /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC6329EDD883004B9AB4 /* SpeziAccount */; }; 2FE5DC6729EDD894004B9AB4 /* SpeziContact in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC6629EDD894004B9AB4 /* SpeziContact */; }; 2FE5DC6A29EDD8A9004B9AB4 /* SpeziFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC6929EDD8A9004B9AB4 /* SpeziFHIR */; }; @@ -83,7 +78,6 @@ 2FE5DC7929EDD8E6004B9AB4 /* SpeziFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC7829EDD8E6004B9AB4 /* SpeziFirestore */; }; 2FE5DC8129EDD91D004B9AB4 /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8029EDD91D004B9AB4 /* SpeziOnboarding */; }; 2FE5DC8429EDD934004B9AB4 /* SpeziQuestionnaire in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8329EDD934004B9AB4 /* SpeziQuestionnaire */; }; - 2FE5DC8729EDD950004B9AB4 /* SpeziScheduler in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8629EDD950004B9AB4 /* SpeziScheduler */; }; 2FE5DC8A29EDD972004B9AB4 /* SpeziLocalStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8929EDD972004B9AB4 /* SpeziLocalStorage */; }; 2FE5DC8C29EDD972004B9AB4 /* SpeziSecureStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8B29EDD972004B9AB4 /* SpeziSecureStorage */; }; 2FE5DC8F29EDD980004B9AB4 /* SpeziViews in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8E29EDD980004B9AB4 /* SpeziViews */; }; @@ -91,7 +85,7 @@ 653A2551283387FE005D4D48 /* NAMSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* NAMSApp.swift */; }; 653A255528338800005D4D48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 653A255428338800005D4D48 /* Assets.xcassets */; }; 653A256228338800005D4D48 /* NAMSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A256128338800005D4D48 /* NAMSTests.swift */; }; - A91459762A4AF4E000A04641 /* SchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91459752A4AF4E000A04641 /* SchedulerTests.swift */; }; + A91459762A4AF4E000A04641 /* ScheduleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91459752A4AF4E000A04641 /* ScheduleTests.swift */; }; A916ADD52AB60227006960DF /* NotificationPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD42AB60227006960DF /* NotificationPermissions.swift */; }; A916ADD72AB62012006960DF /* ProcessInfo+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD62AB62012006960DF /* ProcessInfo+PreviewSimulator.swift */; }; A916ADD92AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD82AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift */; }; @@ -103,23 +97,16 @@ A926D78A2AB7A552000C4C2F /* ProcessInfo+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD62AB62012006960DF /* ProcessInfo+PreviewSimulator.swift */; }; A926D78B2AB7A552000C4C2F /* OnboardingFlow+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD82AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift */; }; A926D78C2AB7A552000C4C2F /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; - A926D78D2AB7A552000C4C2F /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4829EDD7FA004B9AB4 /* ScheduleView.swift */; }; A926D78E2AB7A552000C4C2F /* OnboardingFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3129EDD7CA004B9AB4 /* OnboardingFlow.swift */; }; A926D7902AB7A552000C4C2F /* NAMSStandard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9610AB82AB4868D00E46ADE /* NAMSStandard.swift */; }; A926D7912AB7A552000C4C2F /* CodableArray+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */; }; A926D7922AB7A552000C4C2F /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */; }; A926D7932AB7A552000C4C2F /* Bundle+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */; }; - A926D7942AB7A552000C4C2F /* EventContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4929EDD7FA004B9AB4 /* EventContext.swift */; }; - A926D7952AB7A552000C4C2F /* EventContextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4A29EDD7FA004B9AB4 /* EventContextView.swift */; }; A926D7962AB7A552000C4C2F /* NAMSTestingSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23822989D51F0013F3D9 /* NAMSTestingSetup.swift */; }; - A926D7972AB7A552000C4C2F /* Bundle+Questionnaire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4D29EDD7FA004B9AB4 /* Bundle+Questionnaire.swift */; }; - A926D7982AB7A552000C4C2F /* NAMSTaskContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4B29EDD7FA004B9AB4 /* NAMSTaskContext.swift */; }; A926D79A2AB7A552000C4C2F /* NAMSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5E32BC297E05EA003432F8 /* NAMSAppDelegate.swift */; }; - A926D79B2AB7A552000C4C2F /* NAMSScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4C29EDD7FA004B9AB4 /* NAMSScheduler.swift */; }; A926D79C2AB7A552000C4C2F /* NAMSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* NAMSApp.swift */; }; A926D79D2AB7A552000C4C2F /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2529EDD38A004B9AB4 /* Contacts.swift */; }; A926D79E2AB7A552000C4C2F /* FinishedSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */; }; - A926D79F2AB7A552000C4C2F /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1774A86137A5E26720EFA /* ModalView.swift */; }; A926D7A02AB7A552000C4C2F /* BluetoothManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173D02776C8AAB7386C29 /* BluetoothManager.swift */; }; A926D7A82AB7A552000C4C2F /* Muse.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A99522432AA61DA6009272F4 /* Muse.framework */; }; A926D7A92AB7A552000C4C2F /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7672AB7A552000C4C2F /* SpeziAccount */; }; @@ -178,6 +165,24 @@ A92E34F02ADB9B7E00FE0B51 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E34EF2ADB9B7E00FE0B51 /* OrderedCollections */; }; A92E34F22ADB9B9000FE0B51 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E34F12ADB9B9000FE0B51 /* OrderedCollections */; }; A9405B562A36856300C75412 /* AddPatientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9405B552A36856300C75412 /* AddPatientView.swift */; }; + A94533FA2AEADC8E0095AAD3 /* QuestionnaireTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533F92AEADC8E0095AAD3 /* QuestionnaireTile.swift */; }; + A94533FB2AEADC8E0095AAD3 /* QuestionnaireTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533F92AEADC8E0095AAD3 /* QuestionnaireTile.swift */; }; + A94533FD2AEADCC00095AAD3 /* PatientQuestionnaire.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FC2AEADCC00095AAD3 /* PatientQuestionnaire.swift */; }; + A94533FE2AEADCC00095AAD3 /* PatientQuestionnaire.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FC2AEADCC00095AAD3 /* PatientQuestionnaire.swift */; }; + A94534002AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FF2AEAE1110095AAD3 /* Questionnaire+NAMS.swift */; }; + A94534012AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FF2AEAE1110095AAD3 /* Questionnaire+NAMS.swift */; }; + A94534062AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */ = {isa = PBXBuildFile; fileRef = A94534052AEAE2FF0095AAD3 /* M_CHAT_R_F-en-US-v1.0.json */; }; + A94534072AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */ = {isa = PBXBuildFile; fileRef = A94534052AEAE2FF0095AAD3 /* M_CHAT_R_F-en-US-v1.0.json */; }; + A94534092AEAE3490095AAD3 /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534082AEAE3490095AAD3 /* ScheduleView.swift */; }; + A945340A2AEAE3490095AAD3 /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534082AEAE3490095AAD3 /* ScheduleView.swift */; }; + A945340C2AEAE6380095AAD3 /* PatientTiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = A945340B2AEAE6380095AAD3 /* PatientTiles.swift */; }; + A945340D2AEAE6380095AAD3 /* PatientTiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = A945340B2AEAE6380095AAD3 /* PatientTiles.swift */; }; + A94534102AEAF2AE0095AAD3 /* CompletedQuestionnaire.swift in Sources */ = {isa = PBXBuildFile; fileRef = A945340F2AEAF2AE0095AAD3 /* CompletedQuestionnaire.swift */; }; + A94534112AEAF2AE0095AAD3 /* CompletedQuestionnaire.swift in Sources */ = {isa = PBXBuildFile; fileRef = A945340F2AEAF2AE0095AAD3 /* CompletedQuestionnaire.swift */; }; + A94534132AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534122AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift */; }; + A94534142AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534122AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift */; }; + A94534182AEB0DB20095AAD3 /* QuestionnaireError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534172AEB0DB20095AAD3 /* QuestionnaireError.swift */; }; + A94534192AEB0DB20095AAD3 /* QuestionnaireError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534172AEB0DB20095AAD3 /* QuestionnaireError.swift */; }; A94A42AE2AE9EBE300A3F9E5 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42A92AE9EBE200A3F9E5 /* AccountSheet.swift */; }; A94A42AF2AE9EBE300A3F9E5 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42A92AE9EBE200A3F9E5 /* AccountSheet.swift */; }; A94A42B22AE9EBE300A3F9E5 /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42AB2AE9EBE200A3F9E5 /* AccountButton.swift */; }; @@ -254,9 +259,9 @@ 2DC1751EE233BB85004ECA90 /* IXNMusePreset+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMusePreset+Description.swift"; sourceTree = ""; }; 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDeviceManager.swift; sourceTree = ""; }; 2DC1768C714141C636C5CFCD /* IXNMuse+EEGDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMuse+EEGDevice.swift"; sourceTree = ""; }; - 2DC1774A86137A5E26720EFA /* ModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalView.swift; sourceTree = ""; }; 2DC177545B4C12357C3B2613 /* Fit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fit.swift; sourceTree = ""; }; 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConnectionState+Muse.swift"; sourceTree = ""; }; + 2DC177BFA6C401C2C87FCD5C /* TileType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TileType.swift; sourceTree = ""; }; 2DC1785634B3460C4FB953C7 /* IXNMuseConfiguration+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMuseConfiguration+Description.swift"; sourceTree = ""; }; 2DC17A06799FC1470D4DDC0D /* PatientList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PatientList.swift; sourceTree = ""; }; 2DC17C07ED61540F306E7D23 /* MuseConnectionListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseConnectionListener.swift; sourceTree = ""; }; @@ -283,12 +288,6 @@ 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Binding+Negate.swift"; sourceTree = ""; }; 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Image.swift"; sourceTree = ""; }; 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CodableArray+RawRepresentable.swift"; sourceTree = ""; }; - 2FE5DC4829EDD7FA004B9AB4 /* ScheduleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = ""; }; - 2FE5DC4929EDD7FA004B9AB4 /* EventContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventContext.swift; sourceTree = ""; }; - 2FE5DC4A29EDD7FA004B9AB4 /* EventContextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventContextView.swift; sourceTree = ""; }; - 2FE5DC4B29EDD7FA004B9AB4 /* NAMSTaskContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NAMSTaskContext.swift; sourceTree = ""; }; - 2FE5DC4C29EDD7FA004B9AB4 /* NAMSScheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NAMSScheduler.swift; sourceTree = ""; }; - 2FE5DC4D29EDD7FA004B9AB4 /* Bundle+Questionnaire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Questionnaire.swift"; sourceTree = ""; }; 2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SocialSupportQuestionnaire.json; sourceTree = ""; }; 653A254D283387FE005D4D48 /* NAMS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NAMS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 653A2550283387FE005D4D48 /* NAMSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAMSApp.swift; sourceTree = ""; }; @@ -297,7 +296,7 @@ 653A256128338800005D4D48 /* NAMSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAMSTests.swift; sourceTree = ""; }; 653A256728338800005D4D48 /* NAMSUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NAMSUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 653A258928339462005D4D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A91459752A4AF4E000A04641 /* SchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerTests.swift; sourceTree = ""; }; + A91459752A4AF4E000A04641 /* ScheduleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleTests.swift; sourceTree = ""; }; A916ADD42AB60227006960DF /* NotificationPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissions.swift; sourceTree = ""; }; A916ADD62AB62012006960DF /* ProcessInfo+PreviewSimulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+PreviewSimulator.swift"; sourceTree = ""; }; A916ADD82AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingFlow+PreviewSimulator.swift"; sourceTree = ""; }; @@ -320,6 +319,15 @@ A926D8182AB7B430000C4C2F /* EEGViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGViewModel.swift; sourceTree = ""; }; A926D8192AB7B430000C4C2F /* ConnectedDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; A9405B552A36856300C75412 /* AddPatientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPatientView.swift; sourceTree = ""; }; + A94533F92AEADC8E0095AAD3 /* QuestionnaireTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionnaireTile.swift; sourceTree = ""; }; + A94533FC2AEADCC00095AAD3 /* PatientQuestionnaire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientQuestionnaire.swift; sourceTree = ""; }; + A94533FF2AEAE1110095AAD3 /* Questionnaire+NAMS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Questionnaire+NAMS.swift"; sourceTree = ""; }; + A94534052AEAE2FF0095AAD3 /* M_CHAT_R_F-en-US-v1.0.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "M_CHAT_R_F-en-US-v1.0.json"; sourceTree = ""; }; + A94534082AEAE3490095AAD3 /* ScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = ""; }; + A945340B2AEAE6380095AAD3 /* PatientTiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientTiles.swift; sourceTree = ""; }; + A945340F2AEAF2AE0095AAD3 /* CompletedQuestionnaire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletedQuestionnaire.swift; sourceTree = ""; }; + A94534122AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PatientListModel+QuestionnaireResponse.swift"; sourceTree = ""; }; + A94534172AEB0DB20095AAD3 /* QuestionnaireError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionnaireError.swift; sourceTree = ""; }; A94A42A92AE9EBE200A3F9E5 /* AccountSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = ""; }; A94A42AB2AE9EBE200A3F9E5 /* AccountButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = ""; }; A94A42AD2AE9EBE300A3F9E5 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; @@ -360,7 +368,6 @@ A9610AB72AB4847400E46ADE /* SpeziMockWebService in Frameworks */, 2F49B7762980407C00BCB272 /* Spezi in Frameworks */, 2FE5DC8F29EDD980004B9AB4 /* SpeziViews in Frameworks */, - 2FE5DC8729EDD950004B9AB4 /* SpeziScheduler in Frameworks */, 2FE5DC6A29EDD8A9004B9AB4 /* SpeziFHIR in Frameworks */, 2FE5DC8129EDD91D004B9AB4 /* SpeziOnboarding in Frameworks */, 2FE5DC7929EDD8E6004B9AB4 /* SpeziFirestore in Frameworks */, @@ -452,6 +459,7 @@ 2FE5DC2D29EDD792004B9AB4 /* Resources */ = { isa = PBXGroup; children = ( + A94534052AEAE2FF0095AAD3 /* M_CHAT_R_F-en-US-v1.0.json */, 653A255428338800005D4D48 /* Assets.xcassets */, A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */, 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */, @@ -461,20 +469,6 @@ path = Resources; sourceTree = ""; }; - 2FE5DC3B29EDD7D0004B9AB4 /* Schedule */ = { - isa = PBXGroup; - children = ( - 2FE5DC4D29EDD7FA004B9AB4 /* Bundle+Questionnaire.swift */, - 2FE5DC4929EDD7FA004B9AB4 /* EventContext.swift */, - 2FE5DC4A29EDD7FA004B9AB4 /* EventContextView.swift */, - 2FE5DC4829EDD7FA004B9AB4 /* ScheduleView.swift */, - 2FE5DC4C29EDD7FA004B9AB4 /* NAMSScheduler.swift */, - 2FE5DC4B29EDD7FA004B9AB4 /* NAMSTaskContext.swift */, - 2DC1774A86137A5E26720EFA /* ModalView.swift */, - ); - path = Schedule; - sourceTree = ""; - }; 2FE5DC3D29EDD7E4004B9AB4 /* Helper */ = { isa = PBXGroup; children = ( @@ -520,19 +514,18 @@ 2F5E32BC297E05EA003432F8 /* NAMSAppDelegate.swift */, A9610AB82AB4868D00E46ADE /* NAMSStandard.swift */, 2F4E23822989D51F0013F3D9 /* NAMSTestingSetup.swift */, + A94534082AEAE3490095AAD3 /* ScheduleView.swift */, A94A42A82AE9EBC300A3F9E5 /* Account */, 2FE5DC2729EDD38D004B9AB4 /* Contacts */, A926D7C52AB7A9AB000C4C2F /* EEG */, - 2FE5DC3D29EDD7E4004B9AB4 /* Helper */, - A9BCB58E2AE8586A00DA8588 /* Misc */, 2F4FC8D529EE69BE00BFFE26 /* MockUpload */, A99522402AA61D82009272F4 /* Muse */, 2FE5DC2829EDD398004B9AB4 /* Onboarding */, A989112B2A36686D00E66E3A /* Patients */, 2FE5DC2D29EDD792004B9AB4 /* Resources */, - 2FE5DC3B29EDD7D0004B9AB4 /* Schedule */, 2FC9759D2978E30800BA99FE /* Supporting Files */, - A9DF79E82AE9B97C00AB5983 /* Testing */, + A945340E2AEAF2860095AAD3 /* Tiles */, + A9BCB58E2AE8586A00DA8588 /* Utils */, ); path = NAMS; sourceTree = ""; @@ -553,7 +546,7 @@ A9610ABA2AB49F0900E46ADE /* MockUploadTests.swift */, 2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */, A9FCE8332AE9CA4F0008EA2B /* PatientInformationTests.swift */, - A91459752A4AF4E000A04641 /* SchedulerTests.swift */, + A91459752A4AF4E000A04641 /* ScheduleTests.swift */, ); path = NAMSUITests; sourceTree = ""; @@ -651,6 +644,34 @@ path = Mock; sourceTree = ""; }; + A94533F82AEADBFB0095AAD3 /* Questionnaire */ = { + isa = PBXGroup; + children = ( + A94533FC2AEADCC00095AAD3 /* PatientQuestionnaire.swift */, + A94533FF2AEAE1110095AAD3 /* Questionnaire+NAMS.swift */, + A945340F2AEAF2AE0095AAD3 /* CompletedQuestionnaire.swift */, + ); + path = Questionnaire; + sourceTree = ""; + }; + A945340E2AEAF2860095AAD3 /* Tiles */ = { + isa = PBXGroup; + children = ( + A945340B2AEAE6380095AAD3 /* PatientTiles.swift */, + A94533F92AEADC8E0095AAD3 /* QuestionnaireTile.swift */, + 2DC177BFA6C401C2C87FCD5C /* TileType.swift */, + ); + path = Tiles; + sourceTree = ""; + }; + A94534152AEB0D850095AAD3 /* Errors */ = { + isa = PBXGroup; + children = ( + A94534172AEB0DB20095AAD3 /* QuestionnaireError.swift */, + ); + path = Errors; + sourceTree = ""; + }; A94A42A82AE9EBC300A3F9E5 /* Account */ = { isa = PBXGroup; children = ( @@ -665,6 +686,7 @@ isa = PBXGroup; children = ( A98911302A36688F00E66E3A /* Model */, + A94533F82AEADBFB0095AAD3 /* Questionnaire */, A9405B552A36856300C75412 /* AddPatientView.swift */, A989112C2A36687B00E66E3A /* PatientListSheet.swift */, A989112E2A36688A00E66E3A /* PatientRow.swift */, @@ -684,6 +706,7 @@ A9BCB57E2AE82FFC00DA8588 /* PatientSearchModel.swift */, A9BCB5812AE8307800DA8588 /* SearchToken.swift */, A9BCB5882AE83F7E00DA8588 /* PatientListModel.swift */, + A94534122AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift */, ); path = Model; sourceTree = ""; @@ -701,13 +724,16 @@ path = Muse; sourceTree = ""; }; - A9BCB58E2AE8586A00DA8588 /* Misc */ = { + A9BCB58E2AE8586A00DA8588 /* Utils */ = { isa = PBXGroup; children = ( + A94534152AEB0D850095AAD3 /* Errors */, + 2FE5DC3D29EDD7E4004B9AB4 /* Helper */, + A9DF79E82AE9B97C00AB5983 /* Testing */, A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */, 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */, ); - path = Misc; + path = Utils; sourceTree = ""; }; A9DF79E82AE9B97C00AB5983 /* Testing */ = { @@ -745,7 +771,6 @@ 2FE5DC7829EDD8E6004B9AB4 /* SpeziFirestore */, 2FE5DC8029EDD91D004B9AB4 /* SpeziOnboarding */, 2FE5DC8329EDD934004B9AB4 /* SpeziQuestionnaire */, - 2FE5DC8629EDD950004B9AB4 /* SpeziScheduler */, 2FE5DC8929EDD972004B9AB4 /* SpeziLocalStorage */, 2FE5DC8B29EDD972004B9AB4 /* SpeziSecureStorage */, 2FE5DC8E29EDD980004B9AB4 /* SpeziViews */, @@ -871,7 +896,6 @@ 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */, 2FE5DC7F29EDD91D004B9AB4 /* XCRemoteSwiftPackageReference "SpeziOnboarding" */, 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */, - 2FE5DC8529EDD950004B9AB4 /* XCRemoteSwiftPackageReference "SpeziScheduler" */, 2FE5DC8829EDD972004B9AB4 /* XCRemoteSwiftPackageReference "SpeziStorage" */, 2FE5DC8D29EDD980004B9AB4 /* XCRemoteSwiftPackageReference "SpeziViews" */, 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, @@ -902,6 +926,7 @@ 2FC3439029EE6346002D773C /* SocialSupportQuestionnaire.json in Resources */, A9BCB57C2AE7435E00DA8588 /* Localizable.xcstrings in Resources */, 2F6025CB29BBE70F0045459E /* GoogleService-Info.plist in Resources */, + A94534062AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -929,6 +954,7 @@ A926D7BC2AB7A552000C4C2F /* SocialSupportQuestionnaire.json in Resources */, A9BCB57D2AE7435E00DA8588 /* Localizable.xcstrings in Resources */, A926D7BD2AB7A552000C4C2F /* GoogleService-Info.plist in Resources */, + A94534072AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -978,6 +1004,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A94534002AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */, A926D8302AB7B430000C4C2F /* EEGViewModel.swift in Sources */, A94A42AE2AE9EBE300A3F9E5 /* AccountSheet.swift in Sources */, A926D7FF2AB7B41C000C4C2F /* IXNMuseModel+Description.swift in Sources */, @@ -985,13 +1012,16 @@ A9BCB5822AE8307800DA8588 /* SearchToken.swift in Sources */, A9405B562A36856300C75412 /* AddPatientView.swift in Sources */, 2F4FC8D729EE69D300BFFE26 /* MockUpload.swift in Sources */, + A94533FD2AEADCC00095AAD3 /* PatientQuestionnaire.swift in Sources */, A926D8072AB7B41C000C4C2F /* EEGChannel+Muse.swift in Sources */, A926D8222AB7B430000C4C2F /* EEGRecording.swift in Sources */, A916ADD52AB60227006960DF /* NotificationPermissions.swift in Sources */, A9A179532AC6266A00B180D8 /* EEGDeviceDetails.swift in Sources */, + A94533FA2AEADC8E0095AAD3 /* QuestionnaireTile.swift in Sources */, A926D81E2AB7B430000C4C2F /* NearbyDevices.swift in Sources */, A9BCB58C2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, + A94534092AEAE3490095AAD3 /* ScheduleView.swift in Sources */, 2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */, A916ADD72AB62012006960DF /* ProcessInfo+PreviewSimulator.swift in Sources */, A94A42B62AE9EBE300A3F9E5 /* AccountSetupHeader.swift in Sources */, @@ -1003,7 +1033,6 @@ 2FC975A82978F11A00BA99FE /* Home.swift in Sources */, A94A42B22AE9EBE300A3F9E5 /* AccountButton.swift in Sources */, A926D8242AB7B430000C4C2F /* EEGSeries.swift in Sources */, - 2FE5DC4E29EDD7FA004B9AB4 /* ScheduleView.swift in Sources */, A926D81A2AB7B430000C4C2F /* EEGDeviceList.swift in Sources */, 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, A94A42BA2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */, @@ -1013,26 +1042,22 @@ 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, A9BCB5902AE8588B00DA8588 /* NoInformationText.swift in Sources */, 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, - 2FE5DC4F29EDD7FA004B9AB4 /* EventContext.swift in Sources */, A926D8322AB7B430000C4C2F /* ConnectedDevice.swift in Sources */, - 2FE5DC5029EDD7FA004B9AB4 /* EventContextView.swift in Sources */, A9BCB5862AE8347E00DA8588 /* CollectionReference+AsyncAwait.swift in Sources */, A926D8052AB7B41C000C4C2F /* EEGSeries+Muse.swift in Sources */, 2F4E23832989D51F0013F3D9 /* NAMSTestingSetup.swift in Sources */, - 2FE5DC5329EDD7FA004B9AB4 /* Bundle+Questionnaire.swift in Sources */, - 2FE5DC5129EDD7FA004B9AB4 /* NAMSTaskContext.swift in Sources */, 2F5E32BD297E05EA003432F8 /* NAMSAppDelegate.swift in Sources */, - 2FE5DC5229EDD7FA004B9AB4 /* NAMSScheduler.swift in Sources */, + A94534182AEB0DB20095AAD3 /* QuestionnaireError.swift in Sources */, A926D81C2AB7B430000C4C2F /* EEGDeviceRow.swift in Sources */, 653A2551283387FE005D4D48 /* NAMSApp.swift in Sources */, A989112F2A36688A00E66E3A /* PatientRow.swift in Sources */, A98911322A36689D00E66E3A /* Patient.swift in Sources */, 2FE5DC2629EDD38A004B9AB4 /* Contacts.swift in Sources */, + A94534102AEAF2AE0095AAD3 /* CompletedQuestionnaire.swift in Sources */, 2DC17337BA4FEF5664BC0D10 /* FinishedSetup.swift in Sources */, A926D82E2AB7B430000C4C2F /* EEGDevice.swift in Sources */, A9BCB5892AE83F7E00DA8588 /* PatientListModel.swift in Sources */, A926D8262AB7B430000C4C2F /* EEGChannel.swift in Sources */, - 2DC1734EAF449080A9315932 /* ModalView.swift in Sources */, A926D8032AB7B41C000C4C2F /* EEGReading+Muse.swift in Sources */, 2DC17D4A9B040AF343D9EF1F /* BluetoothManager.swift in Sources */, A926D8282AB7B430000C4C2F /* EEGReading.swift in Sources */, @@ -1047,8 +1072,10 @@ A9A179562AC62BE500B180D8 /* ListRow.swift in Sources */, 2DC175AEF3E3A716DF747E21 /* EEGFrequency.swift in Sources */, 2DC179D0C1180EF9B1E8F276 /* MuseDeviceManager.swift in Sources */, + A94534132AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */, 2DC17ABFAC9031FE699864FB /* IXNMuse+EEGDevice.swift in Sources */, 2DC173E02BF55765A906AF4F /* IXNMuseDataPacketType+Type.swift in Sources */, + A945340C2AEAE6380095AAD3 /* PatientTiles.swift in Sources */, 2DC17FE79AB13F1F300788A6 /* MuseConnectionListener.swift in Sources */, 2DC17A1DBD336BC4543CCA81 /* Fit.swift in Sources */, 2DC179421EE83DA24520EABB /* HeadbandFit+Muse.swift in Sources */, @@ -1059,6 +1086,7 @@ 2DC17F5243570D8FF743EADD /* PatientInformation.swift in Sources */, 2DC17C341155F06C17225169 /* NewPatientModel.swift in Sources */, 2DC1718A3F968CF02D7AF0EC /* PatientList.swift in Sources */, + 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1075,7 +1103,7 @@ buildActionMask = 2147483647; files = ( A9C9B6B42ADE191100C8C46D /* EEGDeviceTests.swift in Sources */, - A91459762A4AF4E000A04641 /* SchedulerTests.swift in Sources */, + A91459762A4AF4E000A04641 /* ScheduleTests.swift in Sources */, 2F4E23872989DB360013F3D9 /* ContactsTests.swift in Sources */, A9610ABB2AB49F0900E46ADE /* MockUploadTests.swift in Sources */, A9FCE8342AE9CA4F0008EA2B /* PatientInformationTests.swift in Sources */, @@ -1087,6 +1115,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A94534012AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */, A926D8312AB7B430000C4C2F /* EEGViewModel.swift in Sources */, A94A42AF2AE9EBE300A3F9E5 /* AccountSheet.swift in Sources */, A926D8002AB7B41C000C4C2F /* IXNMuseModel+Description.swift in Sources */, @@ -1094,13 +1123,16 @@ A926D77F2AB7A552000C4C2F /* StorageKeys.swift in Sources */, A926D7812AB7A552000C4C2F /* MockUpload.swift in Sources */, A9BCB58A2AE83F7E00DA8588 /* PatientListModel.swift in Sources */, + A94533FE2AEADCC00095AAD3 /* PatientQuestionnaire.swift in Sources */, A9DF79DF2AE8A81F00AB5983 /* AddPatientView.swift in Sources */, A926D8082AB7B41C000C4C2F /* EEGChannel+Muse.swift in Sources */, A926D8232AB7B430000C4C2F /* EEGRecording.swift in Sources */, A9DF79E02AE8A82100AB5983 /* PatientListSheet.swift in Sources */, + A94533FB2AEADC8E0095AAD3 /* QuestionnaireTile.swift in Sources */, A926D7822AB7A552000C4C2F /* NotificationPermissions.swift in Sources */, A9A179542AC6266A00B180D8 /* EEGDeviceDetails.swift in Sources */, A926D81F2AB7B430000C4C2F /* NearbyDevices.swift in Sources */, + A945340A2AEAE3490095AAD3 /* ScheduleView.swift in Sources */, A926D7872AB7A552000C4C2F /* Welcome.swift in Sources */, A926D7882AB7A552000C4C2F /* Binding+Negate.swift in Sources */, A94A42B72AE9EBE300A3F9E5 /* AccountSetupHeader.swift in Sources */, @@ -1113,7 +1145,6 @@ A94A42B32AE9EBE300A3F9E5 /* AccountButton.swift in Sources */, A926D8252AB7B430000C4C2F /* EEGSeries.swift in Sources */, A9BCB5912AE8588B00DA8588 /* NoInformationText.swift in Sources */, - A926D78D2AB7A552000C4C2F /* ScheduleView.swift in Sources */, A926D81B2AB7B430000C4C2F /* EEGDeviceList.swift in Sources */, A94A42BB2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */, A926D78E2AB7A552000C4C2F /* OnboardingFlow.swift in Sources */, @@ -1125,25 +1156,21 @@ A926D7922AB7A552000C4C2F /* FeatureFlags.swift in Sources */, A9BCB5832AE8307800DA8588 /* SearchToken.swift in Sources */, A926D7932AB7A552000C4C2F /* Bundle+Image.swift in Sources */, - A926D7942AB7A552000C4C2F /* EventContext.swift in Sources */, A926D8332AB7B430000C4C2F /* ConnectedDevice.swift in Sources */, A9BCB58D2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */, - A926D7952AB7A552000C4C2F /* EventContextView.swift in Sources */, + A94534192AEB0DB20095AAD3 /* QuestionnaireError.swift in Sources */, A926D8062AB7B41C000C4C2F /* EEGSeries+Muse.swift in Sources */, A926D7962AB7A552000C4C2F /* NAMSTestingSetup.swift in Sources */, - A926D7972AB7A552000C4C2F /* Bundle+Questionnaire.swift in Sources */, - A926D7982AB7A552000C4C2F /* NAMSTaskContext.swift in Sources */, A9BCB5802AE82FFC00DA8588 /* PatientSearchModel.swift in Sources */, A926D79A2AB7A552000C4C2F /* NAMSAppDelegate.swift in Sources */, - A926D79B2AB7A552000C4C2F /* NAMSScheduler.swift in Sources */, A9DF79E22AE8A82600AB5983 /* PatientInformation.swift in Sources */, + A94534112AEAF2AE0095AAD3 /* CompletedQuestionnaire.swift in Sources */, A926D81D2AB7B430000C4C2F /* EEGDeviceRow.swift in Sources */, A926D79C2AB7A552000C4C2F /* NAMSApp.swift in Sources */, A926D79D2AB7A552000C4C2F /* Contacts.swift in Sources */, A926D79E2AB7A552000C4C2F /* FinishedSetup.swift in Sources */, A926D82F2AB7B430000C4C2F /* EEGDevice.swift in Sources */, A926D8272AB7B430000C4C2F /* EEGChannel.swift in Sources */, - A926D79F2AB7A552000C4C2F /* ModalView.swift in Sources */, A926D8042AB7B41C000C4C2F /* EEGReading+Muse.swift in Sources */, A926D7A02AB7A552000C4C2F /* BluetoothManager.swift in Sources */, A926D8292AB7B430000C4C2F /* EEGReading.swift in Sources */, @@ -1156,8 +1183,10 @@ 2DC17924E7D44FE1A2562528 /* IXNMusePreset+Description.swift in Sources */, A9A179572AC62BE500B180D8 /* ListRow.swift in Sources */, 2DC17D159C62F690B2137E65 /* EEGFrequency.swift in Sources */, + A94534142AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */, 2DC1713C311BAA65EE0E2748 /* MuseDeviceManager.swift in Sources */, 2DC17DA43107F4E8469B7C73 /* IXNMuse+EEGDevice.swift in Sources */, + A945340D2AEAE6380095AAD3 /* PatientTiles.swift in Sources */, 2DC1762E730B0472308EEFFC /* IXNMuseDataPacketType+Type.swift in Sources */, 2DC17128D81F6CF06F65FFD4 /* MuseConnectionListener.swift in Sources */, 2DC17644A07BC415B89BDACA /* Fit.swift in Sources */, @@ -1168,6 +1197,7 @@ 2DC17DA28E7F6C4330CDB7FC /* EEGViewModel+ActiveMock.swift in Sources */, 2DC179F4A6B69C07C1A440D2 /* EEGMeasurementGenerator.swift in Sources */, 2DC17686B3AEB09A8F60AB8E /* PatientList.swift in Sources */, + 2DC17ADF934F839FC66BF7A0 /* TileType.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1948,14 +1978,6 @@ minimumVersion = 0.4.3; }; }; - 2FE5DC8529EDD950004B9AB4 /* XCRemoteSwiftPackageReference "SpeziScheduler" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziScheduler.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.6.3; - }; - }; 2FE5DC8829EDD972004B9AB4 /* XCRemoteSwiftPackageReference "SpeziStorage" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziStorage.git"; @@ -2140,11 +2162,6 @@ package = 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */; productName = SpeziQuestionnaire; }; - 2FE5DC8629EDD950004B9AB4 /* SpeziScheduler */ = { - isa = XCSwiftPackageProductDependency; - package = 2FE5DC8529EDD950004B9AB4 /* XCRemoteSwiftPackageReference "SpeziScheduler" */; - productName = SpeziScheduler; - }; 2FE5DC8929EDD972004B9AB4 /* SpeziLocalStorage */ = { isa = XCSwiftPackageProductDependency; package = 2FE5DC8829EDD972004B9AB4 /* XCRemoteSwiftPackageReference "SpeziStorage" */; diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a843634..b910621 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -198,15 +198,6 @@ "version" : "0.4.3" } }, - { - "identity" : "spezischeduler", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziScheduler.git", - "state" : { - "revision" : "dcba98814d783438b8505e692d0dfb8d90968597", - "version" : "0.6.3" - } - }, { "identity" : "spezistorage", "kind" : "remoteSourceControl", diff --git a/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS.xcscheme b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS.xcscheme index bc0b6ce..b2c39bc 100644 --- a/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS.xcscheme +++ b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS.xcscheme @@ -56,7 +56,7 @@ + isEnabled = "YES"> ) { self._activePatientId = activePatient diff --git a/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift b/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift new file mode 100644 index 0000000..78c899f --- /dev/null +++ b/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift @@ -0,0 +1,41 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziFHIR +import SpeziFirestore + + +// SpeziFHIR defines the Observation Model which collides with Apples Observation framework naming + +extension PatientListModel { + func add(response: QuestionnaireResponse) async throws { + guard let questionnaireId = response.questionnaire?.value?.url.absoluteString else { + Self.logger.error("Failed to retrieve questionnaire id for response!") + throw QuestionnaireError.unexpectedFormat + } + + guard let activePatient, + let patientId = activePatient.id else { + Self.logger.error("Couldn't save questionnaire response \(questionnaireId). No patient found!") + throw QuestionnaireError.missingPatient + } + + guard let questionnaire = PatientQuestionnaire.all.first(where: { $0.questionnaire.url?.value?.url.absoluteString == questionnaireId }) else { + Self.logger.error("Failed to match questionnaire response with id \(questionnaireId) to any of our local questionnaires.") + throw QuestionnaireError.failedQuestionnaireMatch + } + + do { + try await completedQuestionnairesCollection(patientId: patientId) + .addDocument(from: CompletedQuestionnaire(internalQuestionnaireId: questionnaire.id, questionnaireResponse: response)) + } catch { + Self.logger.error("Failed to save questionnaire response for questionnaire \(questionnaireId)!") + throw FirestoreError(error) + } + } +} diff --git a/NAMS/Patients/Model/PatientListModel.swift b/NAMS/Patients/Model/PatientListModel.swift index 6603d75..d16d002 100644 --- a/NAMS/Patients/Model/PatientListModel.swift +++ b/NAMS/Patients/Model/PatientListModel.swift @@ -8,6 +8,7 @@ import FirebaseFirestore import FirebaseFirestoreSwift +import Observation import OrderedCollections import OSLog import SpeziAccount @@ -23,12 +24,22 @@ class PatientListModel { static let logger = Logger(subsystem: "edu.stanford.NAMS", category: "PatientListModel") var patientList: [Patient]? // swiftlint:disable:this discouraged_optional_collection + var activePatient: Patient? + var questionnaires: [CompletedQuestionnaire]? // swiftlint:disable:this discouraged_optional_collection + var completedQuestionnaires: [String]? { // swiftlint:disable:this discouraged_optional_collection + guard let questionnaires else { + return nil + } + return questionnaires.map { $0.internalQuestionnaireId } + } + var categorizedPatients: OrderedDictionary = [:] - @ObservationIgnored var patientListListener: ListenerRegistration? - @ObservationIgnored var activePatientListener: ListenerRegistration? + @ObservationIgnored private var patientListListener: ListenerRegistration? + @ObservationIgnored private var activePatientListener: ListenerRegistration? + @ObservationIgnored private var activePatientQuestionnairesListener: ListenerRegistration? private var patientsCollection: CollectionReference { Firestore.firestore().collection("patients") @@ -38,6 +49,13 @@ class PatientListModel { init() {} + func completedQuestionnairesCollection(patientId: String) -> CollectionReference { + patientsCollection + .document(patientId) + .collection("questionnaireResponse") + } + + func retrieveList(viewState: Binding) { closeList() @@ -103,20 +121,6 @@ class PatientListModel { } } - func setupTestEnvironment(withPatient patientId: String, viewState: Binding, account: Account) async { - await setupTestAccount(account: account, viewState: viewState) - - do { - try await patientsCollection.document(patientId).setData( - from: Patient(name: .init(givenName: "Leland", familyName: "Stanford")), - merge: true - ) - } catch { - Self.logger.error("Failed to set test patient information: \(error)") - viewState.wrappedValue = .error(FirestoreError(error)) - } - } - func loadActivePatient(for id: String, viewState: Binding) { if activePatient?.id == id { return // already set up @@ -143,6 +147,37 @@ class PatientListModel { } } } + + self.registerPatientCompletedQuestionnaire(patientId: id, viewState: viewState) + } + + private func registerPatientCompletedQuestionnaire(patientId: String, viewState: Binding) { + if activePatientQuestionnairesListener != nil { + return + } + + self.activePatientQuestionnairesListener = completedQuestionnairesCollection(patientId: patientId) + .addSnapshotListener { snapshot, error in + guard let snapshot else { + Self.logger.error("Failed to retrieve questionnaire responses for active patient: \(error)") + viewState.wrappedValue = .error(FirestoreError(error!)) // swiftlint:disable:this force_unwrapping + return + } + + do { + self.questionnaires = try snapshot.documents.map { document in + try document.data(as: CompletedQuestionnaire.self) + } + } catch { + if error is DecodingError { + Self.logger.error("Failed to decode completed questionnaires: \(error)") + viewState.wrappedValue = .error(AnyLocalizedError(error: error)) + } else { + Self.logger.error("Unexpected error occurred while decoding completed questionnaires: \(error)") + viewState.wrappedValue = .error(AnyLocalizedError(error: error)) + } + } + } } func closeList() { @@ -157,6 +192,25 @@ class PatientListModel { activePatientListener.remove() self.activePatientListener = nil } + + if let activePatientQuestionnairesListener { + activePatientQuestionnairesListener.remove() + self.activePatientQuestionnairesListener = nil + } + } + + func setupTestEnvironment(withPatient patientId: String, viewState: Binding, account: Account) async { + await setupTestAccount(account: account, viewState: viewState) + + do { + try await patientsCollection.document(patientId).setData( + from: Patient(name: .init(givenName: "Example", familyName: "Patient")), + merge: true + ) + } catch { + Self.logger.error("Failed to set test patient information: \(error)") + viewState.wrappedValue = .error(FirestoreError(error)) + } } private func setupTestAccount(account: Account, viewState: Binding) async { diff --git a/NAMS/Patients/Questionnaire/CompletedQuestionnaire.swift b/NAMS/Patients/Questionnaire/CompletedQuestionnaire.swift new file mode 100644 index 0000000..9eecfc2 --- /dev/null +++ b/NAMS/Patients/Questionnaire/CompletedQuestionnaire.swift @@ -0,0 +1,17 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import FirebaseFirestoreSwift +import SpeziFHIR + + +struct CompletedQuestionnaire: Codable { + @DocumentID var id: String? + let internalQuestionnaireId: String + let questionnaireResponse: QuestionnaireResponse +} diff --git a/NAMS/Patients/Questionnaire/PatientQuestionnaire.swift b/NAMS/Patients/Questionnaire/PatientQuestionnaire.swift new file mode 100644 index 0000000..76ce171 --- /dev/null +++ b/NAMS/Patients/Questionnaire/PatientQuestionnaire.swift @@ -0,0 +1,51 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziQuestionnaire + + +struct PatientQuestionnaire: Identifiable { + let id: String + let title: LocalizedStringResource + let description: LocalizedStringResource + + let questionnaire: Questionnaire + + let tileType: TileType = .questionnaire + let expectedCompletionMinutes: String + + init( + id: String, + title: LocalizedStringResource, + description: LocalizedStringResource, + questionnaire: Questionnaire, + expectedCompletionMinutes: String + ) { + self.id = id + self.title = title + self.description = description + self.questionnaire = questionnaire + self.expectedCompletionMinutes = expectedCompletionMinutes + } +} + + +extension PatientQuestionnaire { + static let all: [PatientQuestionnaire] = [.mChatRF] + + static var mChatRF: PatientQuestionnaire = { + PatientQuestionnaire( + id: "m_chat_rf_1.0", + title: "M-CHAT R/F", + description: "The Modified Checklist for Autism in Toddlers, Revised with Follow-Up.", + questionnaire: .mChatRF, + expectedCompletionMinutes: "5-10" + ) + }() +} diff --git a/NAMS/Patients/Questionnaire/Questionnaire+NAMS.swift b/NAMS/Patients/Questionnaire/Questionnaire+NAMS.swift new file mode 100644 index 0000000..73a7b43 --- /dev/null +++ b/NAMS/Patients/Questionnaire/Questionnaire+NAMS.swift @@ -0,0 +1,33 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziFHIR + + +extension Questionnaire { + static func questionnaire(withName name: String, bundle: Foundation.Bundle) -> Questionnaire { + guard let resourceURL = bundle.url(forResource: name, withExtension: "json") else { + preconditionFailure("Could not find the questionnaire \"\(name).json\" in the bundle.") + } + + do { + let resourceData = try Data(contentsOf: resourceURL) + return try JSONDecoder().decode(Questionnaire.self, from: resourceData) + } catch { + preconditionFailure("Could not decode the FHIR questionnaire named \"\(name).json\": \(error)") + } + } +} + + +extension Questionnaire { + static var mChatRF: Questionnaire = { + questionnaire(withName: "M_CHAT_R_F-en-US-v1.0", bundle: .main) + }() +} diff --git a/NAMS/Resources/Localizable.xcstrings b/NAMS/Resources/Localizable.xcstrings index ceb60e5..2e2dde2 100644 --- a/NAMS/Resources/Localizable.xcstrings +++ b/NAMS/Resources/Localizable.xcstrings @@ -31,6 +31,9 @@ } } }, + "%@ min" : { + "comment" : "Expected task completion in minutes." + }, "%@, Patient Details" : { "localizations" : { "en" : { @@ -232,6 +235,9 @@ } } }, + "Completed" : { + "comment" : "Completed Task. Subtitle." + }, "Completed Task: %@" : { }, @@ -373,6 +379,12 @@ }, "enter last name" : { + }, + "Failed to associate response with original questionnaire!" : { + + }, + "Failed to complete Questionnaire" : { + }, "FINISHED_SETUP_TEXT" : { "localizations" : { @@ -549,6 +561,9 @@ } } } + }, + "M-CHAT R/F" : { + }, "MEDIOCRE_FIT" : { "localizations" : { @@ -742,6 +757,7 @@ } }, "Preview Modal" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -781,6 +797,9 @@ } } }, + "Questionnaire" : { + "comment" : "Tile Type" + }, "Recording" : { }, @@ -804,6 +823,9 @@ } } } + }, + "Screening" : { + }, "Search for a patient" : { "localizations" : { @@ -896,8 +918,12 @@ } } } + }, + "Start %@" : { + }, "TASK_CONTEXT_ACTION_QUESTIONNAIRE" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -908,6 +934,7 @@ } }, "TASK_CONTEXT_ACTION_TEST" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -918,6 +945,7 @@ } }, "TASK_SOCIAL_SUPPORT_QUESTIONNAIRE_DESCRIPTION" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -928,6 +956,7 @@ } }, "TASK_SOCIAL_SUPPORT_QUESTIONNAIRE_TITLE" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -939,6 +968,12 @@ }, "Task: %@" : { + }, + "The Modified Checklist for Autism in Toddlers, Revised with Follow-Up." : { + + }, + "There was no selected patient found!" : { + }, "Theta" : { "localizations" : { @@ -969,6 +1004,9 @@ } } } + }, + "Unexpected format of the questionnaire response!" : { + }, "WEARING" : { "localizations" : { diff --git a/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json b/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json new file mode 100644 index 0000000..f53b734 --- /dev/null +++ b/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json @@ -0,0 +1 @@ +{"title":"M-CHAT R/F","resourceType":"Questionnaire","language":"en-US","name":"M_CHAT_R_F","status":"active","publisher":"Stanford Biodesign Digital Health","meta":{"profile":["http://spezi.health/fhir/StructureDefinition/sdf-Questionnaire"],"tag":[{"system":"urn:ietf:bcp:47","code":"en-US","display":"English"}]},"useContext":[{"code":{"system":"http://hl7.org/fhir/ValueSet/usage-context-type","code":"focus","display":"Clinical Focus"},"valueCodeableConcept":{"coding":[{"system":"urn:oid:2.16.578.1.12.4.1.1.8655","display":"M-CHAT R/F"}]}}],"contact":[{"name":"http://spezi.health"}],"subjectType":["Patient"],"url":"http://spezi.health/fhir/questionnaire/21a5bee6-b8ad-495b-a05b-26378c8c3b31","version":"1.0","date":"2023-10-26T00:00:00-07:00","id":"mchat-rf","item":[{"linkId":"680becad-7ecc-4909-9aa7-55b680f300b7","type":"choice","text":"If you point at something across the room, does your child look at it?\n\nFor example, if you point at a toy or an animal, does your child look at the toy or animal?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"1cd27a89-5e03-4564-85f3-3ace02a5da44","type":"choice","text":"Have you ever wondered if your child might be deaf?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"1da67934-bb63-421a-b770-a0009f6a19ea","type":"choice","text":"Does your child play pretend or make-believe?\n\nFor example, pretend to drink from an empty cup, pretend to talk on a phone, or pretend to feed a doll or stuffed animal?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"0fd8945c-2fa4-46a3-a3c6-8e1c8b262d77","type":"choice","text":"Does your child like climbing on things?\n\nFor example, furniture, playground equipment, or stairs.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"6302ca35-ad63-43f1-8167-1b86792484ee","type":"choice","text":"Does your child make **unusual** finger movements near his or her eyes?\n\nFor example, does your child wiggle his or her fingers close to his or her eyes?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"64012245-04b4-40b1-8362-7214e066b567","type":"choice","text":"Does your child point with one finger to ask for something or to get help?\n\nFor example, pointing to a snack or toy that is out of reach.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"9843d642-0f1f-4e0c-9fb4-11593ac2c3ce","type":"choice","text":"Does your child point with one finger to show you something interesting?\n\nFor example, pointing to an airplane in the sky or a big truck in the road.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"416d2b7b-b6ad-4778-8788-aa52d20a41cd","type":"choice","text":"Is your child interested in other children?\n\nFor example, does your child watch other children, smile at them, or go to them?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"fa145ad9-cda7-4d6a-8c8d-e18671019396","type":"choice","text":"Does your child show you things by bringing them to you or holding them up for you to see – not to get help, but just to share?\n\nFor example, showing you a flower, a stuffed animal, or a toy truck.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"facdbdd8-f77c-4d23-f4bf-0d8f91ee8fb2","type":"choice","text":"Does your child respond when you call his or her name?\n\nFor example, does your her or she look up, talk or babble, or stop what he or she is doing when you call his or her name?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"1a2dd10f-390f-41aa-f9d2-3e13c3d3d142","type":"choice","text":"When you smile at your child, does he or she smile back at you?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"36bf0bdd-6d80-4882-876e-448430fae877","type":"choice","text":"Does your child get upset by everyday noises?\n\nFor example, does your child scream or cry to noise such as a vacuum cleaner or load music?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"9eeae1fe-b858-4ccf-8b97-a933077691e9","type":"choice","text":"Does your child walk?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"b4aed134-1f35-4e83-e222-243855f38b2d","type":"choice","text":"Does your child look you in the eye when you are talking to him or her, playing with him or her, or dressing him or her?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"234916d7-ad3f-4d73-8ecd-af5e1c9e6578","type":"choice","text":"Does your child try to copy what you do?\n\nFor example, wave bye-bye, clap, or make a funny noise when you do.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"e3fc7bb4-f5a0-4f02-9422-6dded9d683d7","type":"choice","text":"If you turn your head to look at something, does your child look around to see what you are looking at?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"77c47260-d5a9-4151-824a-3fe3e880ca2e","type":"choice","text":"Does your child try to get you to watch him or her?\n\nFor example, does your child look at you for praise, or say \"look\" or \"watch me\"?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"8493c48e-63f1-4479-f324-317858391abf","type":"choice","text":"Does your child understand when you tell him or her to do something?\n\nFor example, if you don’t point, can your child understand “put the book on the chair” or “bring me the blanket”?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"59c27629-6010-4255-caf4-3a4e6e74719a","type":"choice","text":"If something new happens, does your child look at your face to see how you feel about it?\n\nFor example, if he or she hears a strange or funny noise, or sees a new toy, will\nhe or she look at your face?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"3214425d-902f-4d98-8d09-31ee56776848","type":"choice","text":"Does your child like movement activities?\n\nFor example, being swung or bounced on your knee.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]}]} \ No newline at end of file diff --git a/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json.license b/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json.license new file mode 100644 index 0000000..fd338a3 --- /dev/null +++ b/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json.license @@ -0,0 +1,6 @@ + +This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project + +SPDX-FileCopyrightText: 2023 Stanford University + +SPDX-License-Identifier: MIT diff --git a/NAMS/Schedule/Bundle+Questionnaire.swift b/NAMS/Schedule/Bundle+Questionnaire.swift deleted file mode 100644 index 3d84df4..0000000 --- a/NAMS/Schedule/Bundle+Questionnaire.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import SpeziFHIR - - -extension Foundation.Bundle { - func questionnaire(withName name: String) -> Questionnaire { - guard let resourceURL = self.url(forResource: name, withExtension: "json") else { - fatalError("Could not find the questionnaire \"\(name).json\" in the bundle.") - } - - do { - let resourceData = try Data(contentsOf: resourceURL) - return try JSONDecoder().decode(Questionnaire.self, from: resourceData) - } catch { - fatalError("Could not decode the FHIR questionnaire named \"\(name).json\": \(error)") - } - } -} diff --git a/NAMS/Schedule/EventContext.swift b/NAMS/Schedule/EventContext.swift deleted file mode 100644 index 7ed6aa9..0000000 --- a/NAMS/Schedule/EventContext.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SpeziScheduler - - -struct EventContext: Comparable, Identifiable { - let event: Event - let task: Task - - - var id: Event.ID { - event.id - } - - - static func < (lhs: EventContext, rhs: EventContext) -> Bool { - lhs.event.scheduledAt < rhs.event.scheduledAt - } -} diff --git a/NAMS/Schedule/EventContextView.swift b/NAMS/Schedule/EventContextView.swift deleted file mode 100644 index c818e08..0000000 --- a/NAMS/Schedule/EventContextView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SpeziScheduler -import SwiftUI - - -struct EventContextView: View { - let eventContext: EventContext - - - var body: some View { - HStack { - VStack(alignment: .leading) { - HStack { - if eventContext.event.complete { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) - .font(.system(size: 30)) - .accessibilityHidden(true) - } - VStack(alignment: .leading, spacing: 8) { - Text(verbatim: eventContext.task.title) - .font(.headline) - .accessibilityLabel( - eventContext.event.complete - ? "Completed Task: \(eventContext.task.title)" - : "Task: \(eventContext.task.title)" - ) - Text(verbatim: format(eventDate: eventContext.event.scheduledAt)) - .font(.subheadline) - } - } - Divider() - Text(verbatim: eventContext.task.description) - .font(.callout) - if !eventContext.event.complete { - Text(eventContext.task.context.actionType) - .frame(maxWidth: .infinity, minHeight: 50) - .foregroundColor(.white) - .background(Color.accentColor) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .padding(.top, 8) - } - } - } - .disabled(eventContext.event.complete) - .contentShape(Rectangle()) - } - - - private func format(eventDate: Date) -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .none - dateFormatter.timeStyle = .short - return dateFormatter.string(from: eventDate) - } -} - - -#if DEBUG -struct EventContextView_Previews: PreviewProvider { - private static let task = NAMSScheduler.socialSupportTask(testSchedule: true) - - - static var previews: some View { - EventContextView( - eventContext: EventContext( - // We use a force unwrap in the preview as we can not recover from an error here - // and the code will never end up in a production environment. - // swiftlint:disable:next force_unwrapping - event: task.events(from: .now.addingTimeInterval(-60 * 60 * 24)).first!, - task: task - ) - ) - .padding() - } -} -#endif diff --git a/NAMS/Schedule/ModalView.swift b/NAMS/Schedule/ModalView.swift deleted file mode 100644 index ca211f1..0000000 --- a/NAMS/Schedule/ModalView.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// This source file is part of the Stanford Spezi Template Application project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// -import SwiftUI - - -struct ModalView: View { - @Environment(\.dismiss) - private var dismiss - - let text: LocalizedStringResource - let buttonText: LocalizedStringResource - let onClose: () -> Void - - - var body: some View { - VStack { - Spacer() - Text(text) - .padding() - Spacer() - Button { - self.dismiss() - self.onClose() - } label: { - Text(buttonText) - .frame(maxWidth: .infinity, minHeight: 38) - } - .padding() - .buttonStyle(.borderedProminent) - } - } -} - - -struct ModalView_Previews: PreviewProvider { - static var previews: some View { - ModalView(text: "Preview Modal", buttonText: "Close") { - print("Preview Modal closed.") - } - } -} diff --git a/NAMS/Schedule/NAMSScheduler.swift b/NAMS/Schedule/NAMSScheduler.swift deleted file mode 100644 index e8ca3df..0000000 --- a/NAMS/Schedule/NAMSScheduler.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import SpeziFHIR -import SpeziScheduler - - -/// A `Scheduler` using the ``NAMSTaskContext`` to schedule and manage tasks and events in the -/// Neurodevelopment Assessment and Monitoring System (NAMS). -typealias NAMSScheduler = Scheduler - - -extension NAMSScheduler { - private static var supportsNotifications: Bool { - !FeatureFlags.skipOnboarding - } - - /// Creates a default instance of the ``NAMSScheduler`` by scheduling the tasks listed below. - convenience init() { - self.init(tasks: [Self.socialSupportTask()]) - } - - /// Creates a default instance of the ``NAMSScheduler`` by scheduling the tasks listed below. - convenience init(testSchedule: Bool) { - self.init(tasks: [Self.socialSupportTask(testSchedule: testSchedule)]) - } - - static func socialSupportTask(testSchedule: Bool = false) -> SpeziScheduler.Task { - let dateComponents: DateComponents - if FeatureFlags.testSchedule { - // Adds a task at the current time for UI testing if the `--testSchedule` feature flag is set - dateComponents = DateComponents( - hour: Calendar.current.component(.hour, from: .now), - minute: Calendar.current.component(.minute, from: .now) - ) - } else { - // For the normal app usage, we schedule the task for every day at 8:00 AM - dateComponents = DateComponents(hour: 8, minute: 0) - } - - return Task( - title: String(localized: "TASK_SOCIAL_SUPPORT_QUESTIONNAIRE_TITLE"), - description: String(localized: "TASK_SOCIAL_SUPPORT_QUESTIONNAIRE_DESCRIPTION"), - schedule: Schedule( - start: Calendar.current.startOfDay(for: Date()), - repetition: .matching(dateComponents), - end: .numberOfEvents(365) - ), - notifications: !testSchedule && supportsNotifications, - context: NAMSTaskContext.questionnaire(Bundle.main.questionnaire(withName: "SocialSupportQuestionnaire")) - ) - } -} diff --git a/NAMS/Schedule/NAMSTaskContext.swift b/NAMS/Schedule/NAMSTaskContext.swift deleted file mode 100644 index 06fa696..0000000 --- a/NAMS/Schedule/NAMSTaskContext.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import SpeziFHIR - - -/// The context attached to each task in the Neurodevelopment Assessment and Monitoring System (NAMS). -/// -/// We currently only support `Questionnaire`s, more cases can be added in the future. -enum NAMSTaskContext: Codable, Identifiable { - /// The task should display a `Questionnaire`. - case questionnaire(Questionnaire) - /// The task is used for UI testing - case test(LocalizedStringResource) - - - var id: Questionnaire.ID { - switch self { - case let .questionnaire(questionnaire): - return questionnaire.id - case .test: - return FHIRPrimitive(FHIRString(UUID().uuidString)) - } - } - - var actionType: LocalizedStringResource { - switch self { - case .questionnaire: - return "TASK_CONTEXT_ACTION_QUESTIONNAIRE" - case .test: - return "TASK_CONTEXT_ACTION_TEST" - } - } -} diff --git a/NAMS/Schedule/ScheduleView.swift b/NAMS/Schedule/ScheduleView.swift deleted file mode 100644 index 8a9e5d2..0000000 --- a/NAMS/Schedule/ScheduleView.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - - -import SpeziAccount -import SpeziQuestionnaire -import SpeziScheduler -import SwiftUI - - -struct ScheduleView: View { - @EnvironmentObject var scheduler: NAMSScheduler - @ObservedObject var eegModel: EEGViewModel - - @State var eventContextsByDate: [Date: [EventContext]] = [:] - @State var presentedContext: EventContext? - - @State var presentingMuseList = false - @State var presentPatientSheet = false - @Binding var presentingAccount: Bool - - @Binding var activePatientId: String? - - - var startOfDays: [Date] { - Array(eventContextsByDate.keys) - } - - - var body: some View { - // swiftlint:disable:next closure_body_length - NavigationStack { - ZStack { - if activePatientId != nil { - List(startOfDays, id: \.timeIntervalSinceNow) { startOfDay in - Section(format(startOfDay: startOfDay)) { - ForEach(eventContextsByDate[startOfDay] ?? [], id: \.event) { eventContext in - EventContextView(eventContext: eventContext) - .onTapGesture { - if !eventContext.event.complete { - presentedContext = eventContext - } - } - } - } - } - } else { - NoInformationText { - Text("No Patient selected") - } caption: { - Text("Select a patient to continue.") - } - } - } - .onChange(of: scheduler, initial: true) { - calculateEventContextsByDate() - } - .sheet(item: $presentedContext) { presentedContext in - destination(withContext: presentedContext) - } - .sheet(isPresented: $presentingMuseList) { - NearbyDevices(eegModel: eegModel) - } - .sheet(isPresented: $presentPatientSheet) { - PatientListSheet(activePatientId: $activePatientId) - } - .navigationTitle(Text("Schedule", comment: "Schedule Title")) - .toolbar { - toolbar - } - } - } - - @ToolbarContentBuilder private var toolbar: some ToolbarContent { - if activePatientId != nil { - ToolbarItem(placement: .navigationBarLeading) { - Button(action: { - presentingMuseList = true - }) { - Image(systemName: "brain.head.profile").symbolRenderingMode(.hierarchical) - .accessibilityLabel("NEARBY_DEVICES") - } - } - } - - ToolbarItem(placement: .principal) { - Button(action: { - presentPatientSheet = true - }, label: { - CurrentPatientLabel(activePatient: $activePatientId) - }) - } - if AccountButton.shouldDisplay { - ToolbarItem(placement: .primaryAction) { - AccountButton(isPresented: $presentingAccount) - } - } - } - - - init(presentingAccount: Binding, activePatientId: Binding, eegModel: EEGViewModel) { - self._presentingAccount = presentingAccount - self._activePatientId = activePatientId - self.eegModel = eegModel - } - - - private func destination(withContext eventContext: EventContext) -> some View { - @ViewBuilder var destination: some View { - switch eventContext.task.context { - case let .questionnaire(questionnaire): - QuestionnaireView(questionnaire: questionnaire) { _ in - _Concurrency.Task { - await eventContext.event.complete(true) - } - } - case let .test(string): - ModalView(text: string, buttonText: "Close") { - _Concurrency.Task { - await eventContext.event.complete(true) - } - } - } - } - - return destination - } - - - private func format(startOfDay: Date) -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .long - dateFormatter.timeStyle = .none - return dateFormatter.string(from: startOfDay) - } - - private func calculateEventContextsByDate() { - let eventContexts = scheduler.tasks.flatMap { task in - task - .events( - from: Calendar.current.startOfDay(for: .now), - to: .numberOfEventsOrEndDate(100, .now) - ) - .map { event in - EventContext(event: event, task: task) - } - } - .sorted() - - let newEventContextsByDate = Dictionary(grouping: eventContexts) { eventContext in - Calendar.current.startOfDay(for: eventContext.event.scheduledAt) - } - - if newEventContextsByDate != eventContextsByDate { - eventContextsByDate = newEventContextsByDate - } - } -} - - -#if DEBUG -#Preview { - ScheduleView(presentingAccount: .constant(true), activePatientId: .constant("1"), eegModel: EEGViewModel(deviceManager: MockDeviceManager())) - .environmentObject(NAMSScheduler(testSchedule: true)) - .environmentObject(Account(MockUserIdPasswordAccountService())) - .environment(PatientListModel()) -} - -#Preview { - let model = EEGViewModel(deviceManager: MockDeviceManager()) - let details = AccountDetails.Builder() - .set(\.userId, value: "lelandstanford@stanford.edu") - .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) - - return ScheduleView(presentingAccount: .constant(true), activePatientId: .constant("1"), eegModel: model) - .environmentObject(NAMSScheduler(testSchedule: true)) - .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService())) - .environment(PatientListModel()) -} -#endif diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift new file mode 100644 index 0000000..16ab96f --- /dev/null +++ b/NAMS/ScheduleView.swift @@ -0,0 +1,104 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziAccount +import SwiftUI + + +struct ScheduleView2: View { + @ObservedObject var eegModel: EEGViewModel + + @State var presentingMuseList = false + @State var presentPatientSheet = false + @Binding var presentingAccount: Bool + + @Binding var activePatientId: String? + + var body: some View { + NavigationStack { + ZStack { + if activePatientId == nil { + VStack { + NoInformationText { + Text("No Patient selected") + } caption: { + Text("Select a patient to continue.") + } + + Button(action: { + presentPatientSheet = true + }) { + Text("Select Patient") + } + .padding() + } + } else { + PatientTiles() + } + } + .sheet(isPresented: $presentingMuseList) { + NearbyDevices(eegModel: eegModel) + } + .sheet(isPresented: $presentPatientSheet) { + PatientListSheet(activePatientId: $activePatientId) + } + .navigationTitle(Text("Schedule", comment: "Schedule Title")) + .toolbar { + toolbar + } + } + } + + @ToolbarContentBuilder private var toolbar: some ToolbarContent { + if activePatientId != nil { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { + presentingMuseList = true + }) { + Image(systemName: "brain.head.profile").symbolRenderingMode(.hierarchical) + .accessibilityLabel("NEARBY_DEVICES") + } + } + } + + ToolbarItem(placement: .principal) { + Button(action: { + presentPatientSheet = true + }, label: { + CurrentPatientLabel(activePatient: $activePatientId) + }) + } + if AccountButton.shouldDisplay { + ToolbarItem(placement: .primaryAction) { + AccountButton(isPresented: $presentingAccount) + } + } + } + + + init(presentingAccount: Binding, activePatientId: Binding, eegModel: EEGViewModel) { + self._presentingAccount = presentingAccount + self._activePatientId = activePatientId + self.eegModel = eegModel + } +} + + +#if DEBUG +#Preview { + ScheduleView2(presentingAccount: .constant(true), activePatientId: .constant(nil), eegModel: EEGViewModel(deviceManager: MockDeviceManager())) + .environmentObject(Account(MockUserIdPasswordAccountService())) + .environment(PatientListModel()) +} + +#Preview { + ScheduleView2(presentingAccount: .constant(true), activePatientId: .constant("1"), eegModel: EEGViewModel(deviceManager: MockDeviceManager())) + .environmentObject(Account(MockUserIdPasswordAccountService())) + .environment(PatientListModel()) +} +#endif diff --git a/NAMS/Tiles/PatientTiles.swift b/NAMS/Tiles/PatientTiles.swift new file mode 100644 index 0000000..d175ec7 --- /dev/null +++ b/NAMS/Tiles/PatientTiles.swift @@ -0,0 +1,71 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziQuestionnaire +import SpeziViews +import SwiftUI + + +@MainActor +struct PatientTiles: View { + @Environment(PatientListModel.self) + private var patientList + + @State private var viewState: ViewState = .idle + @State private var presentingQuestionnaire: Questionnaire? + + private var isPresentedBinding: Binding { + Binding { + presentingQuestionnaire != nil + } set: { newValue in + if !newValue { + presentingQuestionnaire = nil + } + } + } + + private var questionnaires: [PatientQuestionnaire] { + guard let completedList = patientList.completedQuestionnaires else { + return PatientQuestionnaire.all + } + + return PatientQuestionnaire.all.sorted { lhs, rhs in + !completedList.contains(lhs.id) && completedList.contains(rhs.id) + } + } + + var body: some View { + if patientList.questionnaires == nil { + ProgressView() + } else { + List { + Section("Screening") { + ForEach(questionnaires) { questionnaire in + QuestionnaireTile(patientQuestionnaire: questionnaire, presentingItem: $presentingQuestionnaire) + } + } + } + .viewStateAlert(state: $viewState) + .sheet(item: $presentingQuestionnaire) { questionnaire in + QuestionnaireView(questionnaire: questionnaire, isPresented: isPresentedBinding) { response in + do { + try await patientList.add(response: response) + } catch { + viewState = .error(AnyLocalizedError(error: error)) + } + } + } + } + } +} + + +#Preview { + PatientTiles() + .environment(PatientListModel()) +} diff --git a/NAMS/Tiles/QuestionnaireTile.swift b/NAMS/Tiles/QuestionnaireTile.swift new file mode 100644 index 0000000..27070dc --- /dev/null +++ b/NAMS/Tiles/QuestionnaireTile.swift @@ -0,0 +1,101 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziFHIR +import SwiftUI + + +@MainActor +struct QuestionnaireTile: View { + private let patientQuestionnaire: PatientQuestionnaire + + @Environment(PatientListModel.self) + private var patientList + + @Binding private var presentingItem: Questionnaire? + + @MainActor private var completed: Bool { + patientList.completedQuestionnaires?.contains(patientQuestionnaire.id) == true + } + + var body: some View { + VStack(alignment: .leading) { + tileHeader + + Divider() + + Text(patientQuestionnaire.description) + .font(.callout) + + if !completed { + Button(action: { + presentingItem = patientQuestionnaire.questionnaire + }) { + Text("Start \(patientQuestionnaire.tileType.localizedStringResource)") + .frame(maxWidth: .infinity, minHeight: 30) + } + .buttonStyle(.borderedProminent) + .padding(.top, 8) + } + } + .containerShape(Rectangle()) + } + + @ViewBuilder private var tileHeader: some View { + HStack { + if completed { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + .font(.system(size: 30)) + .accessibilityHidden(true) + } else { + Image(systemName: "list.bullet.clipboard") + .foregroundColor(.mint) + .font(.system(size: 30)) + .accessibilityHidden(true) + } + + VStack(alignment: .leading, spacing: 4) { + Text(patientQuestionnaire.title) + .font(.headline) + + HStack { + if completed { + Text("Completed", comment: "Completed Task. Subtitle.") + } else { + Text(patientQuestionnaire.tileType.localizedStringResource) + + Spacer() + Text("\(patientQuestionnaire.expectedCompletionMinutes) min", comment: "Expected task completion in minutes.") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + .font(.subheadline) + .foregroundColor(.secondary) + .accessibilityElement(children: .combine) + } + } + } + + + init(patientQuestionnaire: PatientQuestionnaire, presentingItem: Binding) { + self.patientQuestionnaire = patientQuestionnaire + self._presentingItem = presentingItem + } +} + + +#if DEBUG +#Preview { + List { + QuestionnaireTile(patientQuestionnaire: .mChatRF, presentingItem: .constant(nil)) + .environment(PatientListModel()) + } +} +#endif diff --git a/NAMS/Tiles/TileType.swift b/NAMS/Tiles/TileType.swift new file mode 100644 index 0000000..0059c88 --- /dev/null +++ b/NAMS/Tiles/TileType.swift @@ -0,0 +1,21 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +enum TileType: CustomLocalizedStringResourceConvertible { + case questionnaire + + var localizedStringResource: LocalizedStringResource { + switch self { + case .questionnaire: + return .init("Questionnaire", comment: "Tile Type") + } + } +} diff --git a/NAMS/Utils/Errors/QuestionnaireError.swift b/NAMS/Utils/Errors/QuestionnaireError.swift new file mode 100644 index 0000000..c8872af --- /dev/null +++ b/NAMS/Utils/Errors/QuestionnaireError.swift @@ -0,0 +1,39 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +enum QuestionnaireError: LocalizedError { + case unexpectedFormat + case missingPatient + case failedQuestionnaireMatch + + private var errorDescriptionResource: LocalizedStringResource { + "Failed to complete Questionnaire" + } + + var errorDescription: String? { + String(localized: errorDescriptionResource) + } + + private var failureReasonResource: LocalizedStringResource { + switch self { + case .unexpectedFormat: + return "Unexpected format of the questionnaire response!" + case .missingPatient: + return "There was no selected patient found!" + case .failedQuestionnaireMatch: + return "Failed to associate response with original questionnaire!" + } + } + + var failureReason: String? { + String(localized: failureReasonResource) + } +} diff --git a/NAMS/Helper/Binding+Negate.swift b/NAMS/Utils/Helper/Binding+Negate.swift similarity index 100% rename from NAMS/Helper/Binding+Negate.swift rename to NAMS/Utils/Helper/Binding+Negate.swift diff --git a/NAMS/Helper/Bundle+Image.swift b/NAMS/Utils/Helper/Bundle+Image.swift similarity index 100% rename from NAMS/Helper/Bundle+Image.swift rename to NAMS/Utils/Helper/Bundle+Image.swift diff --git a/NAMS/Helper/CodableArray+RawRepresentable.swift b/NAMS/Utils/Helper/CodableArray+RawRepresentable.swift similarity index 100% rename from NAMS/Helper/CodableArray+RawRepresentable.swift rename to NAMS/Utils/Helper/CodableArray+RawRepresentable.swift diff --git a/NAMS/Helper/CollectionReference+AsyncAwait.swift b/NAMS/Utils/Helper/CollectionReference+AsyncAwait.swift similarity index 100% rename from NAMS/Helper/CollectionReference+AsyncAwait.swift rename to NAMS/Utils/Helper/CollectionReference+AsyncAwait.swift diff --git a/NAMS/Helper/ProcessInfo+PreviewSimulator.swift b/NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift similarity index 100% rename from NAMS/Helper/ProcessInfo+PreviewSimulator.swift rename to NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift diff --git a/NAMS/Misc/NoInformationText.swift b/NAMS/Utils/NoInformationText.swift similarity index 100% rename from NAMS/Misc/NoInformationText.swift rename to NAMS/Utils/NoInformationText.swift diff --git a/NAMS/Misc/StorageKeys.swift b/NAMS/Utils/StorageKeys.swift similarity index 100% rename from NAMS/Misc/StorageKeys.swift rename to NAMS/Utils/StorageKeys.swift diff --git a/NAMS/Testing/FeatureFlags.swift b/NAMS/Utils/Testing/FeatureFlags.swift similarity index 100% rename from NAMS/Testing/FeatureFlags.swift rename to NAMS/Utils/Testing/FeatureFlags.swift diff --git a/NAMSUITests/ContactsTests.swift b/NAMSUITests/ContactsTests.swift index e5b2918..381439a 100644 --- a/NAMSUITests/ContactsTests.swift +++ b/NAMSUITests/ContactsTests.swift @@ -23,7 +23,7 @@ class ContactsTests: XCTestCase { func testContacts() throws { let app = XCUIApplication() - + XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Contacts"].waitForExistence(timeout: 2)) app.tabBars["Tab Bar"].buttons["Contacts"].tap() diff --git a/NAMSUITests/PatientInformationTests.swift b/NAMSUITests/PatientInformationTests.swift index ada53e2..62b00af 100644 --- a/NAMSUITests/PatientInformationTests.swift +++ b/NAMSUITests/PatientInformationTests.swift @@ -28,26 +28,26 @@ final class PatientInformationTests: XCTestCase { app.tabBars["Tab Bar"].buttons["Schedule"].tap() - XCTAssertTrue(app.buttons["Leland Stanford"].waitForExistence(timeout: 2.0)) - app.buttons["Leland Stanford"].tap() + XCTAssertTrue(app.buttons["Example Patient"].waitForExistence(timeout: 2.0)) + app.buttons["Example Patient"].tap() XCTAssertTrue(app.navigationBars["Patients"].waitForExistence(timeout: 6.0)) - XCTAssertTrue(app.buttons["Selected Patient: Leland Stanford"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.buttons["Leland Stanford, Selected"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.buttons["Selected Patient: Example Patient"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.buttons["Example Patient, Selected"].waitForExistence(timeout: 0.5)) - app.buttons["Leland Stanford, Selected"].tap() + app.buttons["Example Patient, Selected"].tap() XCTAssertTrue(app.staticTexts["No Patient selected"].waitForExistence(timeout: 2.0)) - XCTAssertTrue(app.buttons["Select Patient"].waitForExistence(timeout: 2.0)) - app.buttons["Select Patient"].tap() + XCTAssertTrue(app.navigationBars.buttons["Select Patient"].waitForExistence(timeout: 2.0)) + app.navigationBars.buttons["Select Patient"].tap() XCTAssertTrue(app.navigationBars["Patients"].waitForExistence(timeout: 6.0)) - XCTAssertTrue(app.buttons["Leland Stanford"].waitForExistence(timeout: 0.5)) - app.buttons["Leland Stanford"].tap() + XCTAssertTrue(app.buttons["Example Patient"].waitForExistence(timeout: 0.5)) + app.buttons["Example Patient"].tap() - XCTAssertTrue(app.buttons["Leland Stanford"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.buttons["Example Patient"].waitForExistence(timeout: 0.5)) } func testPatientInformationDetails() { @@ -57,16 +57,16 @@ final class PatientInformationTests: XCTestCase { app.tabBars["Tab Bar"].buttons["Schedule"].tap() - XCTAssertTrue(app.buttons["Leland Stanford"].waitForExistence(timeout: 2.0)) - app.buttons["Leland Stanford"].tap() + XCTAssertTrue(app.buttons["Example Patient"].waitForExistence(timeout: 2.0)) + app.buttons["Example Patient"].tap() XCTAssertTrue(app.navigationBars["Patients"].waitForExistence(timeout: 6.0)) - XCTAssertTrue(app.buttons["Selected Patient: Leland Stanford"].waitForExistence(timeout: 0.5)) - app.buttons["Selected Patient: Leland Stanford"].tap() + XCTAssertTrue(app.buttons["Selected Patient: Example Patient"].waitForExistence(timeout: 0.5)) + app.buttons["Selected Patient: Example Patient"].tap() XCTAssertTrue(app.navigationBars.staticTexts["Patient Overview"].waitForExistence(timeout: 6.0)) - XCTAssertTrue(app.staticTexts["Leland Stanford"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.staticTexts["Example Patient"].waitForExistence(timeout: 0.5)) XCTAssertTrue(app.buttons["Delete Patient"].waitForExistence(timeout: 0.5)) XCTAssertFalse(app.buttons["Select Patient"].waitForExistence(timeout: 0.5)) @@ -75,12 +75,12 @@ final class PatientInformationTests: XCTestCase { app.navigationBars.buttons["Patients"].tap() - XCTAssertTrue(app.buttons["Leland Stanford, Selected"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.buttons["Leland Stanford, Patient Details"].waitForExistence(timeout: 0.5)) - app.buttons["Leland Stanford, Patient Details"].tap() + XCTAssertTrue(app.buttons["Example Patient, Selected"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.buttons["Example Patient, Patient Details"].waitForExistence(timeout: 0.5)) + app.buttons["Example Patient, Patient Details"].tap() XCTAssertTrue(app.navigationBars.staticTexts["Patient Overview"].waitForExistence(timeout: 6.0)) - XCTAssertTrue(app.staticTexts["Leland Stanford"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.staticTexts["Example Patient"].waitForExistence(timeout: 0.5)) // back button XCTAssertTrue(app.navigationBars.buttons["Patients"].waitForExistence(timeout: 0.5)) @@ -98,8 +98,8 @@ final class PatientInformationTests: XCTestCase { app.tabBars["Tab Bar"].buttons["Schedule"].tap() - XCTAssertTrue(app.buttons["Leland Stanford"].waitForExistence(timeout: 6.0)) - app.buttons["Leland Stanford"].tap() + XCTAssertTrue(app.buttons["Example Patient"].waitForExistence(timeout: 6.0)) + app.buttons["Example Patient"].tap() XCTAssertTrue(app.navigationBars["Patients"].waitForExistence(timeout: 6.0)) @@ -141,13 +141,13 @@ final class PatientInformationTests: XCTestCase { app.tabBars["Tab Bar"].buttons["Schedule"].tap() - XCTAssertTrue(app.buttons["Leland Stanford"].waitForExistence(timeout: 2.0)) - app.buttons["Leland Stanford"].tap() + XCTAssertTrue(app.buttons["Example Patient"].waitForExistence(timeout: 2.0)) + app.buttons["Example Patient"].tap() XCTAssertTrue(app.navigationBars["Patients"].waitForExistence(timeout: 6.0)) - XCTAssertTrue(app.buttons["Selected Patient: Leland Stanford"].waitForExistence(timeout: 0.5)) - app.buttons["Selected Patient: Leland Stanford"].tap() + XCTAssertTrue(app.buttons["Selected Patient: Example Patient"].waitForExistence(timeout: 0.5)) + app.buttons["Selected Patient: Example Patient"].tap() XCTAssertTrue(app.navigationBars.staticTexts["Patient Overview"].waitForExistence(timeout: 6.0)) @@ -163,8 +163,8 @@ final class PatientInformationTests: XCTestCase { XCTAssertTrue(app.navigationBars["Patients"].waitForExistence(timeout: 6.0)) - XCTAssertFalse(app.buttons["Leland Stanford"].waitForExistence(timeout: 0.5)) - XCTAssertFalse(app.buttons["Leland Stanford, Selected"].waitForExistence(timeout: 0.5)) - XCTAssertFalse(app.buttons["Selected Patient: Leland Stanford"].waitForExistence(timeout: 0.5)) + XCTAssertFalse(app.buttons["Example Patient"].waitForExistence(timeout: 0.5)) + XCTAssertFalse(app.buttons["Example Patient, Selected"].waitForExistence(timeout: 0.5)) + XCTAssertFalse(app.buttons["Selected Patient: Example Patient"].waitForExistence(timeout: 0.5)) } } diff --git a/NAMSUITests/ScheduleTests.swift b/NAMSUITests/ScheduleTests.swift new file mode 100644 index 0000000..9a38c1a --- /dev/null +++ b/NAMSUITests/ScheduleTests.swift @@ -0,0 +1,63 @@ +// +// This source file is part of the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import XCTest +import XCTestExtensions + + +class ScheduleTests: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + + continueAfterFailure = false + + let app = XCUIApplication() + app.launchArguments = ["--skipOnboarding", "--testSchedule", "--inject-default-patient"] + app.deleteAndLaunch(withSpringboardAppName: "NAMS") + } + + + func testSchedule() throws { + let app = XCUIApplication() + + XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Schedule"].waitForExistence(timeout: 2)) + app.tabBars["Tab Bar"].buttons["Schedule"].tap() + + XCTAssertTrue(app.staticTexts["SCREENING"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.staticTexts["M-CHAT R/F"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.staticTexts["Questionnaire, 5-10 min"].waitForExistence(timeout: 0.5)) + + XCTAssertTrue(app.buttons["Start Questionnaire"].waitForExistence(timeout: 0.5)) + app.buttons["Start Questionnaire"].tap() + + XCTAssertTrue(app.navigationBars.buttons["Cancel"].waitForExistence(timeout: 4)) + XCTAssertTrue(app.staticTexts["M-CHAT R/F"].waitForExistence(timeout: 0.5)) + + while true { + if app.staticTexts["Yes"].exists { + app.staticTexts["Yes"].tap() + + if app.buttons["Next"].exists { + app.buttons["Next"].tap() + usleep(500_000) + } else if app.buttons["Done"].exists { + app.buttons["Done"].tap() + usleep(500_000) + break + } else { + XCTFail("Couldn't navigate questionnaire!") + } + } else { + XCTFail("Couldn't navigate questionnaire!") + } + } + + + XCTAssertTrue(app.staticTexts["Completed"].waitForExistence(timeout: 2.0)) + } +} diff --git a/NAMSUITests/SchedulerTests.swift b/NAMSUITests/SchedulerTests.swift deleted file mode 100644 index 0877e32..0000000 --- a/NAMSUITests/SchedulerTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// This source file is part of the Stanford Spezi Template Application project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import XCTest -import XCTestExtensions - - -class SchedulerTests: XCTestCase { - override func setUpWithError() throws { - try super.setUpWithError() - - continueAfterFailure = false - - let app = XCUIApplication() - app.launchArguments = ["--skipOnboarding", "--testSchedule", "--inject-default-patient"] - app.deleteAndLaunch(withSpringboardAppName: "NAMS") - } - - - func testScheduler() throws { - let app = XCUIApplication() - - XCTAssertTrue(app.tabBars["Tab Bar"].buttons["Schedule"].waitForExistence(timeout: 2)) - app.tabBars["Tab Bar"].buttons["Schedule"].tap() - - XCTAssertTrue(app.staticTexts["Start Questionnaire"].waitForExistence(timeout: 2)) - app.staticTexts["Start Questionnaire"].tap() - - XCTAssertTrue(app.staticTexts["Social Support"].waitForExistence(timeout: 2)) - } -}