├── Backend ├── Tests │ ├── GlobalUsings.cs │ ├── Integrations │ │ └── IntegrationTest.cs │ ├── Tests.csproj │ └── Unit │ │ └── Logic │ │ └── EventsLogicTests.cs ├── Api │ ├── Data │ │ ├── Scripts │ │ │ ├── 009-Index.sql │ │ │ ├── 008-MetricsSource.sql │ │ │ ├── 005-Settings.sql │ │ │ ├── 014-IndexMetrics.sql │ │ │ ├── 012-Admin.sql │ │ │ ├── 006-Metrics.sql │ │ │ ├── 013-MetricsImport.sql │ │ │ ├── 001-Files.sql │ │ │ ├── 004-HealthData.sql │ │ │ ├── 011-Events.sql │ │ │ ├── 010-Metrics.sql │ │ │ └── 002-Users.sql │ │ ├── IDataBase.cs │ │ ├── Models │ │ │ ├── Files.cs │ │ │ ├── Settings.cs │ │ │ ├── Person.cs │ │ │ ├── Address.cs │ │ │ ├── Blob.cs │ │ │ ├── EventType.cs │ │ │ ├── MetricType.cs │ │ │ ├── Treatment.cs │ │ │ ├── Prescription.cs │ │ │ ├── Right.cs │ │ │ ├── User.cs │ │ │ ├── Metric.cs │ │ │ └── Event.cs │ │ ├── MigrationHelper.cs │ │ └── SettingsContext.cs │ ├── Models │ │ ├── EventSummary.cs │ │ ├── TreatmentType.cs │ │ ├── RightType.cs │ │ ├── EventTypes.cs │ │ ├── UserType.cs │ │ ├── Connection.cs │ │ ├── Treatment.cs │ │ ├── FileTypes.cs │ │ ├── Right.cs │ │ ├── Metric.cs │ │ ├── AdminSettings.cs │ │ ├── Person.cs │ │ ├── Event.cs │ │ └── MetricType.cs │ ├── Logic │ │ ├── Import │ │ │ ├── Redmi │ │ │ │ ├── SleepType.cs │ │ │ │ ├── SleepTime.cs │ │ │ │ ├── RedmiRecord.cs │ │ │ │ ├── Record.cs │ │ │ │ └── SleepRecord.cs │ │ │ ├── ListImporter.cs │ │ │ ├── Clue │ │ │ │ └── ClueItem.cs │ │ │ ├── FileImporter.cs │ │ │ └── ClueImporter.cs │ │ ├── ImportLogic.cs │ │ ├── TreatmentLogic.cs │ │ ├── PatientsLogic.cs │ │ └── SettingsLogic.cs │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Helpers │ │ ├── SettingsHelper.cs │ │ ├── Auth │ │ │ ├── TokenHelper.cs │ │ │ ├── PasswordLogic.cs │ │ │ ├── ProxyAuthLogic.cs │ │ │ └── TokenService.cs │ │ ├── Helper.cs │ │ ├── UserHelper.cs │ │ └── RightsHelper.cs │ ├── Dockerfile │ ├── Properties │ │ └── launchSettings.json │ └── Api.csproj └── API.sln ├── flutter_frontend ├── devtools_options.yaml ├── linux │ ├── .gitignore │ ├── main.cc │ ├── flutter │ │ ├── generated_plugin_registrant.h │ │ ├── generated_plugins.cmake │ │ ├── generated_plugin_registrant.cc │ │ └── CMakeLists.txt │ └── my_application.h ├── lib │ ├── helpers │ │ ├── url_dummy.dart │ │ ├── pair.dart │ │ ├── url.dart │ │ ├── metric_helper.dart │ │ ├── users.dart │ │ ├── translation.dart │ │ ├── oauth.dart │ │ └── date.dart │ ├── services │ │ ├── swagger │ │ │ └── generated_code │ │ │ │ ├── client_mapping.dart │ │ │ │ ├── client_index.dart │ │ │ │ └── swagger.enums.swagger.dart │ │ ├── login_service.dart │ │ ├── treatment_service.dart │ │ ├── setting_service.dart │ │ ├── helper_service.dart │ │ ├── user_service.dart │ │ ├── account.dart │ │ ├── metric_service.dart │ │ ├── api_service.dart │ │ └── event_service.dart │ ├── logic │ │ ├── event.dart │ │ ├── settings │ │ │ ├── health_settings.dart │ │ │ ├── theme_settings.dart │ │ │ ├── events_settings.dart │ │ │ ├── metrics_settings.dart │ │ │ └── ordered_item.dart │ │ ├── theme_helper.dart │ │ ├── account │ │ │ └── authentication_bloc.dart │ │ ├── fit │ │ │ └── task_bloc.dart │ │ └── d_i.dart │ └── ui │ │ ├── common │ │ ├── square_outline_input_border.dart │ │ ├── square_dialog.dart │ │ ├── notification.dart │ │ ├── statefull_check.dart │ │ ├── file_input.dart │ │ ├── custom_switch.dart │ │ ├── type_input.dart │ │ ├── password_input.dart │ │ ├── square_text_field.dart │ │ ├── loader.dart │ │ ├── date_range_input.dart │ │ ├── date_range_picker.dart │ │ └── ordered_list.dart │ │ ├── splash.dart │ │ ├── care_dashboard.dart │ │ ├── blocs │ │ ├── events │ │ │ ├── delete_event.dart │ │ │ └── events_grid.dart │ │ ├── care │ │ │ ├── patients_dashboard.dart │ │ │ └── agenda.dart │ │ ├── administration │ │ │ ├── events │ │ │ │ ├── event_form.dart │ │ │ │ └── event_settings.dart │ │ │ ├── users │ │ │ │ └── user_change_role.dart │ │ │ └── metrics │ │ │ │ └── metrics_settings.dart │ │ ├── treatments │ │ │ └── treatments_grid.dart │ │ ├── metrics │ │ │ └── metrics_grid.dart │ │ └── calendar │ │ │ └── calendar_view.dart │ │ ├── patient_dashboard.dart │ │ ├── admin_dashboard.dart │ │ ├── dashboard.dart │ │ └── task_status_dialog.dart ├── web │ ├── favicon-32x32.png │ ├── icons │ │ ├── Icon-192.png │ │ └── Icon-310.png │ ├── manifest.json │ └── index.html ├── android │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── ic_launcher-playstore.png │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ │ ├── values │ │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── fschiltz │ │ │ │ │ └── helse │ │ │ │ │ └── MainActivity.kt │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ ├── local.properties │ │ └── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle │ └── settings.gradle ├── build.yaml ├── ios │ ├── Runner │ │ ├── GeneratedPluginRegistrant.h │ │ └── GeneratedPluginRegistrant.m │ └── Flutter │ │ ├── Generated.xcconfig │ │ └── flutter_export_environment.sh ├── README.md ├── .gitignore ├── .metadata ├── test │ └── widget_test.dart └── analysis_options.yaml ├── Assets └── cardiogram.png ├── .vscode ├── settings.json └── launch.json ├── .editorconfig ├── .dockerignore ├── .github ├── workflows │ ├── release.yml │ ├── cd.yml │ ├── build-docker.yml │ ├── ci.yml │ └── build-android.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── dependabot.yml ├── docker-compose.yaml ├── docker-compose.debug.yml ├── LICENSE └── Dockerfile /Backend/Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /flutter_frontend/devtools_options.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | -------------------------------------------------------------------------------- /flutter_frontend/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /Assets/cardiogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/Assets/cardiogram.png -------------------------------------------------------------------------------- /flutter_frontend/lib/helpers/url_dummy.dart: -------------------------------------------------------------------------------- 1 | class UrlHelper { 2 | static void removeParam() {} 3 | } 4 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/009-Index.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX healthMetricSource ON health.Metric (source) INCLUDE (tag); -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/008-MetricsSource.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE health.Metric ADD COLUMN source INT NOT NULL DEFAULT 0; 2 | 3 | -------------------------------------------------------------------------------- /Backend/Api/Models/EventSummary.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public record EventSummary(Dictionary Data); -------------------------------------------------------------------------------- /Backend/Api/Models/TreatmentType.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public enum TreatmentType 4 | { 5 | Care, 6 | } 7 | -------------------------------------------------------------------------------- /flutter_frontend/web/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/web/favicon-32x32.png -------------------------------------------------------------------------------- /flutter_frontend/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/web/icons/Icon-192.png -------------------------------------------------------------------------------- /flutter_frontend/web/icons/Icon-310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/web/icons/Icon-310.png -------------------------------------------------------------------------------- /Backend/Api/Models/RightType.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public enum RightType 4 | { 5 | View = 1, 6 | Edit = 2, 7 | } -------------------------------------------------------------------------------- /flutter_frontend/lib/helpers/pair.dart: -------------------------------------------------------------------------------- 1 | class Pair { 2 | final T1 a; 3 | final T2 b; 4 | 5 | Pair(this.a, this.b); 6 | } 7 | -------------------------------------------------------------------------------- /flutter_frontend/lib/services/swagger/generated_code/client_mapping.dart: -------------------------------------------------------------------------------- 1 | final Map)> generatedMapping = {}; 2 | -------------------------------------------------------------------------------- /Backend/Api/Models/EventTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public enum EventTypes 4 | { 5 | Sleep = 1, 6 | Care = 2, 7 | Workout = 3, 8 | 9 | } -------------------------------------------------------------------------------- /flutter_frontend/lib/services/swagger/generated_code/client_index.dart: -------------------------------------------------------------------------------- 1 | export 'swagger.swagger.dart' show Swagger; 2 | export 'swagger.swagger.dart' show Swagger; 3 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/005-Settings.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA admin; 2 | 3 | CREATE TABLE admin.Settings 4 | ( 5 | Name VARCHAR PRIMARY KEY NOT NULL, 6 | Blob json NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /Backend/Api/Data/IDataBase.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Data; 2 | 3 | namespace Api.Data; 4 | 5 | public interface IContext { 6 | Task BeginTransactionAsync(); 7 | } 8 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "Backend/API.sln", 3 | "java.compile.nullAnalysis.mode": "automatic", 4 | "java.configuration.updateBuildConfiguration": "automatic" 5 | } -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #7AFFB5 4 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /Backend/Api/Logic/Import/Redmi/SleepType.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Logic.Import.Redmi; 2 | 3 | internal enum SleepType 4 | { 5 | None, 6 | State1, 7 | State2, 8 | State3, 9 | State4, 10 | } 11 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FSchiltz/Helse/HEAD/flutter_frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /flutter_frontend/lib/helpers/url.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html' as html; 2 | 3 | class UrlHelper { 4 | static void removeParam() { 5 | html.window.history.replaceState(html.window.history.state, 'title', '/'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/kotlin/com/fschiltz/helse/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.fschiltz.helse 2 | 3 | import io.flutter.embedding.android.FlutterFragmentActivity 4 | 5 | class MainActivity : FlutterFragmentActivity() 6 | -------------------------------------------------------------------------------- /flutter_frontend/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char **argv) 4 | { 5 | g_autoptr(MyApplication) app = my_application_new(); 6 | return g_application_run(G_APPLICATION(app), argc, argv); 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = false 10 | insert_final_newline = false 11 | -------------------------------------------------------------------------------- /flutter_frontend/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/event.dart: -------------------------------------------------------------------------------- 1 | enum SubmissionStatus { initial, success, failure, inProgress, waiting, skipped } 2 | 3 | sealed class ChangedEvent { 4 | const ChangedEvent(this.value, this.field); 5 | 6 | final String field; 7 | final T? value; 8 | } 9 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/014-IndexMetrics.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP INDEX health.metric_date_type_personid_idx; 3 | 4 | 5 | CREATE INDEX IF NOT EXISTS metric_date_type_personid_idx 6 | ON health.metric USING btree 7 | (date ASC NULLS LAST) 8 | INCLUDE(type, personid, value); -------------------------------------------------------------------------------- /Backend/Api/Models/UserType.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | [Flags] 4 | public enum UserType 5 | { 6 | Patient = 0, 7 | Admin = 1, 8 | Caregiver = 2, 9 | User = 4, 10 | CareWithSelf = User | Caregiver, 11 | Superuser = Admin | User | Caregiver, 12 | } -------------------------------------------------------------------------------- /flutter_frontend/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip 6 | -------------------------------------------------------------------------------- /Backend/Api/Models/Connection.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | /// 4 | /// Data needed to start a connection 5 | /// 6 | /// 7 | /// 8 | public record Connection(string User, string Password, string? Redirect); 9 | -------------------------------------------------------------------------------- /Backend/Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "LinqToDB.Data.DataConnection": "Warning", 7 | "Auth" : "Information" 8 | } 9 | }, 10 | "AllowedHosts": "*" 11 | } 12 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/012-Admin.sql: -------------------------------------------------------------------------------- 1 | 2 | -- User move to the new flag 3 | UPDATE person.User SET Type = 4 where Type = 1; 4 | 5 | -- Caregiver move to the new flag 6 | UPDATE person.User SET Type = 6 where Type = 3; 7 | 8 | -- Admin move to the new flag 9 | UPDATE person.User SET Type = 1 where Type = 2; -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Files.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "file")] 6 | public class Files 7 | { 8 | [PrimaryKey, Identity] 9 | public long Id { get; set; } 10 | 11 | [Column, NotNull] 12 | public DateTime Creation { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /Backend/Api/Logic/Import/Redmi/SleepTime.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Logic.Import.Redmi; 2 | 3 | public class SleepTime 4 | { 5 | public int End_time { get; set; } 6 | public int State { get; set; } 7 | public int Start_time { get; set; } 8 | 9 | public string GetKey() => Start_time + "_" + End_time; 10 | } 11 | -------------------------------------------------------------------------------- /flutter_frontend/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | swagger_dart_code_generator: 5 | options: 6 | input_folder: "lib/services/swagger/" 7 | output_folder: "lib/services/swagger/generated_code/" 8 | input_urls: 9 | - url: http://localhost:5059/swagger/v1/swagger.json -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/006-Metrics.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE health.MetricType ADD COLUMN IF NOT EXISTS summaryType BIGINT NOT NULL DEFAULT 0; 2 | ALTER TABLE health.MetricType ALTER COLUMN summaryType DROP DEFAULT; 3 | 4 | ALTER TABLE health.MetricType ADD COLUMN IF NOT EXISTS Type BIGINT NOT NULL DEFAULT 0; 5 | ALTER TABLE health.MetricType ALTER COLUMN Type DROP DEFAULT; 6 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/square_outline_input_border.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SquareOutlineInputBorder extends OutlineInputBorder { 4 | SquareOutlineInputBorder(Color color) 5 | : super( 6 | borderRadius: BorderRadius.circular(0), 7 | borderSide: BorderSide(color: color), 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/013-MetricsImport.sql: -------------------------------------------------------------------------------- 1 | 2 | INSERT INTO health.MetricType(id, description, name, unit, type, summaryType, usereditable) 3 | VALUES (12, null, 'Medication', '', 0, 0, false), 4 | (13, null, 'Tests', '', 0, 0, false), 5 | (14, null, 'Sex', '', 0, 0, false), 6 | (15, null, 'Stool', '', 0, 0, false), 7 | (16, null, 'Spotting', '', 0, 0, false); -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Settings.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB; 2 | using LinqToDB.Mapping; 3 | 4 | namespace Api.Data.Models; 5 | 6 | [Table(Schema = "admin")] 7 | public class Settings 8 | { 9 | [PrimaryKey] 10 | public required string Name { get; set; } 11 | 12 | [Column, NotNull, DataType(DataType.Json)] 13 | public required string Blob { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /Backend/Api/Models/Treatment.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public class CreateTreatment 4 | { 5 | public List Events { get; set; } = []; 6 | 7 | public long? PersonId { get; set; } 8 | } 9 | 10 | public class Treatement 11 | { 12 | public List Events { get; set; } = []; 13 | 14 | public TreatmentType Type { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /flutter_frontend/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Sat May 04 21:06:52 CEST 2024 8 | sdk.dir=/home/francois/Android/Sdk 9 | -------------------------------------------------------------------------------- /Backend/Api/Logic/Import/Redmi/RedmiRecord.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Logic.Import.Redmi; 2 | 3 | internal class RedmiRecord 4 | { 5 | public string? Uid { get; set; } 6 | public string? Sid { get; set; } 7 | public string? Key { get; set; } 8 | public int Time { get; set; } 9 | public string? Value { get; set; } 10 | 11 | public string GetKey() 12 | { 13 | return $"{Key}_{Time}"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /flutter_frontend/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /flutter_frontend/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter_frontend/lib/helpers/metric_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/services/swagger/generated_code/swagger.swagger.dart'; 2 | 3 | class MetricHelper { 4 | 5 | static String getMetricText(Metric metric) { 6 | String tag = ''; 7 | 8 | if (metric.tag != null) tag += ': ${metric.tag}'; 9 | if (metric.source != null && metric.source != FileTypes.none) { 10 | tag += '(${metric.source?.name})'; 11 | } 12 | return tag; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/bin 15 | **/charts 16 | **/docker-compose* 17 | **/compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/settings/health_settings.dart: -------------------------------------------------------------------------------- 1 | class HealthSettings { 2 | static const _syncHealth = "syncHealth"; 3 | bool syncHealth; 4 | 5 | HealthSettings(this.syncHealth); 6 | 7 | // stupid boilerplate code because dart can't decode json 8 | HealthSettings.fromJson(dynamic json) 9 | : syncHealth = json[_syncHealth] as bool? ?? false; 10 | 11 | Map toJson() => { 12 | _syncHealth: syncHealth, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /Backend/Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "Jwt": { 9 | "Issuer": "https://test.com/", 10 | "Audience": "https://test.com/", 11 | "Key": "shortkey'" 12 | }, 13 | "ConnectionStrings": { 14 | "Default": "Server=localhost;Port=5432;Database=helse;User Id=postgres;Password=postgres; Include Error Detail=true" 15 | } 16 | } -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/splash.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'common/loader.dart'; 4 | 5 | class SplashPage extends StatelessWidget { 6 | const SplashPage({super.key}); 7 | 8 | static Route route() { 9 | return MaterialPageRoute(builder: (_) => const SplashPage()); 10 | } 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return const Scaffold( 15 | body: Center(child: HelseLoader()), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /flutter_frontend/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication *my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /flutter_frontend/ios/Runner/GeneratedPluginRegistrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GeneratedPluginRegistrant_h 8 | #define GeneratedPluginRegistrant_h 9 | 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface GeneratedPluginRegistrant : NSObject 15 | + (void)registerWithRegistry:(NSObject*)registry; 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | #endif /* GeneratedPluginRegistrant_h */ 20 | -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/theme_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui'; 3 | 4 | class ThemeHelper { 5 | final Map colors = {}; 6 | 7 | Color stateColor(String state) { 8 | if (colors.containsKey(state)) { 9 | return colors[state]!; 10 | } else { 11 | var r = Random(); 12 | var color = Color.fromRGBO(r.nextInt(55) + 100, r.nextInt(105) + 150, r.nextInt(105) + 100, 1); 13 | colors[state] = color; 14 | return color; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: created 6 | 7 | jobs: 8 | build-android: 9 | permissions: 10 | contents: write 11 | uses: ./.github/workflows/build-android.yml 12 | with: 13 | push: true 14 | sign: true 15 | upload: true 16 | secrets: inherit 17 | 18 | build-docker: 19 | permissions: 20 | contents: read 21 | packages: write 22 | uses: ./.github/workflows/build-docker.yml 23 | with: 24 | push: true 25 | 26 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Person.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "person")] 6 | public class Person 7 | { 8 | [PrimaryKey, Identity] 9 | public long Id { get; set; } 10 | 11 | [Column] 12 | public string? Name { get; set; } 13 | 14 | [Column] 15 | public string? Surname { get; set; } 16 | 17 | // National identifier 18 | [Column] 19 | public string? Identifier { get; set; } 20 | 21 | [Column] 22 | public DateTime? Birth { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /Backend/Api/Helpers/SettingsHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Api.Data; 3 | 4 | namespace Api.Helpers; 5 | 6 | public static class SettingsHelper 7 | { 8 | 9 | public static async Task SaveSettingsAsync(this ISettingsContext db, string name, T blob) where T : class 10 | { 11 | await using var transaction = await db.BeginTransactionAsync(); 12 | 13 | var data = JsonSerializer.Serialize(blob); 14 | 15 | await db.Upsert(name, data); 16 | 17 | await transaction.CommitAsync(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /flutter_frontend/lib/helpers/users.dart: -------------------------------------------------------------------------------- 1 | 2 | import '../services/swagger/generated_code/swagger.swagger.dart'; 3 | 4 | extension UsersHelper on UserType { 5 | /// Rights is just a flag and admin is true when the first bit is 1 6 | bool isAdmin() => ((value ?? 0).isOdd); 7 | 8 | bool isCare() => has(UserType.caregiver); 9 | 10 | bool isUser() => has(UserType.user); 11 | 12 | bool has(UserType enumFlag) { 13 | int toSearch = enumFlag.value ?? 0; 14 | int hay = value ?? 0; 15 | return (hay & toSearch) == toSearch; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/001-Files.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE SCHEMA file; 3 | 4 | CREATE TABLE file.Blob 5 | ( 6 | Id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, 7 | FileId BIGINT NOT NULL, 8 | Data bytea NOT NULL, 9 | Type INT NOT NULL 10 | ); 11 | 12 | CREATE TABLE file.Files 13 | ( 14 | Id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, 15 | Creation TIMESTAMP NOT NULL 16 | ); 17 | 18 | ALTER TABLE file.Blob 19 | ADD CONSTRAINT FK_Files_TO_Blob 20 | FOREIGN KEY (FileId) 21 | REFERENCES file.Files (Id); 22 | 23 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Address.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "person")] 6 | public class Address 7 | { 8 | [PrimaryKey] 9 | public long Id { get; set; } 10 | 11 | [Column, NotNull] 12 | public long Person { get; set; } 13 | 14 | [Column] 15 | public string? Street { get; set; } 16 | 17 | [Column] 18 | public string? Number { get; set; } 19 | 20 | [Column] 21 | public string? Postal { get; set; } 22 | 23 | [Column] 24 | public string? Country { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /Backend/Api/Models/FileTypes.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.ComponentModel; 3 | 4 | namespace Api.Models; 5 | 6 | /// 7 | /// Import file type supported by the app 8 | /// 9 | public enum FileTypes 10 | { 11 | /// 12 | /// Manually added data 13 | /// 14 | None = 0, 15 | 16 | [Description("Redmi watch fitness file")] 17 | RedmiWatch, 18 | 19 | [Description("Data from health connect on android")] 20 | GoogleHealthConnect, 21 | 22 | [Description("Data from the Clue application")] 23 | Clue, 24 | } 25 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Blob.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "file")] 6 | public class Blob 7 | { 8 | [PrimaryKey, Identity] 9 | public long Id { get; set; } 10 | 11 | [Column, NotNull] 12 | public long FileId { get; set; } 13 | 14 | [Association(ThisKey = nameof(FileId), OtherKey = nameof(Files.Id))] 15 | public Files? File { get; set; } 16 | 17 | [Column, NotNull] 18 | public required string Data { get; set; } 19 | 20 | [Column, NotNull] 21 | public int Type { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/004-HealthData.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO health.metrictype( description, name, unit, type, summaryType) 2 | VALUES ( null, 'Heart', 'bpm', 1, 0), 3 | (null , 'Oxygen', '%', 1, 0), 4 | (null , 'Wheight', 'Kg', 1, 0), 5 | (null , 'Height', 'm', 1, 0), 6 | (null , 'Temperature', 'C', 1, 0), 7 | (null , 'Steps', '', 1, 1), 8 | (null , 'Calories', 'kcal', 1, 1), 9 | (null , 'Distance', 'm', 1, 1) ; 10 | 11 | INSERT INTO health.EventType(description, name, standalone) 12 | VALUES (null, 'Sleep', true), 13 | (null, 'Care', true) ; -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/settings/theme_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ThemeSettings { 5 | static const _theme = "theme"; 6 | ThemeMode theme; 7 | 8 | ThemeSettings(this.theme); 9 | 10 | // stupid boilerplate code because dart can't decode json 11 | ThemeSettings.fromJson(dynamic json) 12 | : theme = ThemeMode.values.firstWhereOrNull((e) => e.name == json[_theme]) ?? ThemeMode.system; 13 | 14 | Map toJson() => { 15 | _theme: theme.name, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /flutter_frontend/ios/Flutter/Generated.xcconfig: -------------------------------------------------------------------------------- 1 | // This is a generated file; do not edit or check into version control. 2 | FLUTTER_ROOT=/home/francois/App/flutter 3 | FLUTTER_APPLICATION_PATH=/home/francois/Progra/Helse/App 4 | COCOAPODS_PARALLEL_CODE_SIGN=true 5 | FLUTTER_TARGET=lib/main.dart 6 | FLUTTER_BUILD_DIR=build 7 | FLUTTER_BUILD_NAME=0.11.0 8 | FLUTTER_BUILD_NUMBER=11 9 | EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 10 | EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 11 | DART_OBFUSCATION=false 12 | TRACK_WIDGET_CREATION=true 13 | TREE_SHAKE_ICONS=false 14 | PACKAGE_CONFIG=.dart_tool/package_config.json 15 | -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/settings/events_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/logic/settings/ordered_item.dart'; 2 | 3 | class EventsSettings { 4 | static const _events = "events"; 5 | List events; 6 | 7 | EventsSettings(this.events); 8 | 9 | // stupid boilerplate code because dart can't decode json 10 | EventsSettings.fromJson(dynamic json) 11 | : events = (json[_events] as List? ?? []) 12 | .map((e) => OrderedItem.fromJson(e)) 13 | .toList(); 14 | 15 | Map toJson() => { 16 | _events: events, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-android: 13 | permissions: 14 | contents: read 15 | uses: ./.github/workflows/build-android.yml 16 | with: 17 | push: true 18 | sign: true 19 | upload: false 20 | 21 | secrets: inherit 22 | 23 | build-docker: 24 | permissions: 25 | contents: read 26 | packages: write 27 | uses: ./.github/workflows/build-docker.yml 28 | with: 29 | push: true 30 | -------------------------------------------------------------------------------- /Backend/Api/Helpers/Auth/TokenHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Api.Helpers.Auth; 4 | 5 | public static class TokenHelper 6 | { 7 | public static string? GetUser(this ClaimsPrincipal claims, string tokenType = "access") 8 | { 9 | if(!claims.Claims.Any(x => x.Type == "token" && x.Value == tokenType)) 10 | { 11 | // Wrong token type 12 | return null; 13 | } 14 | var claim = claims.Claims.FirstOrDefault(x => x.Type.EndsWith("nameidentifier", StringComparison.OrdinalIgnoreCase)); 15 | 16 | return claim?.Value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/settings/metrics_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/logic/settings/ordered_item.dart'; 2 | 3 | class MetricsSettings { 4 | static const _metrics = "metrics"; 5 | List metrics; 6 | 7 | MetricsSettings(this.metrics); 8 | 9 | // stupid boilerplate code because dart can't decode json 10 | MetricsSettings.fromJson(dynamic json) 11 | : metrics = (json[_metrics] as List? ?? []) 12 | .map((e) => OrderedItem.fromJson(e)) 13 | .toList(); 14 | 15 | Map toJson() => { 16 | _metrics: metrics, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /flutter_frontend/README.md: -------------------------------------------------------------------------------- 1 | # helse 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /flutter_frontend/ios/Flutter/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/home/francois/App/flutter" 4 | export "FLUTTER_APPLICATION_PATH=/home/francois/Progra/Helse/App" 5 | export "COCOAPODS_PARALLEL_CODE_SIGN=true" 6 | export "FLUTTER_TARGET=lib/main.dart" 7 | export "FLUTTER_BUILD_DIR=build" 8 | export "FLUTTER_BUILD_NAME=0.11.0" 9 | export "FLUTTER_BUILD_NUMBER=11" 10 | export "DART_OBFUSCATION=false" 11 | export "TRACK_WIDGET_CREATION=true" 12 | export "TREE_SHAKE_ICONS=false" 13 | export "PACKAGE_CONFIG=.dart_tool/package_config.json" 14 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | volumes: 4 | storage: 5 | 6 | services: 7 | app: 8 | build: . 9 | ports: 10 | - 8080:8080 11 | environment: 12 | - ConnectionStrings__Default=Server=database;Port=5432;Database=helse;User Id=postgres;Password=postgres 13 | - Jwt__Issuer=test.com 14 | - Jwt__Audience=test.com 15 | - Jwt__Key=ffeyufegyvegiuegiygcyvgzyi 16 | 17 | database: 18 | image: postgres:15-alpine 19 | environment: 20 | POSTGRES_PASSWORD: postgres 21 | POSTGRES_USER: postgres 22 | POSTGRES_DB: helse 23 | PG_DATA: /var/lib/postgresql/data 24 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/EventType.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "health")] 6 | public class EventType 7 | { 8 | [PrimaryKey, Identity] 9 | public long Id { get; set; } 10 | 11 | [Column] 12 | public required string Name { get; set; } 13 | 14 | [Column] 15 | public string? Description { get; set; } 16 | 17 | // If the event is standalone or part of a treatment 18 | [Column, NotNull] 19 | public bool StandAlone { get; set; } 20 | 21 | [Column] 22 | public bool UserEditable { get; set; } 23 | 24 | [Column] 25 | public bool Visible { get; set; } 26 | } -------------------------------------------------------------------------------- /Backend/Api/Models/Right.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public class Right 4 | { 5 | public long? PersonId { get; set; } 6 | 7 | public long UserId { get; set; } 8 | 9 | public DateTime Start { get; set; } 10 | 11 | public DateTime? Stop { get; set; } 12 | 13 | public RightType Type { get; set; } 14 | } 15 | 16 | public static class RightExtensions 17 | { 18 | public static Right FromDb(this Api.Data.Models.Right x) => new() 19 | { 20 | Stop = x.Stop, 21 | PersonId = x.PersonId, 22 | Start = x.Start, 23 | Type = (Models.RightType)x.Type, 24 | UserId = x.UserId 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /flutter_frontend/lib/helpers/translation.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/ui/common/date_range_picker.dart'; 2 | 3 | class Translation { 4 | static String get(DatePreset value) { 5 | switch (value) { 6 | case DatePreset.today: 7 | return 'Today'; 8 | case DatePreset.week: 9 | return '7 days'; 10 | case DatePreset.month: 11 | return '30 days'; 12 | case DatePreset.trimestre: 13 | return '3 Months'; 14 | case DatePreset.halfYear: 15 | return '6 Months'; 16 | case DatePreset.year: 17 | return '1 Year'; 18 | case DatePreset.yearToDate: 19 | return 'Year to date'; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/care_dashboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'blocs/care/agenda.dart'; 4 | import 'blocs/care/patients.dart'; 5 | 6 | class CareDashBoard extends StatelessWidget { 7 | final DateTimeRange date; 8 | const CareDashBoard({super.key, required this.date}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return SingleChildScrollView( 13 | child: Center( 14 | child: Column( 15 | mainAxisSize: MainAxisSize.min, 16 | children: [ 17 | const Patients(), 18 | Agenda(date: date), 19 | ], 20 | ), 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /flutter_frontend/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helse", 3 | "short_name": "helse", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-310.png", 19 | "sizes": "310x310", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /Backend/Api/Models/Metric.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public class Metric : MetricBase 4 | { 5 | public long Id { get; set; } 6 | 7 | public long Person { get; set; } 8 | 9 | public long User { get; set; } 10 | } 11 | 12 | public class UpdateMetric : MetricBase 13 | { 14 | public long Id { get; set; } 15 | } 16 | 17 | public class CreateMetric : MetricBase 18 | { 19 | } 20 | 21 | public abstract class MetricBase 22 | { 23 | public DateTime Date { get; set; } 24 | 25 | public required string Value { get; set; } 26 | 27 | public string? Tag { get; set; } 28 | 29 | public long Type { get; set; } 30 | 31 | public FileTypes Source { get; set; } 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/square_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SquareDialog extends StatelessWidget { 4 | const SquareDialog({ 5 | super.key, 6 | required this.content, 7 | this.actions, 8 | this.title, 9 | }); 10 | 11 | final Widget? content; 12 | final Text? title; 13 | final List? actions; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return AlertDialog( 18 | shape: const RoundedRectangleBorder( 19 | borderRadius: BorderRadius.all(Radius.circular(0.0))), 20 | scrollable: true, 21 | content: content, 22 | title: title, 23 | actions: actions, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/MetricType.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "health")] 6 | public class MetricType 7 | { 8 | [PrimaryKey, Identity] 9 | public long Id { get; set; } 10 | 11 | [Column] 12 | public required string Name { get; set; } 13 | 14 | [Column] 15 | public string? Description { get; set; } 16 | 17 | [Column] 18 | public string? Unit { get; set; } 19 | 20 | [Column] 21 | public long Type { get; set; } 22 | 23 | [Column] 24 | public long SummaryType { get; set; } 25 | 26 | [Column] 27 | public bool UserEditable { get; set; } 28 | 29 | [Column] 30 | public bool Visible { get; set; } 31 | } -------------------------------------------------------------------------------- /Backend/Api/Helpers/Helper.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Api.Helpers; 4 | 5 | public static class Helper 6 | { 7 | public static string? DescriptionAttr(this T source) 8 | { 9 | if (source is null) 10 | return null; 11 | 12 | var sourceText = source.ToString(); 13 | 14 | if (sourceText is null) 15 | return sourceText; 16 | 17 | var fi = source.GetType().GetField(sourceText); 18 | 19 | var attributes = (DescriptionAttribute[]?)fi?.GetCustomAttributes(typeof(DescriptionAttribute), false); 20 | 21 | if (attributes != null && attributes.Length > 0) return attributes[0].Description; 22 | else return sourceText; 23 | } 24 | } -------------------------------------------------------------------------------- /Backend/Api/Models/AdminSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public class Proxy 4 | { 5 | public const string Name = "proxy"; 6 | 7 | public bool ProxyAuth { get; set; } 8 | 9 | public bool AutoRegister { get; set; } 10 | 11 | public string? Header { get; set; } 12 | } 13 | 14 | public class Oauth 15 | { 16 | public const string Name = "Oauth"; 17 | 18 | public bool Enabled { get; set; } 19 | 20 | public bool AutoRegister { get; set; } 21 | 22 | public bool AutoLogin { get; set; } 23 | 24 | public string? ClientId { get; set; } 25 | 26 | public string? ClientSecret { get; set; } 27 | 28 | public string? Url { get; set; } 29 | 30 | public string? Tokenurl { get; set; } 31 | } 32 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Treatment.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "health")] 6 | public class Treatment 7 | { 8 | [PrimaryKey] 9 | public long Id { get; set; } 10 | 11 | [Column] 12 | public long? PrescriptionId { get; set; } 13 | 14 | [Association(ThisKey = nameof(PrescriptionId), OtherKey = nameof(Data.Models.Prescription.Id))] 15 | public Prescription? Prescription { get; set; } 16 | 17 | [Column, NotNull] 18 | public int Type { get; set; } 19 | 20 | [Column, NotNull] 21 | public long PersonId { get; set; } 22 | 23 | [Association(ThisKey = nameof(PersonId), OtherKey = nameof(Data.Models.Person.Id))] 24 | public Person? Person { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /Backend/Api/Logic/Import/ListImporter.cs: -------------------------------------------------------------------------------- 1 | using Api.Data; 2 | using Api.Data.Models; 3 | 4 | 5 | namespace Api.Logic.Import; 6 | 7 | public class ListImporter(ImportData file, IHealthContext db, User user) : Importer(db, user) 8 | { 9 | public ImportData Data { get; } = file; 10 | 11 | public override async Task Import() 12 | { 13 | if (Data.Metrics is not null) 14 | { 15 | foreach (var metric in Data.Metrics) 16 | { 17 | await ImportMetric(metric); 18 | } 19 | } 20 | 21 | if (Data.Events is not null) 22 | { 23 | foreach (var value in Data.Events) 24 | { 25 | await ImportEvent(value); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Backend/Api/Models/Person.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public class Person : PersonBase 4 | { 5 | 6 | public long Id { get; set; } 7 | } 8 | 9 | public class PersonCreation : PersonBase { } 10 | 11 | public abstract class PersonBase 12 | { 13 | 14 | public string? Name { get; set; } 15 | 16 | public string? Surname { get; set; } 17 | 18 | public string? Identifier { get; set; } 19 | 20 | public DateTime? Birth { get; set; } 21 | 22 | public string? UserName { get; set; } 23 | 24 | public string? Password { get; set; } 25 | 26 | public UserType Type { get; set; } 27 | 28 | public string? Email { get; set; } 29 | 30 | public string? Phone { get; set; } 31 | 32 | public List Rights { get; set; } = new List(); 33 | } -------------------------------------------------------------------------------- /docker-compose.debug.yml: -------------------------------------------------------------------------------- 1 | # Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP.NET Core service. 2 | 3 | version: '3.4' 4 | 5 | services: 6 | api: 7 | image: api 8 | build: 9 | context: . 10 | dockerfile: Backend/Api/Dockerfile 11 | args: 12 | - configuration=Debug 13 | ports: 14 | - 5059:5059 15 | environment: 16 | - ASPNETCORE_ENVIRONMENT=Development 17 | - ConnectionStrings__Default=Server=192.168.178.21;Port=5432;Database=helse;User Id=postgres;Password=postgres 18 | - Jwt__Issuer=test.com 19 | - Jwt__Audience=test.com 20 | - Jwt__Key=ffeyufegyvegiuegiygcyvgzyi 21 | volumes: 22 | - ~/.vsdbg:/remote_debugger:rw 23 | -------------------------------------------------------------------------------- /flutter_frontend/lib/services/login_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/services/api_service.dart'; 2 | import 'package:helse/services/swagger/generated_code/swagger.swagger.dart'; 3 | 4 | class LoginService extends ApiService { 5 | LoginService(super.account); 6 | Future login(String username, String password, String? redirect) async { 7 | var api = await getService(); 8 | var response = await api.apiAuthPost(body: Connection(user: username, password: password, redirect: redirect)); 9 | 10 | switch (response.statusCode) { 11 | case 401: 12 | throw Exception("Incorrect username or password"); 13 | case 200: 14 | return response.body; 15 | default: 16 | throw Exception(response.error ?? "Error"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Prescription.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "health")] 6 | public class Prescription 7 | { 8 | [PrimaryKey] 9 | public long Id { get; set; } 10 | 11 | [Column, NotNull] 12 | public long UserId { get; set; } 13 | 14 | [Association(ThisKey = nameof(UserId), OtherKey = nameof(Data.Models.User.Id))] 15 | public User? User { get; set; } 16 | 17 | [Column, NotNull] 18 | public long FileId { get; set; } 19 | 20 | [Association(ThisKey = nameof(FileId), OtherKey = nameof(Data.Models.Files.Id))] 21 | public Files? File { get; set; } 22 | 23 | [Column, NotNull] 24 | public DateTime Start { get; set; } 25 | 26 | [Column, NotNull] 27 | public DateTime End { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Backend/Api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base 2 | WORKDIR /app 3 | EXPOSE 5059 4 | 5 | ENV ASPNETCORE_URLS=http://+:5059 6 | 7 | USER app 8 | FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build 9 | ARG configuration=Release 10 | WORKDIR /src 11 | COPY ["Backend/Api/Api.csproj", "Backend/Api/"] 12 | RUN dotnet restore "Backend/Api/Api.csproj" 13 | COPY . . 14 | WORKDIR "/src/Backend/Api" 15 | RUN dotnet build "Api.csproj" -c $configuration -o /app/build 16 | 17 | FROM build AS publish 18 | ARG configuration=Release 19 | RUN dotnet publish "Api.csproj" -c $configuration -o /app/publish /p:UseAppHost=false 20 | 21 | FROM base AS final 22 | WORKDIR /app 23 | COPY --from=publish /app/publish . 24 | ENTRYPOINT ["dotnet", "Api.dll"] 25 | -------------------------------------------------------------------------------- /Backend/Api/Logic/Import/Redmi/Record.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Logic.Import.Redmi; 2 | 3 | internal class Record 4 | { 5 | public int? Date_time { get; set; } 6 | public int? Time { get; set; } 7 | public int Timezone { get; set; } 8 | } 9 | 10 | 11 | internal class SpoRecord : Record 12 | { 13 | public string? Spo2 { get; set; } 14 | } 15 | 16 | internal class HeartRecord : Record 17 | { 18 | public string? Bpm { get; set; } 19 | } 20 | 21 | internal class StepRecord : Record 22 | { 23 | public string? Steps { get; set; } 24 | public string? Distance { get; set; } 25 | } 26 | 27 | internal class CalorieRecord : Record 28 | { 29 | public string? Calories { get; set; } 30 | } 31 | 32 | internal class WeightRecord : Record 33 | { 34 | public string? Weight { get; set; } 35 | } 36 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/notification.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:toastification/toastification.dart'; 3 | 4 | class Notify { 5 | static void showError(String content) { 6 | toastification.show( 7 | title: Text(content), 8 | type: ToastificationType.error, 9 | style: ToastificationStyle.minimal, 10 | alignment: Alignment.centerRight, 11 | autoCloseDuration: const Duration(seconds: 5) 12 | ); 13 | } 14 | 15 | static void show(String content) { 16 | toastification.show( 17 | title: Text(content), 18 | type: ToastificationType.success, 19 | style: ToastificationStyle.minimal, 20 | alignment: Alignment.centerRight, 21 | autoCloseDuration: const Duration(seconds: 5) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/statefull_check.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class StatefullCheck extends StatefulWidget { 4 | const StatefullCheck(this.value, this.callback, {super.key}); 5 | 6 | final bool value; 7 | final void Function(bool) callback; 8 | 9 | @override 10 | State createState() => _StatefullCheckState(); 11 | } 12 | 13 | 14 | class _StatefullCheckState extends State { 15 | late bool check = widget.value; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Checkbox( 20 | value: check, 21 | onChanged: (value) { 22 | setState(() { 23 | check = value ?? false; 24 | }); 25 | 26 | widget.callback(value ?? false); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/account/authentication_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | import '../d_i.dart'; 5 | import 'authentication_logic.dart'; 6 | 7 | class AuthenticationBloc extends Cubit { 8 | AuthenticationBloc() : super(AuthenticationStatus.unknown) { 9 | _authenticationStatusSubscription = DI.authentication.status.listen((status) => _onAuthenticationStatusChanged(status)); 10 | } 11 | 12 | late StreamSubscription _authenticationStatusSubscription; 13 | 14 | @override 15 | Future close() { 16 | _authenticationStatusSubscription.cancel(); 17 | return super.close(); 18 | } 19 | 20 | Future _onAuthenticationStatusChanged(AuthenticationStatus status) async { 21 | emit(status); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Backend/Api/Models/Event.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public abstract class BaseEvent 4 | { 5 | public int Type { get; set; } 6 | public string? Description { get; set; } 7 | public DateTime Start { get; set; } 8 | public DateTime Stop { get; set; } 9 | 10 | public string? Tag { get; set; } 11 | } 12 | 13 | public class Event : BaseEvent 14 | { 15 | public long User { get; set; } 16 | public long? File { get; set; } 17 | public long? Treatment { get; set; } 18 | 19 | public long Id { get; set; } 20 | public long Person { get; set; } 21 | 22 | public bool Valid { get; set; } 23 | public long? Address { get; set; } 24 | } 25 | 26 | public class CreateEvent : BaseEvent 27 | { 28 | } 29 | 30 | public class UpdateEvent : BaseEvent 31 | { 32 | public long Id { get; set; } 33 | } -------------------------------------------------------------------------------- /flutter_frontend/lib/services/treatment_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/services/api_service.dart'; 2 | 3 | import 'swagger/generated_code/swagger.swagger.dart'; 4 | 5 | class TreatmentService extends ApiService { 6 | TreatmentService(super.account); 7 | 8 | Future?> treatments(DateTime? start, DateTime? end, {int? person}) async { 9 | var api = await getService(); 10 | return await call(() => api.apiTreatmentGet(start: start, end: end, personId: person)); 11 | } 12 | 13 | Future addTreatment(CreateTreatment treatment) async { 14 | var api = await getService(); 15 | await call(() => api.apiTreatmentPost(body: treatment)); 16 | } 17 | 18 | Future?> treatmentTypes() async { 19 | var api = await getService(); 20 | return await call(api.apiTreatmentTypeGet); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/events/delete_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DeleteEvent extends StatelessWidget { 4 | final Function callback; 5 | const DeleteEvent(this.callback, {super.key, int? person}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return AlertDialog( 10 | icon: const Icon(Icons.delete_sharp), 11 | title: const Text('Delete the event ?'), 12 | actions: [ 13 | TextButton( 14 | onPressed: () => Navigator.pop(context, 'Cancel'), 15 | child: const Text('Cancel'), 16 | ), 17 | TextButton( 18 | onPressed: () async { 19 | await callback(); 20 | Navigator.pop(context, 'OK'); 21 | }, 22 | child: const Text('OK'), 23 | ), 24 | ], 25 | ); 26 | } 27 | } -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Right.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "person")] 6 | public class Right 7 | { 8 | [PrimaryKey, Identity] 9 | public long Id { get; set; } 10 | 11 | [Column] 12 | public long? PersonId { get; set; } 13 | 14 | [Association(ThisKey = nameof(PersonId), OtherKey = nameof(Data.Models.Person.Id))] 15 | public Person? Person { get; set; } 16 | 17 | [Column, NotNull] 18 | public long UserId { get; set; } 19 | 20 | [Association(ThisKey = nameof(UserId), OtherKey = nameof(Data.Models.User.Id))] 21 | public User? User { get; set; } 22 | 23 | [Column, NotNull] 24 | public DateTime Start { get; set; } 25 | 26 | [Column] 27 | public DateTime? Stop { get; set; } 28 | 29 | [Column, NotNull] 30 | public int Type { get; set; } 31 | } 32 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/patient_dashboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'blocs/events/events_grid.dart'; 4 | import 'blocs/metrics/metrics_grid.dart'; 5 | 6 | class PatientDashboard extends StatelessWidget { 7 | final DateTimeRange date; 8 | final int? person; 9 | const PatientDashboard({super.key, required this.date, this.person}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return SingleChildScrollView( 14 | child: Padding( 15 | padding: const EdgeInsets.all(8.0), 16 | child: Column( 17 | children: [ 18 | MetricsGrid(date: date, person: person), 19 | const SizedBox( 20 | height: 10, 21 | ), 22 | EventsGrid(date: date, person: person), 23 | ], 24 | ), 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Backend/Api/Data/MigrationHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using DbUp; 3 | 4 | namespace Api.Data; 5 | 6 | public static class MigrationHelper 7 | { 8 | public static void Init(string connection, bool inMemory, ILogger logger) 9 | { 10 | if (inMemory) 11 | { 12 | return; 13 | } 14 | 15 | EnsureDatabase.For.PostgresqlDatabase(connection); 16 | 17 | var result = DeployChanges.To.PostgresqlDatabase(connection) 18 | .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly()) 19 | .LogToAutodetectedLog() 20 | .Build() 21 | .PerformUpgrade(); 22 | 23 | if (result.Successful) 24 | { 25 | logger.LogInformation("Migration Db"); 26 | } 27 | else 28 | { 29 | throw new Exception("Migration error"); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Backend/Api/Logic/Import/Redmi/SleepRecord.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Logic.Import.Redmi; 2 | 3 | public class SleepRecord 4 | { 5 | public int Avg_hr { get; set; } 6 | public int Bpmedtime { get; set; } 7 | public int Sleep_deep_duration { get; set; } 8 | public int Device_bedtime { get; set; } 9 | public int Device_wake_up_time { get; set; } 10 | public int Sleep_light_duration { get; set; } 11 | public int Max_hr { get; set; } 12 | public int Min_hr { get; set; } 13 | public int ProtoTime { get; set; } 14 | public int Sleep_rem_duration { get; set; } 15 | public int Duration { get; set; } 16 | public List? Items { get; set; } 17 | public int Timezone { get; set; } 18 | public int Version { get; set; } 19 | public int Awake_count { get; set; } 20 | public int Sleep_awake_duration { get; set; } 21 | public int Wake_up_time { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /flutter_frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/file_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:file_selector/file_selector.dart'; 3 | 4 | class FileInput extends StatelessWidget { 5 | final void Function(XFile value) callback; 6 | final String label; 7 | final IconData icone; 8 | 9 | const FileInput(this.callback, this.label, this.icone, {super.key}); 10 | 11 | Future _pickFile() async { 12 | final XFile? file = await openFile(); 13 | if (file != null) { 14 | callback(file); 15 | } 16 | } 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return ElevatedButton.icon( 21 | onPressed: _pickFile, 22 | icon: Icon(icone), 23 | label: Text(label), 24 | style: ElevatedButton.styleFrom( 25 | minimumSize: const Size.fromHeight(50), 26 | shape: const ContinuousRectangleBorder(), 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /flutter_frontend/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version '8.4.0' apply false 23 | id "org.jetbrains.kotlin.android" version "1.9.24" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /flutter_frontend/lib/services/setting_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/services/api_service.dart'; 2 | 3 | import 'swagger/generated_code/swagger.swagger.dart'; 4 | 5 | class SettingService extends ApiService { 6 | SettingService(super.account); 7 | 8 | Future oauth() async { 9 | var api = await getService(); 10 | return await call(api.apiAdminSettingsOauthGet) ?? const Oauth(); 11 | } 12 | 13 | Future updateOauth(Oauth settings) async { 14 | var api = await getService(); 15 | await call(() => api.apiAdminSettingsOauthPost(body: settings)); 16 | } 17 | 18 | Future proxy() async { 19 | var api = await getService(); 20 | return await call(api.apiAdminSettingsProxyGet) ?? const Proxy(); 21 | } 22 | 23 | Future updateProxy(Proxy settings) async { 24 | var api = await getService(); 25 | await call(() => api.apiAdminSettingsProxyPost(body: settings)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /flutter_frontend/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | file_selector_linux 7 | gtk 8 | url_launcher_linux 9 | ) 10 | 11 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 12 | ) 13 | 14 | set(PLUGIN_BUNDLED_LIBRARIES) 15 | 16 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 17 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 18 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 21 | endforeach(plugin) 22 | 23 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 26 | endforeach(ffi_plugin) 27 | -------------------------------------------------------------------------------- /Backend/Tests/Integrations/IntegrationTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Testing; 2 | using Microsoft.AspNetCore.TestHost; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using NSubstitute; 6 | using Api.Data; 7 | 8 | namespace Tests.Integrations; 9 | 10 | public abstract class IntegrationTest : IClassFixture> 11 | { 12 | protected readonly HttpClient _client; 13 | 14 | public IntegrationTest(WebApplicationFactory factory) 15 | { 16 | _client = factory 17 | .WithWebHostBuilder(builder => 18 | builder 19 | .ConfigureAppConfiguration((_, config) => config.AddInMemoryCollection([new("InTest", "True"), new ("ConnectionStrings:Default", ":memory:")])) 20 | .ConfigureTestServices(services => {})) 21 | .CreateClient(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/User.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "person")] 6 | public class User 7 | { 8 | public static User Empty => new() 9 | { 10 | Identifier = string.Empty, 11 | Password = string.Empty, 12 | }; 13 | 14 | [PrimaryKey, Identity] 15 | public long Id { get; set; } 16 | 17 | [Column, NotNull] 18 | public long PersonId { get; set; } 19 | 20 | [Association(ThisKey = nameof(PersonId), OtherKey = nameof(Models.Person.Id))] 21 | public Person? Person { get; set; } 22 | 23 | [Column, NotNull] 24 | public required string Identifier { get; set; } 25 | 26 | [Column, NotNull] 27 | public required string Password { get; set; } 28 | 29 | [Column, NotNull] 30 | public int Type { get; set; } 31 | 32 | [Column] 33 | public string? Email { get; set; } 34 | 35 | [Column] 36 | public string? Phone { get; set; } 37 | } -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/admin_dashboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'dashboard.dart'; 4 | 5 | class AdminDashBoard extends StatelessWidget { 6 | final DateTimeRange date; 7 | const AdminDashBoard({super.key, required this.date}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return DefaultTabController( 12 | length: 2, 13 | child: Column( 14 | children: [ 15 | const TabBar( 16 | tabs: [ 17 | Tab(icon: Icon(Icons.monitor_heart_sharp)), 18 | Tab(icon: Icon(Icons.settings)), 19 | ], 20 | ), 21 | Expanded( 22 | child: TabBarView( 23 | children: [ 24 | Dashboard(date: date), 25 | const SingleChildScrollView( 26 | child: Center(child: Text("Stats")), 27 | ), 28 | ], 29 | ), 30 | ), 31 | ], 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/settings/ordered_item.dart: -------------------------------------------------------------------------------- 1 | enum GraphKind { 2 | event, 3 | line, 4 | bar, 5 | } 6 | 7 | class OrderedItem { 8 | bool visible = true; 9 | int order = 0; 10 | String name; 11 | int id; 12 | GraphKind graph; 13 | GraphKind detailGraph; 14 | 15 | OrderedItem(this.id, this.name, this.graph, this.detailGraph); 16 | 17 | OrderedItem.fromJson(dynamic json) 18 | : visible = json["visible"] as bool, 19 | id = json['id'] as int, 20 | order = json['order'] as int, 21 | name = json['name'] as String, 22 | graph = GraphKind.values.byName((json['graph'] as String?) ?? GraphKind.bar.name), 23 | detailGraph = GraphKind.values.byName((json['detailGraph'] as String?) ?? GraphKind.line.name); 24 | 25 | Map toJson() => { 26 | 'visible': visible, 27 | 'order': order, 28 | 'name': name, 29 | 'id': id, 30 | 'graph': graph.name, 31 | 'detailGraph': detailGraph.name, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /flutter_frontend/lib/services/helper_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/services/api_service.dart'; 2 | 3 | import 'swagger/generated_code/swagger.swagger.dart'; 4 | 5 | class HelperService extends ApiService { 6 | HelperService(super.account); 7 | 8 | Future?> fileTypes() async { 9 | var api = await getService(); 10 | return await call(api.apiImportTypesGet); 11 | } 12 | 13 | Future import(String? file, int type) async { 14 | var api = await getService(); 15 | 16 | await call(() => api.apiImportTypePost(body: ImportFile(content: file), type: type)); 17 | } 18 | 19 | Future importData(ImportData file) async { 20 | var api = await getService(); 21 | await call(() => api.apiImportPost(body: file)); 22 | } 23 | 24 | Future isInit(String url) async { 25 | var api = await getService(override: url); 26 | var response = await api.apiStatusGet(); 27 | 28 | if (!response.isSuccessful) return null; 29 | 30 | return response.bodyOrThrow; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Backend/Api/Models/MetricType.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Models; 2 | 3 | public class MetricType 4 | { 5 | public required string Name { get; set; } 6 | public string? Unit { get; set; } 7 | public MetricSummary SummaryType { get; set; } 8 | public string? Description { get; set; } 9 | 10 | public MetricDataType Type { get; set; } 11 | public long Id { get; set; } 12 | 13 | public bool UserEditable { get; set; } 14 | 15 | public bool Visible { get; set; } 16 | } 17 | 18 | public enum MetricTypes 19 | { 20 | Heart = 1, 21 | Oxygen = 2, 22 | Wheight = 3, 23 | Height = 4, 24 | Temperature = 5, 25 | Steps = 6, 26 | Calories = 7, 27 | Distance = 8, 28 | Menstruation = 9, 29 | Pain = 10, 30 | Mood = 11, 31 | Medication = 12, 32 | Tests = 13, 33 | Sex = 14, 34 | Stool = 15, 35 | Spotting = 16, 36 | } 37 | 38 | public enum MetricDataType 39 | { 40 | Text, 41 | Number, 42 | } 43 | 44 | public enum MetricSummary 45 | { 46 | Latest, 47 | Sum, 48 | Mean, 49 | } 50 | -------------------------------------------------------------------------------- /flutter_frontend/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | void fl_register_plugins(FlPluginRegistry* registry) { 14 | g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = 15 | fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); 16 | file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 17 | g_autoptr(FlPluginRegistrar) gtk_registrar = 18 | fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); 19 | gtk_plugin_register_with_registrar(gtk_registrar); 20 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 21 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 22 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 23 | } 24 | -------------------------------------------------------------------------------- /flutter_frontend/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "2feea7a4071e25c1e3aac9c17016531bc4442f2a" 8 | channel: "beta" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2feea7a4071e25c1e3aac9c17016531bc4442f2a 17 | base_revision: 2feea7a4071e25c1e3aac9c17016531bc4442f2a 18 | - platform: web 19 | create_revision: 2feea7a4071e25c1e3aac9c17016531bc4442f2a 20 | base_revision: 2feea7a4071e25c1e3aac9c17016531bc4442f2a 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/custom_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomSwitch extends StatefulWidget { 4 | final bool value; 5 | final void Function(bool? value) onChanged; 6 | 7 | const CustomSwitch({ 8 | super.key, 9 | required this.value, 10 | required this.onChanged, 11 | }); 12 | 13 | @override 14 | State createState() => _CustomSwitchState(); 15 | } 16 | 17 | class _CustomSwitchState extends State { 18 | bool value = false; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | value = widget.value; 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Padding( 29 | padding: const EdgeInsets.only(left: 8.0), 30 | child: Transform.scale( 31 | scale: 0.8, 32 | child: Switch( 33 | value: value, 34 | onChanged: (bool? v) { 35 | setState(() { 36 | value = v ?? false; 37 | }); 38 | widget.onChanged.call(v); 39 | }, 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Francois Schiltz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Metric.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "health")] 6 | public class Metric 7 | { 8 | [PrimaryKey, Identity] 9 | public long Id { get; set; } 10 | 11 | [Column, NotNull] 12 | public long PersonId { get; set; } 13 | 14 | [Association(ThisKey = nameof(PersonId), OtherKey = nameof(Data.Models.Person.Id))] 15 | public Person? Person { get; set; } 16 | 17 | [Column, NotNull] 18 | public long UserId { get; set; } 19 | 20 | [Association(ThisKey = nameof(UserId), OtherKey = nameof(Data.Models.User.Id))] 21 | public User? User { get; set; } 22 | 23 | [Column, NotNull] 24 | public DateTime Date { get; set; } 25 | 26 | [Column, NotNull] 27 | public required string Value { get; set; } 28 | 29 | [Column] 30 | public string? Tag { get; set; } 31 | 32 | [Column] 33 | public long Type { get; set; } 34 | 35 | [Association(ThisKey = nameof(Type), OtherKey = nameof(Data.Models.MetricType.Id))] 36 | public MetricType? MetricType { get; set; } 37 | 38 | [Column] 39 | public int Source { get; set; } 40 | } 41 | -------------------------------------------------------------------------------- /flutter_frontend/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:helse/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const App()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add_sharp)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/type_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/ui/common/square_outline_input_border.dart'; 3 | 4 | class DropDownItem { 5 | final T value; 6 | final String description; 7 | 8 | const DropDownItem(this.value, this.description); 9 | } 10 | 11 | class TypeInput extends StatelessWidget { 12 | final List> types; 13 | final void Function(T?) callback; 14 | final String? label; 15 | final T? value; 16 | 17 | const TypeInput(this.types, this.callback, {super.key, this.label, this.value}); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | var theme = Theme.of(context).colorScheme; 22 | 23 | return DropdownButtonFormField( 24 | onChanged: callback, 25 | value: value, 26 | items: types.map((type) => DropdownMenuItem(value: type.value, child: Text(type.description))).toList(), 27 | decoration: InputDecoration( 28 | labelText: label ?? 'Type', 29 | prefixIcon: const Icon(Icons.list_sharp), 30 | prefixIconColor: theme.primary, 31 | filled: true, 32 | fillColor: theme.surface, 33 | border: SquareOutlineInputBorder(theme.primary), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/011-Events.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | LOCK TABLE health.Eventtype IN EXCLUSIVE MODE; 3 | 4 | ALTER TABLE health.Eventtype ADD COLUMN Visible boolean NOT NULL DEFAULT TRUE; 5 | ALTER TABLE health.Eventtype ADD COLUMN UserEditable boolean NOT NULL DEFAULT TRUE; 6 | 7 | --Make the first row non user editable (this disable deleting but allow changing names and description) 8 | UPDATE health.Eventtype SET UserEditable = FALSE WHERE Id < 3; 9 | UPDATE health.Eventtype SET Visible = TRUE WHERE Id < 3; 10 | 11 | 12 | -- Add delete cascade to allow update the primay key of the type 13 | 14 | ALTER TABLE health.Event 15 | DROP CONSTRAINT FK_Type_TO_Event; 16 | 17 | ALTER TABLE health.Event 18 | ADD CONSTRAINT FK_Type_TO_Event 19 | FOREIGN KEY (Type) 20 | REFERENCES health.EventType (Id); 21 | 22 | 23 | UPDATE health.EventType SET Id = (Id + 100) WHERE UserEditable = TRUE; 24 | 25 | 26 | -- Set the key at around 100 to leave space for future hardcoded metric 27 | SELECT setval(pg_get_serial_sequence('health.EventType','id'), COALESCE((SELECT MAX(Id)+100 FROM health.EventType), 1), false); 28 | 29 | 30 | INSERT INTO health.EventType(id, description, name, standalone, usereditable) 31 | VALUES (3, null, 'Workout', true, false); 32 | 33 | COMMIT; 34 | 35 | -------------------------------------------------------------------------------- /Backend/Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:18388", 8 | "sslPort": 44359 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5059", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7290;http://localhost:5059", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pub" # See documentation for possible values 9 | directory: "/flutter_frontend" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | non-major-versions: # the name of the group 14 | update-types: # the key used to specify the semver level to include in the group 15 | - "minor" # an array, possible values being minor, patch and major 16 | - "patch" 17 | 18 | - package-ecosystem: "nuget" # See documentation for possible values 19 | directory: "/Backend" # Location of package manifests 20 | schedule: 21 | interval: "weekly" 22 | groups: 23 | non-major-versions: # the name of the group 24 | update-types: # the key used to specify the semver level to include in the group 25 | - "minor" # an array, possible values being minor, patch and major 26 | - "patch" 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the backend on microsoft image 2 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 3 | 4 | # Building the backend 5 | WORKDIR /source 6 | 7 | ## copy and publish app and libraries for the backend 8 | COPY ./Backend/Api . 9 | RUN dotnet publish -o /backend -c release 10 | 11 | 12 | # build the flutter web app 13 | # preparing the ubuntu container with all necessary tools 14 | RUN apt update -y 15 | RUN apt install -y bash curl file git unzip zip xz-utils libglu1-mesa 16 | 17 | ## Download Flutter SDK 18 | RUN git clone https://github.com/flutter/flutter.git -b stable /flutter 19 | ENV PATH "$PATH:/flutter/bin" 20 | RUN flutter precache 21 | 22 | ## Run flutter doctor 23 | RUN flutter config --enable-web 24 | RUN flutter doctor -v 25 | 26 | # Copy files to container and build 27 | RUN mkdir /app/ 28 | COPY ./flutter_frontend/ /app/ 29 | WORKDIR /app/ 30 | 31 | # run flutter clean, pub get and then build for web 32 | RUN flutter clean 33 | RUN flutter pub get 34 | RUN flutter build web 35 | 36 | 37 | # final stage/image 38 | FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine 39 | WORKDIR /app 40 | ## Copy the backend 41 | COPY --from=build /backend ./ 42 | ## Copy the webApp 43 | COPY --from=build /app/build/web /app/wwwroot 44 | 45 | EXPOSE 8080 46 | 47 | ENV DOTNET_EnableDiagnostics=0 48 | ENTRYPOINT ["dotnet", "Api.dll"] 49 | -------------------------------------------------------------------------------- /flutter_frontend/lib/services/user_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/services/api_service.dart'; 2 | 3 | import 'swagger/generated_code/swagger.swagger.dart'; 4 | 5 | class UserService extends ApiService { 6 | UserService(super.account); 7 | 8 | Future?> persons() async { 9 | var api = await getService(); 10 | return await call(api.apiPersonGet); 11 | } 12 | 13 | Future addPerson(PersonCreation person) async { 14 | var api = await getService(); 15 | 16 | await call(() => api.apiPersonPost(body: person)); 17 | } 18 | 19 | Future?> patients() async { 20 | var api = await getService(); 21 | return await call(api.apiPatientsGet); 22 | } 23 | 24 | Future updatePersonRole(int personId, UserType type) async { 25 | var api = await getService(); 26 | await call(() => api.apiPersonRolePost(personId: personId, role: type)); 27 | } 28 | 29 | Future> caregiver() async { 30 | var api = await getService(); 31 | return await call(api.apiPersonCaregiverGet) ?? []; 32 | } 33 | 34 | Future sharePatient({required int patient, required int caregiver, required bool edit }) async { 35 | 36 | var api = await getService(); 37 | return await call(() => api.apiPatientsShareGet(patient: patient, caregiver: caregiver, edit: edit)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yml: -------------------------------------------------------------------------------- 1 | name: Build docker 2 | on: 3 | workflow_call: 4 | inputs: 5 | tags: 6 | required: false 7 | type: string 8 | labels: 9 | required: false 10 | type: string 11 | push: 12 | required: true 13 | type: boolean 14 | 15 | 16 | env: 17 | REGISTRY: ghcr.io 18 | IMAGE_NAME: ${{ github.repository }} 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4.1.4 25 | 26 | - name: Log in to the Container registry 27 | if: ${{ inputs.push }} 28 | uses: docker/login-action@v3.1.0 29 | with: 30 | registry: ${{ env.REGISTRY }} 31 | username: ${{ github.actor }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Extract metadata (tags, labels) for Docker 35 | id: meta 36 | if: ${{ inputs.push }} 37 | uses: docker/metadata-action@v5.5.1 38 | with: 39 | images: ghcr.io/fschiltz/helse 40 | 41 | - name: Build and push Docker image 42 | uses: docker/build-push-action@v5.3.0 43 | with: 44 | push: ${{ inputs.push }} 45 | tags: ${{ inputs.tags || steps.meta.outputs.tags }} 46 | labels: ${{ inputs.labels || steps.meta.outputs.labels }} 47 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/dashboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../helpers/users.dart'; 4 | import '../services/swagger/generated_code/swagger.swagger.dart'; 5 | import 'care_dashboard.dart'; 6 | import 'patient_dashboard.dart'; 7 | 8 | class Dashboard extends StatelessWidget { 9 | final DateTimeRange date; 10 | final UserType? type; 11 | const Dashboard({super.key, required this.date, this.type}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | List tabs = []; 16 | List icons = []; 17 | 18 | if (type?.isUser() == true) { 19 | icons.add(Icons.monitor_heart_sharp); 20 | tabs.add(PatientDashboard(date: date)); 21 | } 22 | 23 | if (type?.isCare() == true) { 24 | icons.add(Icons.personal_injury_sharp); 25 | tabs.add(CareDashBoard(date: date)); 26 | } 27 | 28 | return DefaultTabController( 29 | length: tabs.length, 30 | child: (tabs.length == 1) 31 | ? tabs[0] 32 | : Column( 33 | children: [ 34 | TabBar(tabs: icons.map((t) => Tab(icon: Icon(t))).toList()), 35 | Expanded( 36 | child: TabBarView( 37 | children: tabs, 38 | ), 39 | ), 40 | ], 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /flutter_frontend/lib/services/account.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | /// Token storage abstraction 4 | class Account { 5 | final storage = SharedPreferences.getInstance(); 6 | 7 | static const url = "urlPath"; 8 | static const token = "sessionToken"; 9 | static const grant = "grant"; 10 | static const redirect = "redirect"; 11 | static const refresh = "refresh"; 12 | static const fitRun = "fitLastRun"; 13 | static const theme = 'theme'; 14 | static const health = 'health'; 15 | static const metrics = 'metrics'; 16 | static const events = 'events'; 17 | static const dateRange = 'dateRange'; 18 | 19 | Future get(String name) async { 20 | return (await storage).getString(name); 21 | } 22 | 23 | Future set(String name, String value) async { 24 | await (await storage).setString(name, value); 25 | } 26 | 27 | Future remove(String name) async { 28 | await (await storage).remove(name); 29 | } 30 | 31 | Future clean() async { 32 | var s = await storage; 33 | await s.remove(token); 34 | await s.remove(grant); 35 | await s.remove(redirect); 36 | await s.remove(refresh); 37 | await s.remove(fitRun); 38 | await s.remove(theme); 39 | await s.remove(health); 40 | await s.remove(metrics); 41 | await s.remove(events); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/010-Metrics.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | LOCK TABLE health.MetricType IN EXCLUSIVE MODE; 3 | 4 | ALTER TABLE health.MetricType ADD COLUMN Visible boolean NOT NULL DEFAULT TRUE; 5 | ALTER TABLE health.MetricType ADD COLUMN UserEditable boolean NOT NULL DEFAULT TRUE; 6 | 7 | --Make the first row non user editable (this disable deleting but allow changing names and description) 8 | UPDATE health.MetricType SET UserEditable = FALSE WHERE Id < 9; 9 | UPDATE health.MetricType SET Visible = TRUE WHERE Id < 9; 10 | 11 | 12 | -- Add delete cascade to allow update the primay key of the type 13 | ALTER TABLE health.Metric 14 | DROP CONSTRAINT FK_Type_TO_Metric; 15 | 16 | ALTER TABLE health.Metric 17 | ADD CONSTRAINT FK_Type_TO_Metric 18 | FOREIGN KEY (Type) 19 | REFERENCES health.MetricType (Id) 20 | ON UPDATE CASCADE; 21 | 22 | 23 | UPDATE health.MetricType SET Id = (Id + 100) WHERE UserEditable = TRUE; 24 | 25 | -- Set the key at around 100 to leave space for future hardcoded metric 26 | SELECT setval(pg_get_serial_sequence('health.MetricType','id'), COALESCE((SELECT MAX(Id)+100 FROM health.MetricType), 1), false); 27 | 28 | INSERT INTO health.MetricType(id, description, name, unit, type, summaryType, usereditable) 29 | VALUES (9, null, 'Menstruation', '', 0, 0, false), 30 | (10, null, 'Pain', '', 0, 0, false), 31 | (11, null, 'Mood', '', 0, 0, false); 32 | 33 | COMMIT; -------------------------------------------------------------------------------- /flutter_frontend/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | helse 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Backend/Api/Logic/Import/Clue/ClueItem.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace Api.Logic.Import.Clue; 7 | 8 | public class ClueItem 9 | { 10 | public string? Type { get; set; } 11 | public string? Id { get; set; } 12 | public string? Date { get; set; } 13 | 14 | [JsonConverter(typeof(SingleOrArrayConverter))] 15 | public List? Value { get; set; } 16 | } 17 | 18 | public class Value 19 | { 20 | public string? Option { get; set; } 21 | } 22 | 23 | class SingleOrArrayConverter : JsonConverter 24 | { 25 | public override bool CanConvert(Type objectType) 26 | { 27 | return (objectType == typeof(List)); 28 | } 29 | 30 | public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 31 | { 32 | JToken token = JToken.Load(reader); 33 | if (token.Type == JTokenType.Array) 34 | { 35 | return token.ToObject>() ?? []; 36 | } 37 | 38 | var item = token.ToObject(); 39 | 40 | if (item is not null) 41 | return new List { item }; 42 | else 43 | return new List(); 44 | } 45 | 46 | public override bool CanWrite => false; 47 | 48 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException(); 49 | } -------------------------------------------------------------------------------- /Backend/Api/Helpers/Auth/PasswordLogic.cs: -------------------------------------------------------------------------------- 1 | using Api.Data; 2 | using Api.Models; 3 | using Microsoft.AspNetCore.Identity; 4 | 5 | namespace Api.Helpers.Auth; 6 | 7 | public static class PasswordHelper 8 | { 9 | public static async Task<(bool, TokenInfo?)> ConnectPassword(Connection user, IUserContext db, ILogger log) 10 | { 11 | // auth 12 | var fromDb = await db.TokenFromDb(user.User); 13 | 14 | if (fromDb is null) 15 | return (false, null); 16 | 17 | // generate the token 18 | switch (TokenService.Verify(user.Password, fromDb.Password)) 19 | { 20 | case PasswordVerificationResult.Success: 21 | // Success, nothing to do 22 | break; 23 | case PasswordVerificationResult.SuccessRehashNeeded: 24 | // Success but the password needs an update 25 | await UpdatePasswordAsync(fromDb.Id, user.Password, db); 26 | break; 27 | case PasswordVerificationResult.Failed: 28 | default: 29 | log.LogWarning("Unauthorized access to getToken with user {user}", user.User); 30 | return (false, null); 31 | } 32 | 33 | return (true, fromDb); 34 | } 35 | 36 | public async static Task UpdatePasswordAsync(long user, string password, IUserContext db) 37 | { 38 | var hash = TokenService.Hash(password); 39 | 40 | await db.UpdatePassword(user, hash); 41 | } 42 | } -------------------------------------------------------------------------------- /Backend/Api/Helpers/UserHelper.cs: -------------------------------------------------------------------------------- 1 | using Api.Data; 2 | using Api.Helpers.Auth; 3 | using Api.Models; 4 | 5 | namespace Api.Helpers; 6 | 7 | public static class UserHelper 8 | { 9 | public static async Task CreateUserAsync(this IUserContext users, PersonCreation newUser, long userId) 10 | { 11 | // Open a transaction 12 | await using var transaction = await users.BeginTransactionAsync(); 13 | 14 | // create the person 15 | var id = await users.InsertPerson(newUser); 16 | 17 | // create the user if needed 18 | // patient are non user of the app, only external people managed by a caregiver 19 | // TODO add patient account creation 20 | if (newUser.Type != Api.Models.UserType.Patient) 21 | { 22 | if (newUser.UserName is null) 23 | throw new ArgumentException("Missing username", nameof(newUser)); 24 | 25 | if (newUser.Password is null) 26 | throw new ArgumentException("Missing password", nameof(newUser)); 27 | 28 | var password = TokenService.Hash(newUser.Password); 29 | await users.InsertUser(newUser, id, password); 30 | } 31 | else 32 | { 33 | // we had an implicit right for the current user if needed 34 | await users.AddRight(userId, id, RightType.Edit); 35 | await users.AddRight(userId, id, RightType.View); 36 | } 37 | 38 | await transaction.CommitAsync(); 39 | } 40 | } -------------------------------------------------------------------------------- /Backend/Api/Data/SettingsContext.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using LinqToDB; 3 | using LinqToDB.Data; 4 | 5 | namespace Api.Data; 6 | 7 | public interface ISettingsContext : IContext 8 | { 9 | Task GetSettings(string name) where T : new(); 10 | Task Upsert(string name, string data); 11 | } 12 | 13 | /// 14 | /// Class for calling the database for the settings 15 | /// 16 | public class SettingsContext(DataConnection db) : ISettingsContext 17 | { 18 | public Task BeginTransactionAsync() => db.BeginTransactionAsync(); 19 | 20 | public Task Delete(string name) => db.GetTable().DeleteAsync(x => x.Name == name); 21 | 22 | public async Task GetSettings(string name) where T : new() 23 | { 24 | var settings = await db.GetTable().Where(x => x.Name == name).SingleOrDefaultAsync(); 25 | if (settings?.Blob is null) 26 | { 27 | return new T(); 28 | } 29 | 30 | return JsonSerializer.Deserialize(settings.Blob) ?? new(); 31 | } 32 | 33 | public Task Upsert(string name, string data) 34 | { 35 | return db.GetTable().InsertOrUpdateAsync(() => new Data.Models.Settings 36 | { 37 | Name = name, 38 | Blob = data, 39 | }, 40 | (x) => new Data.Models.Settings 41 | { 42 | Name = name, 43 | Blob = data, 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main" ] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-android: 13 | permissions: 14 | contents: read 15 | uses: ./.github/workflows/build-android.yml 16 | with: 17 | push: false 18 | sign: false 19 | upload: false 20 | mode: debug 21 | secrets: inherit 22 | 23 | build-net: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Setup dotnet 29 | uses: actions/setup-dotnet@v4 30 | with: 31 | dotnet-version: '8.0.x' 32 | 33 | - name: Install dependencies 34 | run: dotnet restore 35 | working-directory: 'Backend' 36 | 37 | - name: Build 38 | run: dotnet build 39 | working-directory: 'Backend' 40 | 41 | - name: Test with the dotnet CLI 42 | run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover 43 | working-directory: 'Backend' 44 | 45 | - name: Codecov 46 | uses: codecov/codecov-action@v4.0.1 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | 50 | build-docker: 51 | permissions: 52 | contents: read 53 | uses: ./.github/workflows/build-docker.yml 54 | with: 55 | push: false 56 | tags: ci 57 | labels: ci 58 | -------------------------------------------------------------------------------- /Backend/Api/Logic/Import/FileImporter.cs: -------------------------------------------------------------------------------- 1 | using Api.Data; 2 | using Api.Data.Models; 3 | using Api.Models; 4 | 5 | namespace Api.Logic.Import; 6 | 7 | public abstract class FileImporter(string file, IHealthContext db, User user) : Importer(db, user) 8 | { 9 | public string File { get; } = file; 10 | } 11 | 12 | public abstract class Importer(IHealthContext db, Data.Models.User user) 13 | { 14 | public Data.Models.User User { get; } = user; 15 | 16 | public abstract Task Import(); 17 | 18 | protected async Task ImportEvent(CreateEvent e) 19 | { 20 | if (e.Tag is null) 21 | return; 22 | 23 | await using var transaction = await db.BeginTransactionAsync(); 24 | 25 | // check if the event exists 26 | var fromDb = await db.ExistsEvent(User.PersonId, e.Tag); 27 | 28 | if (!fromDb) 29 | { 30 | await db.Insert(e, User.PersonId, User.Id); 31 | } 32 | 33 | await transaction.CommitAsync(); 34 | } 35 | 36 | protected async Task ImportMetric(CreateMetric metric) 37 | { 38 | if (metric.Tag is null) 39 | return; 40 | 41 | await using var transaction = await db.BeginTransactionAsync(); 42 | 43 | // check if the metric exists 44 | var fromDb = await db.ExistsMetric(User.PersonId, metric.Tag, metric.Source); 45 | 46 | if (!fromDb) 47 | { 48 | await db.Insert(metric, User.PersonId, User.Id); 49 | } 50 | 51 | await transaction.CommitAsync(); 52 | } 53 | } -------------------------------------------------------------------------------- /Backend/API.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.001.0 5 | MinimumVisualStudioVersion = 10.0.40219.1: 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api", "Api\Api.csproj", "{FD7633AC-DA86-4B06-B78D-6FE08EEE2894}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{B27D9BF5-6A5F-4123-AB5E-090FFB1F6F59}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {FD7633AC-DA86-4B06-B78D-6FE08EEE2894}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {FD7633AC-DA86-4B06-B78D-6FE08EEE2894}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {FD7633AC-DA86-4B06-B78D-6FE08EEE2894}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {FD7633AC-DA86-4B06-B78D-6FE08EEE2894}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {B27D9BF5-6A5F-4123-AB5E-090FFB1F6F59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {B27D9BF5-6A5F-4123-AB5E-090FFB1F6F59}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {B27D9BF5-6A5F-4123-AB5E-090FFB1F6F59}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {B27D9BF5-6A5F-4123-AB5E-090FFB1F6F59}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {27E15E12-4E13-428B-A8D8-702FABEC8D4F} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/password_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'square_text_field.dart'; 4 | 5 | class PasswordInput extends StatefulWidget { 6 | final TextEditingController? controller; 7 | final String? Function(String? value)? validate; 8 | final FocusNode? nextFocus; 9 | final FocusNode? focus; 10 | final String text; 11 | 12 | const PasswordInput({ 13 | super.key, 14 | this.controller, 15 | this.nextFocus, 16 | this.validate, 17 | this.focus, 18 | this.text = "Password", 19 | }); 20 | 21 | @override 22 | State createState() => _PasswordInputState(); 23 | } 24 | 25 | class _PasswordInputState extends State { 26 | bool _obscurePassword = true; 27 | 28 | void togglePasswordVisibility() => setState(() { 29 | _obscurePassword = !_obscurePassword; 30 | }); 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | var iconButton = IconButton( 35 | onPressed: togglePasswordVisibility, 36 | icon: _obscurePassword 37 | ? const Icon(Icons.visibility_sharp) 38 | : const Icon(Icons.visibility_off_sharp)); 39 | 40 | return SquareTextField( 41 | theme: Theme.of(context).colorScheme, 42 | controller: widget.controller, 43 | obscureText: _obscurePassword, 44 | focusNode: widget.focus, 45 | label: widget.text, 46 | icon: Icons.password_sharp, 47 | suffixIcon: iconButton, 48 | type: TextInputType.visiblePassword, 49 | validator: widget.validate, 50 | onEditingComplete: () => widget.nextFocus?.requestFocus(), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/square_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/ui/common/square_outline_input_border.dart'; 3 | 4 | class SquareTextField extends StatelessWidget { 5 | const SquareTextField({ 6 | super.key, 7 | this.focusNode, 8 | required this.theme, 9 | required this.label, 10 | this.onEditingComplete, 11 | required this.icon, 12 | this.controller, 13 | this.validator, 14 | this.obscureText = false, 15 | this.onIconPressed, 16 | this.type, 17 | this.suffixIcon, 18 | }); 19 | 20 | final IconButton? suffixIcon; 21 | final void Function()? onEditingComplete; 22 | final String label; 23 | final FocusNode? focusNode; 24 | final ColorScheme theme; 25 | final IconData icon; 26 | final TextEditingController? controller; 27 | final String? Function(String? value)? validator; 28 | final bool obscureText; 29 | final void Function()? onIconPressed; 30 | final TextInputType? type; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return TextFormField( 35 | validator: validator, 36 | controller: controller, 37 | focusNode: focusNode, 38 | onEditingComplete: onEditingComplete, 39 | keyboardType: type ?? TextInputType.text, 40 | obscureText: obscureText, 41 | decoration: InputDecoration( 42 | labelText: label, 43 | prefixIcon: Icon(icon), 44 | prefixIconColor: theme.primary, 45 | suffixIcon: suffixIcon, 46 | filled: true, 47 | fillColor: theme.surface, 48 | border: SquareOutlineInputBorder(theme.primary), 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/care/patients_dashboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../helpers/date.dart'; 4 | import '../../../services/swagger/generated_code/swagger.swagger.dart'; 5 | import '../../common/date_range_picker.dart'; 6 | import '../../patient_dashboard.dart'; 7 | 8 | class PatientsDashboard extends StatefulWidget { 9 | final Person person; 10 | 11 | const PatientsDashboard(this.person, {super.key}); 12 | 13 | @override 14 | State createState() => _PatientDashboardState(); 15 | } 16 | 17 | class _PatientDashboardState extends State { 18 | DateTimeRange date = DateHelper.now(); 19 | 20 | void _setDate(DateTimeRange value) { 21 | setState(() { 22 | date = value; 23 | }); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | var isLargeScreen = MediaQuery.of(context).size.width > 600; 29 | return Scaffold( 30 | appBar: AppBar( 31 | title: Padding( 32 | padding: const EdgeInsets.all(8.0), 33 | child: Text(widget.person.name ?? "", style: Theme.of(context).textTheme.displayMedium), 34 | ), 35 | actions: [ 36 | Padding( 37 | padding: const EdgeInsets.only(right: 20.0), 38 | child: DateRangePicker(_setDate, date, isLargeScreen), 39 | ), 40 | ], 41 | ), 42 | body: Row( 43 | children: [ 44 | Expanded( 45 | child: PatientDashboard( 46 | date: date, 47 | person: widget.person.id, 48 | ), 49 | ), 50 | ], 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Backend/Api/Data/Models/Event.cs: -------------------------------------------------------------------------------- 1 | using LinqToDB.Mapping; 2 | 3 | namespace Api.Data.Models; 4 | 5 | [Table(Schema = "health")] 6 | public class Event 7 | { 8 | [PrimaryKey] 9 | public long Id { get; set; } 10 | 11 | [Column, NotNull] 12 | public long PersonId { get; set; } 13 | 14 | [Association(ThisKey = nameof(PersonId), OtherKey = nameof(Data.Models.Person.Id))] 15 | public Person? Person { get; set; } 16 | 17 | [Column, NotNull] 18 | public long UserId { get; set; } 19 | 20 | [Association(ThisKey = nameof(UserId), OtherKey = nameof(Data.Models.User.Id))] 21 | public User? User { get; set; } 22 | 23 | [Column] 24 | public long? FileId { get; set; } 25 | 26 | [Association(ThisKey = nameof(FileId), OtherKey = nameof(Data.Models.Files.Id))] 27 | public Files? File { get; set; } 28 | 29 | [Column] 30 | public long? TreatmentId { get; set; } 31 | 32 | [Association(ThisKey = nameof(TreatmentId), OtherKey = nameof(Data.Models.Treatment.Id))] 33 | public Treatment? Treatment { get; set; } 34 | 35 | [Column, NotNull] 36 | public int Type { get; set; } 37 | 38 | [Column] 39 | public string? Description { get; set; } 40 | 41 | [Column, NotNull] 42 | public DateTime Start { get; set; } 43 | 44 | [Column, NotNull] 45 | public DateTime Stop { get; set; } 46 | 47 | [Column, NotNull] 48 | public bool Valid { get; set; } 49 | 50 | [Column] 51 | public long? AddressId { get; set; } 52 | 53 | [Association(ThisKey = nameof(AddressId), OtherKey = nameof(Data.Models.Address.Id))] 54 | public Treatment? Address { get; set; } 55 | 56 | [Column] 57 | public string? Tag { get; set; } 58 | } 59 | -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/fit/task_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | 5 | import '../../ui/common/notification.dart'; 6 | import '../event.dart'; 7 | 8 | class Execution { 9 | DateTime date; 10 | SubmissionStatus state; 11 | String? status; 12 | 13 | Execution(this.date, this.state, this.status); 14 | } 15 | 16 | class TaskBloc extends Cubit { 17 | final List executions = []; 18 | Timer? timer; 19 | Future Function() action; 20 | Future Function() check; 21 | Duration duration; 22 | bool _running = false; 23 | 24 | TaskBloc(this.action, this.duration, this.check) : super(SubmissionStatus.initial); 25 | 26 | void cancel() { 27 | timer?.cancel(); 28 | emit(SubmissionStatus.initial); 29 | } 30 | 31 | Future start() async { 32 | timer = Timer.periodic(duration, (timer) async { 33 | try { 34 | if (!_running) { 35 | _running = true; 36 | if (await check.call()) { 37 | emit(SubmissionStatus.inProgress); 38 | var status = await action.call(); 39 | 40 | executions.add(Execution(DateTime.now(), status != null ? SubmissionStatus.success : SubmissionStatus.skipped, status)); 41 | emit(SubmissionStatus.success); 42 | } else { 43 | emit(SubmissionStatus.initial); 44 | } 45 | _running = false; 46 | } 47 | } catch (ex) { 48 | _running = false; 49 | executions.add(Execution(DateTime.now(), SubmissionStatus.failure, ex.toString())); 50 | emit(SubmissionStatus.failure); 51 | Notify.showError("$ex"); 52 | } 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /flutter_frontend/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | 13 | linter: 14 | # The lint rules applied to this project can be customized in the 15 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 16 | # included above or to enable additional rules. A list of all available lints 17 | # and their documentation is published at 18 | # https://dart-lang.github.io/linter/lints/index.html. 19 | # 20 | # Instead of disabling a lint rule for the entire project in the 21 | # section below, it can also be suppressed for a single line of code 22 | # or a specific dart file by using the `// ignore: name_of_lint` and 23 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 24 | # producing the lint. 25 | rules: 26 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 27 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 28 | 29 | # Additional information about this file can be found at 30 | # https://dart.dev/guides/language/analysis-options 31 | 32 | analyzer: 33 | exclude: [ generated_code/**] 34 | language: 35 | strict-casts: true 36 | strict-inference: true 37 | strict-raw-types: true -------------------------------------------------------------------------------- /Backend/Api/Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/loader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HelseLoader extends StatefulWidget { 4 | final bool static; 5 | final Color? color; 6 | final double size; 7 | final void Function()? onTouch; 8 | 9 | const HelseLoader({ 10 | super.key, 11 | this.static = false, 12 | this.color, 13 | this.size = 40, 14 | this.onTouch, 15 | }); 16 | 17 | @override 18 | State createState() => HelseLoaderState(); 19 | } 20 | 21 | class HelseLoaderState extends State with SingleTickerProviderStateMixin { 22 | late AnimationController controller; 23 | late Animation animation; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | controller = AnimationController( 29 | vsync: this, 30 | duration: const Duration(seconds: 1), 31 | ) 32 | ..forward() 33 | ..repeat(reverse: true); 34 | animation = Tween(begin: 0.0, end: 1.0).animate(controller); 35 | } 36 | 37 | @override 38 | void dispose() { 39 | controller.dispose(); 40 | super.dispose(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | var theme = widget.color ?? Theme.of(context).colorScheme.secondary; 46 | return Container( 47 | alignment: Alignment.center, 48 | child: FadeTransition( 49 | opacity: TweenSequence([ 50 | TweenSequenceItem( 51 | tween: Tween(begin: widget.static ? 1 : 0, end: 1).chain(CurveTween(curve: Curves.easeInOut)), 52 | weight: 1, 53 | ), 54 | ]).animate(controller), 55 | child: IconButton( 56 | color: theme, 57 | icon: const Icon(Icons.favorite), 58 | iconSize: widget.size, 59 | onPressed: widget.onTouch ?? () {}, 60 | ), 61 | )); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /flutter_frontend/lib/services/swagger/generated_code/swagger.enums.swagger.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:collection/collection.dart'; 3 | 4 | enum FileTypes { 5 | @JsonValue(null) 6 | swaggerGeneratedUnknown(null), 7 | 8 | @JsonValue(0) 9 | none(0), 10 | @JsonValue(1) 11 | redmiwatch(1), 12 | @JsonValue(2) 13 | googlehealthconnect(2), 14 | @JsonValue(3) 15 | clue(3); 16 | 17 | final int? value; 18 | 19 | const FileTypes(this.value); 20 | } 21 | 22 | enum MetricDataType { 23 | @JsonValue(null) 24 | swaggerGeneratedUnknown(null), 25 | 26 | @JsonValue(0) 27 | text(0), 28 | @JsonValue(1) 29 | number(1); 30 | 31 | final int? value; 32 | 33 | const MetricDataType(this.value); 34 | } 35 | 36 | enum MetricSummary { 37 | @JsonValue(null) 38 | swaggerGeneratedUnknown(null), 39 | 40 | @JsonValue(0) 41 | latest(0), 42 | @JsonValue(1) 43 | sum(1), 44 | @JsonValue(2) 45 | mean(2); 46 | 47 | final int? value; 48 | 49 | const MetricSummary(this.value); 50 | } 51 | 52 | enum RightType { 53 | @JsonValue(null) 54 | swaggerGeneratedUnknown(null), 55 | 56 | @JsonValue(1) 57 | view(1), 58 | @JsonValue(2) 59 | edit(2); 60 | 61 | final int? value; 62 | 63 | const RightType(this.value); 64 | } 65 | 66 | enum TreatmentType { 67 | @JsonValue(null) 68 | swaggerGeneratedUnknown(null), 69 | 70 | @JsonValue(0) 71 | care(0); 72 | 73 | final int? value; 74 | 75 | const TreatmentType(this.value); 76 | } 77 | 78 | enum UserType { 79 | @JsonValue(null) 80 | swaggerGeneratedUnknown(null), 81 | 82 | @JsonValue(0) 83 | patient(0), 84 | @JsonValue(1) 85 | admin(1), 86 | @JsonValue(2) 87 | caregiver(2), 88 | @JsonValue(4) 89 | user(4), 90 | @JsonValue(6) 91 | carewithself(6), 92 | @JsonValue(7) 93 | superuser(7); 94 | 95 | final int? value; 96 | 97 | const UserType(this.value); 98 | } 99 | -------------------------------------------------------------------------------- /Backend/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | all 32 | 33 | 34 | all 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/task_status_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/helpers/date.dart'; 3 | import 'package:helse/logic/fit/task_bloc.dart'; 4 | 5 | class TaskStatusDialog extends StatelessWidget { 6 | final List tasks; 7 | const TaskStatusDialog( 8 | this.tasks, { 9 | super.key, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return AlertDialog( 15 | title: const Text('Health sync history'), 16 | content: tasks.isEmpty 17 | ? const Text('No tasks') 18 | : Scrollbar( 19 | interactive: true, 20 | child: SingleChildScrollView( 21 | child: SizedBox( 22 | width: 400, 23 | height: 800, 24 | child: Column( 25 | mainAxisSize: MainAxisSize.min, 26 | children: [ 27 | Flexible( 28 | child: ListView.builder( 29 | itemCount: tasks.length, 30 | itemBuilder: (x, key) { 31 | var theme = Theme.of(x).textTheme; 32 | var task = tasks[key]; 33 | return Column( 34 | children: [ 35 | Text('${task.state.name} at ${DateHelper.format(task.date, context: x)}', style: theme.bodyLarge), 36 | Text(task.status ?? '', style: theme.bodySmall), 37 | const SizedBox(height: 10), 38 | ], 39 | ); 40 | }, 41 | ), 42 | ), 43 | ], 44 | ), 45 | ), 46 | ), 47 | )); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Backend/Api/Data/Scripts/002-Users.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA person; 2 | 3 | CREATE TABLE person.Address 4 | ( 5 | Id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, 6 | PersonId BIGINT NOT NULL, 7 | Street VARCHAR NULL , 8 | Number VARCHAR NULL , 9 | Postal VARCHAR NULL , 10 | Country CHAR NULL 11 | ); 12 | 13 | CREATE TABLE person.Person 14 | ( 15 | Id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, 16 | Name VARCHAR NULL , 17 | Surname VARCHAR NULL , 18 | Identifier VARCHAR NULL UNIQUE, 19 | Birth DATE NULL 20 | ); 21 | 22 | CREATE TABLE person.Right 23 | ( 24 | Id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, 25 | PersonId BIGINT NULL , 26 | UserId BIGINT NOT NULL, 27 | Start TIMESTAMP NOT NULL, 28 | Stop TIMESTAMP NULL , 29 | Type INT NOT NULL 30 | ); 31 | 32 | CREATE INDEX IF NOT EXISTS user_personid 33 | ON person."right" USING btree 34 | (userid ASC NULLS LAST) 35 | INCLUDE(personid, type, start, stop) 36 | WITH (deduplicate_items=True) 37 | TABLESPACE pg_default; 38 | 39 | CREATE TABLE person.User 40 | ( 41 | Id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, 42 | PersonId BIGINT NOT NULL, 43 | Identifier VARCHAR NOT NULL UNIQUE, 44 | Password VARCHAR NOT NULL, 45 | Type INT NOT NULL, 46 | Email VARCHAR NULL , 47 | Phone VARCHAR NULL 48 | ); 49 | 50 | ALTER TABLE person.Right 51 | ADD CONSTRAINT FK_Person_TO_Right 52 | FOREIGN KEY (PersonId) 53 | REFERENCES person.Person (Id); 54 | 55 | ALTER TABLE person.Right 56 | ADD CONSTRAINT FK_User_TO_Right 57 | FOREIGN KEY (UserId) 58 | REFERENCES person.User (Id); 59 | 60 | ALTER TABLE person.User 61 | ADD CONSTRAINT FK_Person_TO_User 62 | FOREIGN KEY (PersonId) 63 | REFERENCES person.Person (Id); 64 | 65 | ALTER TABLE person.Address 66 | ADD CONSTRAINT FK_Person_TO_Address 67 | FOREIGN KEY (PersonId) 68 | REFERENCES person.Person (Id); 69 | 70 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/administration/events/event_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../common/square_text_field.dart'; 4 | import '../../../common/statefull_check.dart'; 5 | 6 | class EventAddForm extends StatelessWidget { 7 | final TextEditingController controllerDescription; 8 | final TextEditingController controllerName; 9 | final FocusNode focusNodeName = FocusNode(); 10 | final FocusNode focusNodeDescription = FocusNode(); 11 | 12 | final void Function(bool value) visibleCallback; 13 | final bool visible; 14 | 15 | EventAddForm({ 16 | super.key, 17 | required this.controllerDescription, 18 | required this.controllerName, 19 | required this.visible, 20 | required this.visibleCallback, 21 | }); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | var theme = Theme.of(context).colorScheme; 26 | 27 | return Column( 28 | children: [ 29 | SquareTextField( 30 | controller: controllerName, 31 | focusNode: focusNodeName, 32 | label: "name", 33 | icon: Icons.person_sharp, 34 | theme: theme, 35 | validator: validateName, 36 | onEditingComplete: () => focusNodeDescription.requestFocus(), 37 | ), 38 | const SizedBox(height: 10), 39 | SquareTextField( 40 | controller: controllerDescription, 41 | focusNode: focusNodeDescription, 42 | label: "Description", 43 | icon: Icons.person_sharp, 44 | theme: theme, 45 | ),Padding( 46 | padding: const EdgeInsets.all(8.0), 47 | child: Row(children: [ 48 | const Text("Visible: "), 49 | StatefullCheck(visible, visibleCallback), 50 | ]), 51 | ), 52 | ], 53 | ); 54 | } 55 | 56 | String? validateName(String? value) { 57 | if (value == null || value.isEmpty) { 58 | return "Please enter a name."; 59 | } 60 | 61 | return null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /flutter_frontend/lib/services/metric_service.dart: -------------------------------------------------------------------------------- 1 | import 'api_service.dart'; 2 | import 'swagger/generated_code/swagger.swagger.dart'; 3 | 4 | class MetricService extends ApiService { 5 | MetricService(super.account); 6 | 7 | Future?> metricsType(bool all) async { 8 | var api = await getService(); 9 | return await call(() => api.apiMetricsTypeGet(all: all)); 10 | } 11 | 12 | Future deleteMetrics(int id) async { 13 | var api = await getService(); 14 | await call(() => api.apiMetricsIdDelete(id: id)); 15 | } 16 | 17 | Future addMetricsType(MetricType metric) async { 18 | var api = await getService(); 19 | await call(() => api.apiMetricsTypePost(body: metric)); 20 | } 21 | 22 | Future updateMetricsType(MetricType metric) async { 23 | var api = await getService(); 24 | await call(() => api.apiMetricsTypePut(body: metric)); 25 | } 26 | 27 | Future deleteMetricsType(int metric) async { 28 | var api = await getService(); 29 | await call(() => api.apiMetricsTypeIdDelete(id: metric)); 30 | } 31 | 32 | Future> metrics(int? type, DateTime? start, DateTime? end, {int? person, bool simple = false}) async { 33 | var api = await getService(); 34 | List? metrics; 35 | if (simple) { 36 | metrics = await call(() => api.apiMetricsSummaryGet(tile: 16, type: type, start: start?.toUtc(), end: end?.toUtc(), personId: person)); 37 | } else { 38 | metrics = await call(() => api.apiMetricsGet(type: type, start: start?.toUtc(), end: end?.toUtc(), personId: person)); 39 | } 40 | 41 | return metrics ?? []; 42 | } 43 | 44 | Future addMetrics(CreateMetric metric, {int? person}) async { 45 | var api = await getService(); 46 | await call(() => api.apiMetricsPost(body: metric, personId: person)); 47 | } 48 | 49 | Future updateMetrics(UpdateMetric metric) async { 50 | var api = await getService(); 51 | await call(() => api.apiMetricsPut(body: metric)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Backend/Api/Helpers/Auth/ProxyAuthLogic.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using Api.Data; 3 | using Api.Models; 4 | 5 | namespace Api.Helpers.Auth; 6 | 7 | public static class ProxyAuthHelper 8 | { 9 | public static async Task<(bool, TokenInfo?)> ConnectHeader(IUserContext db, HttpContext context, Proxy settings, ILogger log) 10 | { 11 | if (settings.Header is null) 12 | { 13 | return (false, null); 14 | } 15 | 16 | log.LogInformation("Connexion by proxy tentative using {header} in {headers}", settings.Header, context.Request.Headers); 17 | context.Request.Headers.TryGetValue(settings.Header, out var headers); 18 | var header = headers.FirstOrDefault(); 19 | 20 | if (header is not null) 21 | { 22 | log.LogInformation("Connexion by proxy auth header {header} and user {user}", settings.Header, header); 23 | } 24 | else 25 | { 26 | log.LogWarning("Connexion by proxy auth header {header} rejected", settings.Header); 27 | return (false, null); 28 | } 29 | 30 | var fromDb = await db.TokenFromDb(header); 31 | 32 | var logged = false; 33 | if (fromDb is null) 34 | { 35 | if (settings.AutoRegister) 36 | { 37 | log.LogInformation("User created for {header}", context.Request.Headers); 38 | // If auto register and not found, we create it 39 | await db.CreateUserAsync(new PersonCreation 40 | { 41 | UserName = header, 42 | Password = RandomNumberGenerator.GetInt32(100000000, int.MaxValue).ToString(), 43 | Type = UserType.User 44 | }, 0); 45 | logged = true; 46 | fromDb = await db.TokenFromDb(header); 47 | } 48 | } 49 | else 50 | { 51 | logged = true; 52 | } 53 | 54 | return (logged, fromDb); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Backend/Api/Logic/ImportLogic.cs: -------------------------------------------------------------------------------- 1 | using Api.Data; 2 | using Api.Helpers; 3 | using Api.Logic.Import; 4 | using Api.Models; 5 | using LinqToDB; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace Api.Logic; 9 | 10 | public record FileType(int Type, string? Name); 11 | 12 | public class ImportFile 13 | { 14 | public required string Content { get; set; } 15 | } 16 | 17 | public class ImportData 18 | { 19 | public List Metrics { get; set; } = []; 20 | 21 | public List Events { get; set; } = []; 22 | } 23 | 24 | /// 25 | /// Logic for the import of file 26 | /// 27 | public static class ImportLogic 28 | { 29 | public static IResult GetImportTypes() 30 | => TypedResults.Ok(Enum.GetValues().Select(x => new FileType((int)x, x.DescriptionAttr()))); 31 | 32 | public static async Task PostFileAsync([FromBody] ImportFile file, int type, IUserContext users, IHealthContext db, HttpContext context) 33 | { 34 | var (error, user) = await users.GetUser(context.User); 35 | if (error is not null) 36 | return error; 37 | 38 | var content = file.Content; 39 | 40 | Importer importer = (FileTypes)type switch 41 | { 42 | FileTypes.Clue => new ClueImporter(content, db, user), 43 | FileTypes.RedmiWatch => new RedmiWatch(content, db, user), 44 | _ => throw new NotSupportedException("Invalid file type"), 45 | }; 46 | 47 | await importer.Import(); 48 | 49 | return TypedResults.NoContent(); 50 | } 51 | 52 | public static async Task PostListAsync([FromBody] ImportData file, IUserContext users, IHealthContext db, HttpContext context) 53 | { 54 | var (error, user) = await users.GetUser(context.User); 55 | if (error is not null) 56 | return error; 57 | 58 | Importer importer = new ListImporter(file, db, user); 59 | 60 | await importer.Import(); 61 | 62 | return TypedResults.NoContent(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /flutter_frontend/lib/services/api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:chopper/chopper.dart'; 3 | import 'package:helse/services/swagger/generated_code/swagger.swagger.dart'; 4 | import 'package:jwt_decoder/jwt_decoder.dart'; 5 | import '../logic/d_i.dart'; 6 | import 'account.dart'; 7 | 8 | abstract class ApiService { 9 | final Account _account; 10 | 11 | ApiService(Account account) : _account = account; 12 | 13 | Future call(Future> Function() call) async { 14 | var response = await call(); 15 | T? result; 16 | 17 | if (!response.isSuccessful) { 18 | switch (response.statusCode) { 19 | case 401: 20 | // no auth, we remove the token and return null; 21 | // TODO get a new access_token if the refresh is still valid 22 | _account.remove(Account.token); 23 | DI.authentication.logOut(); 24 | result = null; 25 | break; 26 | default: 27 | throw Exception(response.error ?? "Login error"); 28 | } 29 | } else { 30 | result = response.body; 31 | } 32 | return result; 33 | } 34 | 35 | Future getService({String? override}) async { 36 | var url = override ?? await _account.get(Account.url); 37 | if (url == null) throw Exception("Url missing"); 38 | 39 | // first we try to get a new refresh token if needed 40 | var token = await _account.get(Account.token); 41 | if (token != null && token.isNotEmpty) { 42 | if (JwtDecoder.isExpired(token)) { 43 | var refresh = await _account.get(Account.refresh); 44 | var client = Swagger.create(baseUrl: Uri.parse(url), interceptors: [ 45 | HeadersInterceptor({'Authorization': 'Bearer $refresh'}) 46 | ]); 47 | var response = await client.apiAuthPost(body: const Connection(user: "", password: "")); 48 | token = response.body?.accessToken ?? ''; 49 | await _account.set(Account.token, token); 50 | } 51 | } 52 | 53 | return Swagger.create(baseUrl: Uri.parse(url), interceptors: [ 54 | HeadersInterceptor({'Authorization': 'Bearer $token'}) 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Backend/Tests/Unit/Logic/EventsLogicTests.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Api.Data; 3 | using Api.Data.Models; 4 | using Api.Logic; 5 | using Api.Models; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Http.HttpResults; 8 | using NSubstitute; 9 | 10 | namespace Tests.Unit.Logic; 11 | 12 | public class EventsLogicTests 13 | { 14 | private readonly IUserContext _users = Substitute.For(); 15 | private readonly IHealthContext _db = Substitute.For(); 16 | 17 | [Fact] 18 | public async Task EventType_NonAdmin() 19 | { 20 | var type = new Api.Data.Models.EventType() 21 | { 22 | Name = "", 23 | Description = "", 24 | }; 25 | var context = new DefaultHttpContext 26 | { 27 | User = new System.Security.Claims.ClaimsPrincipal([ 28 | new ClaimsIdentity(), 29 | ]) 30 | }; 31 | _users.Get(default).ReturnsForAnyArgs(new PersonFromDb(new User 32 | { 33 | Identifier = "", 34 | Password = "", 35 | Type = (int)UserType.Patient, 36 | }, new Api.Data.Models.Person())); 37 | 38 | var result = await EventsLogic.CreateTypeAsync(type, _users, _db, context); 39 | Assert.IsType(result); 40 | } 41 | 42 | [Fact] 43 | public async Task EventType() 44 | { 45 | var type = new Api.Data.Models.EventType() 46 | { 47 | Name = "", 48 | Description = "", 49 | }; 50 | var context = new DefaultHttpContext 51 | { 52 | User = new System.Security.Claims.ClaimsPrincipal([ 53 | new ClaimsIdentity(), 54 | ]) 55 | }; 56 | _users.Get(default).ReturnsForAnyArgs(new PersonFromDb(new User 57 | { 58 | Identifier = "", 59 | Password = "", 60 | Type = (int)UserType.Admin, 61 | }, new Api.Data.Models.Person())); 62 | 63 | var result = await EventsLogic.CreateTypeAsync(type, _users, _db, context); 64 | Assert.IsType(result); 65 | } 66 | } -------------------------------------------------------------------------------- /Backend/Api/Helpers/RightsHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Api.Data; 3 | using Api.Data.Models; 4 | using Api.Helpers.Auth; 5 | using Api.Models; 6 | 7 | namespace Api.Helpers; 8 | 9 | public static class RightsHelper 10 | { 11 | /// 12 | /// Validate that the user is a caregiver with the correct right 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | internal static async Task ValidateCaregiverAsync(this IUserContext db, User user, long personId, RightType type) 20 | { 21 | var now = DateTime.UtcNow; 22 | // check if the user has the right 23 | var right = await db.HasRightAsync(user.Id, personId, type, now); 24 | return right is not null; 25 | } 26 | 27 | internal static async Task IsAdmin(this IUserContext db, ClaimsPrincipal context) 28 | { 29 | var (error, user) = await db.GetUser(context); 30 | if (error is not null) 31 | return error; 32 | 33 | if (!user.Type.HasRight(UserType.Admin)) 34 | return TypedResults.Forbid(); 35 | 36 | return null; 37 | } 38 | 39 | internal static bool HasRight(this int type, Models.UserType right) => ((Models.UserType)type).HasFlag(right); 40 | 41 | internal static async Task<(IResult?, User)> GetUser(this IUserContext db, ClaimsPrincipal context) 42 | { 43 | // get the connected user 44 | var userName = context.GetUser(); 45 | 46 | var user = await db.Get(userName); 47 | if (user is null) 48 | return (TypedResults.Unauthorized(), User.Empty); 49 | 50 | return (null, user.User); 51 | } 52 | 53 | public static async Task TokenFromDb(this IUserContext db, string user) 54 | { 55 | var fromDb = await db.Get(user); 56 | 57 | if (fromDb is null) 58 | return null; 59 | 60 | return new TokenInfo(fromDb.User.Id, fromDb.User.Type.ToString(), 61 | fromDb.User.Identifier, fromDb.User.Password, 62 | fromDb.Person.Surname, fromDb.Person.Name, 63 | fromDb.User.Email); 64 | } 65 | } -------------------------------------------------------------------------------- /flutter_frontend/lib/services/event_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:helse/services/api_service.dart'; 2 | 3 | import 'swagger/generated_code/swagger.swagger.dart'; 4 | 5 | class EventService extends ApiService { 6 | EventService(super.account); 7 | 8 | Future?> eventsType(bool all) async { 9 | var api = await getService(); 10 | return await call(() => api.apiEventsTypeGet(all: all)); 11 | } 12 | 13 | Future addEventsType(EventType event) async { 14 | var api = await getService(); 15 | await call(() => api.apiEventsTypePost(body: event)); 16 | } 17 | 18 | Future updateEventsType(EventType event) async { 19 | var api = await getService(); 20 | await call(() => api.apiEventsTypePut(body: event)); 21 | } 22 | 23 | Future deleteEventsType(int event) async { 24 | var api = await getService(); 25 | await call(() => api.apiEventsTypeIdDelete(id: event)); 26 | } 27 | 28 | Future?> events(int? type, DateTime? start, DateTime? end, {int? person}) async { 29 | var api = await getService(); 30 | return await call(() => api.apiEventsGet(type: type, start: start?.toUtc(), end: end?.toUtc(), personId: person)); 31 | } 32 | 33 | Future?> eventsSummary(int? type, DateTime? start, DateTime? end, {int? person}) async { 34 | var api = await getService(); 35 | return await call(() => api.apiEventsSummaryGet(type: type, start: start?.toUtc(), end: end?.toUtc(), personId: person)); 36 | } 37 | 38 | Future?> agenda(DateTime? start, DateTime? end) async { 39 | var api = await getService(); 40 | return await call(() => api.apiPatientsAgendaGet(start: start?.toUtc(), end: end?.toUtc())); 41 | } 42 | 43 | Future addEvent(CreateEvent event, {int? person}) async { 44 | var api = await getService(); 45 | await call(() => api.apiEventsPost(body: event, personId: person)); 46 | } 47 | 48 | Future updateEvent(UpdateEvent event, {int? person}) async { 49 | var api = await getService(); 50 | await call(() => api.apiEventsPut(body: event, personId: person)); 51 | } 52 | 53 | Future deleteEvent(int event) async { 54 | var api = await getService(); 55 | await call(() => api.apiEventsIdDelete(id: event)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/date_range_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | 4 | class DateRangeInput extends StatelessWidget { 5 | final void Function(DateTimeRange date) _setDateCallback; 6 | final DateTimeRange initial; 7 | final DateTimeRange? range; 8 | final DateFormat formatter = DateFormat('dd/MM/yyyy'); 9 | final bool large; 10 | final bool showIcon; 11 | 12 | DateRangeInput(void Function(DateTimeRange date) setDate, this.initial, this.large, {super.key, this.range, this.showIcon = true}) : _setDateCallback = setDate; 13 | 14 | String _displayDate(DateTimeRange date) { 15 | return "${formatter.format(date.start)} - ${formatter.format(date.end)}"; 16 | } 17 | 18 | Future _setDate(BuildContext context, DateTimeRange initial) async { 19 | var date = await _pick(context, initial); 20 | if (date != null) { 21 | _setDateCallback(date); 22 | } 23 | } 24 | 25 | Future _pick(BuildContext context, DateTimeRange initial) async { 26 | var selectedDate = await showDateRangePicker( 27 | context: context, 28 | initialDateRange: initial, //get today's date 29 | firstDate: range?.start ?? DateTime(1000), 30 | lastDate: range?.end ?? DateTime(3000)); 31 | if (selectedDate == null) return null; 32 | 33 | var start = selectedDate.start; 34 | var end = selectedDate.end; 35 | 36 | return DateTimeRange(start: start, end: DateTime(end.year, end.month, end.day, 23, 59, 59)); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | var theme = Theme.of(context); 42 | return InkWell( 43 | onTap: () { 44 | _setDate(context, initial); 45 | }, 46 | child: Row( 47 | children: [ 48 | if(showIcon) 49 | const Padding( 50 | padding: EdgeInsets.only(right: 8.0), 51 | child: Icon( 52 | Icons.edit_calendar_sharp, 53 | ), 54 | ), 55 | Text( 56 | _displayDate(initial), 57 | style: large ? theme.textTheme.bodyMedium : theme.textTheme.bodySmall, 58 | softWrap: true, 59 | maxLines: 2, 60 | ), 61 | ], 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Backend/Api/Logic/Import/ClueImporter.cs: -------------------------------------------------------------------------------- 1 | using Api.Data; 2 | using Api.Data.Models; 3 | using Api.Logic.Import.Clue; 4 | using Api.Models; 5 | using Newtonsoft.Json; 6 | 7 | namespace Api.Logic.Import; 8 | 9 | public class ClueImporter(string file, IHealthContext db, User user) : FileImporter(file, db, user) 10 | { 11 | public override async Task Import() 12 | { 13 | // parse the file as an arry of json object 14 | var json = JsonConvert.DeserializeObject(File); 15 | if (json is null) 16 | { 17 | return; 18 | } 19 | 20 | foreach (var node in json) 21 | { 22 | if (node.Date is null || node.Value?.Any() != true) 23 | continue; 24 | 25 | foreach (var value in node.Value.Where(x => x.Option is not null)) 26 | { 27 | var (type, subValue) = GetType(node.Type); 28 | var metric = new CreateMetric 29 | { 30 | Type = (int)type, 31 | Tag = node.Id + value.Option, 32 | Source = FileTypes.Clue, 33 | Date = DateTime.Parse(node.Date), 34 | Value = $"{subValue}{value.Option?.Replace('_', ' ')}", 35 | }; 36 | // import the data 37 | await ImportMetric(metric); 38 | } 39 | } 40 | } 41 | 42 | private static (MetricTypes, string?) GetType(string? type) => type switch 43 | { 44 | "medication" => (MetricTypes.Medication, null), 45 | "birth_control_ring" => (MetricTypes.Medication, "Birth control ring "), 46 | "birth_control_pill" => (MetricTypes.Medication, "Birth control pill "), 47 | "mind" => (MetricTypes.Mood, null), 48 | "pain" => (MetricTypes.Pain, null), 49 | "sex_life" => (MetricTypes.Sex, null), 50 | "stool" => (MetricTypes.Stool, null), 51 | "energy" => (MetricTypes.Mood, null), 52 | "spotting" => (MetricTypes.Spotting, null), 53 | "period" => (MetricTypes.Menstruation, null), 54 | "discharge" => (MetricTypes.Menstruation, "discharge "), 55 | "feelings" => (MetricTypes.Mood, null), 56 | "pms" => (MetricTypes.Pain, "Premenstrual syndrome "), 57 | "tests" => (MetricTypes.Tests, null), 58 | _ => throw new InvalidDataException(type), 59 | }; 60 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Launch (web)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceFolder}/Backend/Api/bin/Debug/net8.0/Api.dll", 10 | "args": [], 11 | "cwd": "${workspaceFolder}/Backend/Api", 12 | "stopAtEntry": false, 13 | "env": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | } 16 | }, 17 | { 18 | "name": ".NET Core Attach", 19 | "type": "coreclr", 20 | "request": "attach" 21 | }, 22 | { 23 | "name": ".NET Core Launch with swagger", 24 | "type": "coreclr", 25 | "request": "launch", 26 | "preLaunchTask": "build", 27 | "program": "${workspaceFolder}/Backend/Api/bin/Debug/net8.0/Api.dll", 28 | "args": [], 29 | "cwd": "${workspaceFolder}/Backend/Api", 30 | "stopAtEntry": false, 31 | "env": { 32 | "ASPNETCORE_ENVIRONMENT": "Development" 33 | }, 34 | "sourceFileMap": { 35 | "/Views": "${workspaceFolder}/Views" 36 | }, 37 | "serverReadyAction": { 38 | "action": "openExternally", 39 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)", 40 | "uriFormat": "%s/swagger/index.html" 41 | } 42 | }, 43 | { 44 | "name": "Flutter android", 45 | "type": "dart", 46 | "request": "launch", 47 | "program": "flutter_frontend/lib/main.dart", 48 | }, 49 | { 50 | "name": "Flutter web", 51 | "type": "dart", 52 | "request": "launch", 53 | "program": "flutter_frontend/lib/main.dart", 54 | "flutterMode": "release", 55 | "args": [ 56 | "-d", 57 | "web-server" 58 | ] 59 | } 60 | ], 61 | "compounds": [ 62 | { 63 | "name": "Launch", 64 | "configurations": [ 65 | "Flutter web", 66 | ".NET Core Launch (web)" 67 | ], 68 | "stopAll": true 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/administration/users/user_change_role.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/logic/d_i.dart'; 3 | import 'package:helse/services/swagger/generated_code/swagger.swagger.dart'; 4 | import 'package:helse/ui/common/square_dialog.dart'; 5 | 6 | import '../../../../services/swagger/generated_code/swagger.enums.swagger.dart'; 7 | import '../../../common/notification.dart'; 8 | import '../../../common/type_input.dart'; 9 | 10 | class ChangeRole extends StatefulWidget { 11 | final void Function() callback; 12 | final UserType type; 13 | final int id; 14 | 15 | const ChangeRole(this.callback, this.type, this.id, {super.key}); 16 | 17 | @override 18 | State createState() => _ChangeRoleState(); 19 | } 20 | 21 | class _ChangeRoleState extends State { 22 | final GlobalKey _formKey = GlobalKey(); 23 | UserType? _type; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return SquareDialog( 28 | title: const Text("Change role"), 29 | actions: [ 30 | ElevatedButton( 31 | style: ElevatedButton.styleFrom( 32 | minimumSize: const Size.fromHeight(50), 33 | shape: const ContinuousRectangleBorder(), 34 | ), 35 | onPressed: submit, 36 | child: const Text("Update"), 37 | ), 38 | ], 39 | content: Form( 40 | key: _formKey, 41 | child: SingleChildScrollView( 42 | padding: const EdgeInsets.symmetric(horizontal: 30.0), 43 | child: Column( 44 | children: [ 45 | TypeInput( 46 | value: widget.type, 47 | UserType.values.map((x) => DropDownItem(x, x.name)).toList(), 48 | (value) => setState(() { 49 | _type = value; 50 | })), 51 | ], 52 | ), 53 | ), 54 | ), 55 | ); 56 | } 57 | 58 | void submit() async { 59 | var localContext = context; 60 | try { 61 | // save the user 62 | await DI.user.updatePersonRole(widget.id, _type ?? UserType.user); 63 | 64 | widget.callback.call(); 65 | 66 | if (localContext.mounted) { 67 | Navigator.of(localContext).pop(); 68 | } 69 | 70 | Notify.show("Updated Successfully"); 71 | } catch (ex) { 72 | Notify.showError("$ex"); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/care/agenda.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/logic/d_i.dart'; 3 | 4 | import '../../../services/swagger/generated_code/swagger.swagger.dart'; 5 | import '../events/events_graph.dart'; 6 | import '../../common/loader.dart'; 7 | 8 | class Agenda extends StatefulWidget { 9 | final DateTimeRange date; 10 | const Agenda({super.key, required this.date}); 11 | 12 | @override 13 | State createState() => _AgendaState(); 14 | } 15 | 16 | class _AgendaState extends State { 17 | List? events; 18 | 19 | Future?> _getData() async { 20 | if (events != null) { 21 | return events; 22 | } 23 | 24 | var date = widget.date; 25 | 26 | var start = DateTime(date.start.year, date.start.month, date.start.day); 27 | var end = DateTime(date.end.year, date.end.month, date.end.day).add(const Duration(days: 1)); 28 | 29 | events = await DI.event.agenda(start, end); 30 | 31 | return events; 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Column( 37 | children: [ 38 | Padding( 39 | padding: const EdgeInsets.all(8.0), 40 | child: Row( 41 | children: [ 42 | Text("Agenda", style: Theme.of(context).textTheme.headlineSmall), 43 | ], 44 | ), 45 | ), 46 | FutureBuilder( 47 | future: _getData(), 48 | builder: (ctx, snapshot) { 49 | // Checking if future is resolved 50 | if (snapshot.connectionState == ConnectionState.done) { 51 | // If we got an error 52 | if (snapshot.hasError) { 53 | return Center( 54 | child: Text( 55 | '${snapshot.error} occurred', 56 | style: const TextStyle(fontSize: 18), 57 | ), 58 | ); 59 | 60 | // if we got our data 61 | } 62 | 63 | final events = (snapshot.hasData) ? snapshot.data as List : List.empty(); 64 | return Padding( 65 | padding: const EdgeInsets.all(8.0), 66 | child: EventGraph(events, widget.date, (e) => {}), 67 | ); 68 | } 69 | return const HelseLoader(); 70 | }), 71 | ], 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /flutter_frontend/lib/helpers/oauth.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'package:app_links/app_links.dart'; 3 | import 'package:helse/logic/account/authentication_logic.dart'; 4 | import 'package:helse/logic/d_i.dart'; 5 | import 'package:universal_html/html.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:url_launcher/url_launcher.dart'; 8 | 9 | import '../services/account.dart'; 10 | 11 | class OauthClient { 12 | String? _auth; 13 | String? _clientId; 14 | final Account account; 15 | 16 | OauthClient(this.account); 17 | 18 | void listen(Null Function(dynamic user) param0) {} 19 | 20 | Uri get redirectUrl { 21 | if (kIsWeb) { 22 | return Uri.base; 23 | } else { 24 | return Uri.parse('com.helse://login-callback'); 25 | } 26 | } 27 | 28 | void init({ 29 | required String auth, 30 | required String clientId, 31 | }) async { 32 | _auth = auth; 33 | _clientId = clientId; 34 | } 35 | 36 | Future login(String url) async { 37 | await account.set(Account.url, url); 38 | 39 | var grant = await account.get(Account.grant); 40 | if (grant == null) { 41 | var redirect = redirectUrl.toString(); 42 | final authUrl = 43 | '$_auth?client_id=$_clientId&response_type=code&scope=openid+profile+offline_access&state=STATE&redirect_uri=$redirect'; 44 | 45 | await account.set(Account.redirect, redirect); 46 | 47 | if (kIsWeb) { 48 | window.location.assign(authUrl); 49 | return null; 50 | } else { 51 | _doAuthOnMobile(authUrl, redirect); 52 | return null; 53 | } 54 | } else { 55 | return grant; 56 | } 57 | } 58 | 59 | Future getCode(Map uri) async { 60 | var code = uri['code'] ?? ''; 61 | await account.set(Account.grant, code); 62 | } 63 | 64 | void _doAuthOnMobile(String result, String redirect) async { 65 | var links = AppLinks(); 66 | links.uriLinkStream.listen((uri) { 67 | if (uri.toString().startsWith(redirect)) { 68 | getCode(uri.queryParameters).then((value) => 69 | DI.authentication.set(AuthenticationStatus.unauthenticated)); 70 | } 71 | }); 72 | 73 | DI.authentication.set(AuthenticationStatus.unknown); 74 | var uri = Uri.parse(result); 75 | if (await canLaunchUrl(uri)) { 76 | await launchUrl(uri); 77 | } 78 | } 79 | 80 | void doAuthOnWeb(Map uri) => getCode(uri); 81 | } 82 | -------------------------------------------------------------------------------- /Backend/Api/Helpers/Auth/TokenService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.IdentityModel.Tokens; 3 | using System.IdentityModel.Tokens.Jwt; 4 | using Microsoft.AspNetCore.Identity; 5 | using Api.Data.Models; 6 | 7 | namespace Api.Helpers.Auth; 8 | 9 | public record TokenInfo(long Id, string Role, string Identifier, string Password, string? Surname, string? Name, string? Email); 10 | 11 | public class TokenService(string issuer, string audience, SymmetricSecurityKey key) 12 | { 13 | private readonly string _issuer = issuer; 14 | private readonly string _audience = audience; 15 | private readonly SymmetricSecurityKey _key = key; 16 | 17 | public static string Hash(string password) 18 | { 19 | return new PasswordHasher().HashPassword(User.Empty, password); 20 | } 21 | 22 | public static PasswordVerificationResult Verify(string password, string hash) 23 | { 24 | return new PasswordHasher().VerifyHashedPassword(User.Empty, hash, password); 25 | } 26 | 27 | public string GetRefreshToken(TokenInfo info, DateTime expires) 28 | { 29 | var claims = new List 30 | { 31 | new(JwtRegisteredClaimNames.NameId, info.Identifier), 32 | new("roles", info.Role), 33 | new("surname", info.Surname ?? string.Empty), 34 | new("name", info.Name ?? string.Empty), 35 | new("token", "refresh"), 36 | }; 37 | 38 | if (info.Email != null) 39 | { 40 | claims.Add( 41 | new Claim(JwtRegisteredClaimNames.Email, info.Email)); 42 | } 43 | return GetToken(claims, expires); 44 | } 45 | 46 | public string GetAccessToken(TokenInfo info, DateTime expires) 47 | { 48 | var claims = new List 49 | { 50 | new(JwtRegisteredClaimNames.NameId, info.Identifier), 51 | new ("token", "access"), 52 | }; 53 | 54 | return GetToken(claims, expires); 55 | } 56 | 57 | private string GetToken(List claims, DateTime expires) 58 | { 59 | var tokenDescriptor = new SecurityTokenDescriptor 60 | { 61 | Subject = new ClaimsIdentity(claims), 62 | Expires = expires, 63 | Issuer = _issuer, 64 | Audience = _audience, 65 | SigningCredentials = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature) 66 | }; 67 | var tokenHandler = new JwtSecurityTokenHandler(); 68 | var token = tokenHandler.CreateToken(tokenDescriptor); 69 | return tokenHandler.WriteToken(token); 70 | } 71 | } -------------------------------------------------------------------------------- /Backend/Api/Logic/TreatmentLogic.cs: -------------------------------------------------------------------------------- 1 | using Api.Data; 2 | using Api.Helpers; 3 | using Api.Models; 4 | using LinqToDB; 5 | 6 | namespace Api.Logic; 7 | 8 | /// 9 | /// Management of the users and rights 10 | /// 11 | public static class TreatmentLogic 12 | { 13 | public static async Task GetTypeAsync(IHealthContext db) => TypedResults.Ok(await db.GetEventTypes(false)); 14 | 15 | public async static Task PostAsync(Models.CreateTreatment treatment, IUserContext db, HttpContext context) 16 | { 17 | var (error, user) = await db.GetUser(context.User); 18 | if (error is not null) 19 | return error; 20 | 21 | // check the personId 22 | 23 | if (treatment.PersonId is not null 24 | && treatment.PersonId != user.PersonId 25 | && !await db.ValidateCaregiverAsync(user, treatment.PersonId.Value, RightType.Edit)) 26 | { 27 | return TypedResults.Forbid(); 28 | } 29 | 30 | // TODO lock only the tables 31 | await using var transaction = await db.BeginTransactionAsync(); 32 | 33 | // create the treament 34 | var id = await db.InsertTreatment(treatment.PersonId ?? user.PersonId, TreatmentType.Care); 35 | 36 | // create the events 37 | // TODO bulk insert 38 | foreach (var e in treatment.Events) 39 | { 40 | await db.InsertEvent(e, treatment.PersonId ?? user.PersonId, user.Id, id); 41 | } 42 | 43 | await transaction.CommitAsync(); 44 | 45 | return TypedResults.NoContent(); 46 | } 47 | 48 | public async static Task GetAsync(DateTime start, DateTime end, long? personId, IUserContext users, IHealthContext db, HttpContext context) 49 | { 50 | var (error, user) = await users.GetUser(context.User); 51 | if (error is not null) 52 | return error; 53 | 54 | if (personId is not null && !await users.ValidateCaregiverAsync(user, personId.Value, RightType.View)) 55 | return TypedResults.Forbid(); 56 | 57 | var id = personId ?? user.PersonId; 58 | var events = await db.GetEvents(id, start, end); 59 | 60 | return TypedResults.Ok(events.GroupBy(x => x.TreatmentId).Select(t => new Treatement 61 | { 62 | Events = t.Select(x => new Models.Event 63 | { 64 | Id = x.Id, 65 | Type = x.Type, 66 | Description = x.Description, 67 | Stop = x.Stop, 68 | File = x.FileId, 69 | Start = x.Start, 70 | Valid = x.Valid, 71 | }).ToList() 72 | })); 73 | } 74 | } -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/treatments/treatments_grid.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/logic/d_i.dart'; 3 | 4 | import '../../../services/swagger/generated_code/swagger.swagger.dart'; 5 | import '../events/events_graph.dart'; 6 | import '../../common/loader.dart'; 7 | import '../../common/notification.dart'; 8 | 9 | class TreatmentsGrid extends StatefulWidget { 10 | const TreatmentsGrid({super.key, required this.date, this.person}); 11 | 12 | final DateTimeRange date; 13 | final int? person; 14 | 15 | @override 16 | State createState() => _TreatmentsGridState(); 17 | } 18 | 19 | class _TreatmentsGridState extends State { 20 | List? _treatments; 21 | 22 | Future?> _getData() async { 23 | try { 24 | if (_treatments != null) return _treatments; 25 | 26 | var date = widget.date; 27 | 28 | var start = DateTime(date.start.year, date.start.month, date.start.day); 29 | var end = DateTime(date.end.year, date.end.month, date.end.day) 30 | .add(const Duration(days: 1)); 31 | 32 | _treatments = 33 | await DI.treatement?.treatments(start, end, person: widget.person); 34 | return _treatments; 35 | } catch (ex) { 36 | Notify.showError("$ex"); 37 | } 38 | return _treatments; 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return FutureBuilder( 44 | future: _getData(), 45 | builder: (ctx, snapshot) { 46 | // Checking if future is resolved 47 | if (snapshot.connectionState == ConnectionState.done) { 48 | // If we got an error 49 | if (snapshot.hasError) { 50 | return Center( 51 | child: Text( 52 | '${snapshot.error} occurred', 53 | style: const TextStyle(fontSize: 18), 54 | ), 55 | ); 56 | 57 | // if we got our data 58 | } 59 | 60 | final events = (snapshot.hasData) 61 | ? snapshot.data as List 62 | : List.empty(); 63 | 64 | return ListView( 65 | shrinkWrap: true, 66 | children: events 67 | .map((e) => Padding( 68 | padding: const EdgeInsets.all(8.0), 69 | child: EventGraph( 70 | e.events ?? List.empty(), widget.date, (e) => {}), 71 | )) 72 | .toList(), 73 | ); 74 | } 75 | return const HelseLoader(); 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/administration/metrics/metrics_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/services/swagger/generated_code/swagger.swagger.dart'; 3 | import 'package:helse/ui/common/notification.dart'; 4 | 5 | import '../../../common/loader.dart'; 6 | 7 | class MetricSettingsView extends StatefulWidget { 8 | const MetricSettingsView({super.key}); 9 | 10 | @override 11 | State createState() => _SettingsViewState(); 12 | } 13 | 14 | class _SettingsViewState extends State { 15 | Proxy? _settings; 16 | final GlobalKey _formKey = GlobalKey(); 17 | 18 | void _resetSettings() { 19 | setState(() { 20 | _settings = null; 21 | }); 22 | } 23 | 24 | Future _getData() async { 25 | 26 | _settings = const Proxy(); 27 | return _settings; 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return FutureBuilder( 33 | future: _getData(), 34 | builder: (context, snapshot) { 35 | // Checking if future is resolved 36 | if (snapshot.connectionState == ConnectionState.done) { 37 | // If we got an error 38 | if (snapshot.hasError) { 39 | return Center( 40 | child: Text( 41 | '${snapshot.error} occurred', 42 | style: const TextStyle(fontSize: 18), 43 | ), 44 | ); 45 | 46 | // if we got our data 47 | } else if (snapshot.hasData) { 48 | return Form( 49 | key: _formKey, 50 | child: const Column( 51 | children: [ 52 | /*ElevatedButton( 53 | style: ElevatedButton.styleFrom( 54 | minimumSize: const Size.fromHeight(50), 55 | shape: const ContinuousRectangleBorder(), 56 | ), 57 | onPressed: submit, 58 | child: const Text("Save"), 59 | ),*/ 60 | ], 61 | ), 62 | ); 63 | } 64 | } 65 | return const Center( 66 | child: SizedBox(width: 50, height: 50, child: HelseLoader())); 67 | }); 68 | } 69 | 70 | void submit() async { 71 | try { 72 | if (_formKey.currentState?.validate() ?? false) { 73 | // save the settings 74 | // await AppState.settings?.save(); 75 | 76 | Notify.show("Saved Successfully"); 77 | 78 | _resetSettings(); 79 | } 80 | } catch (ex) { 81 | Notify.showError("Error: $ex"); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/events/events_grid.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:helse/ui/common/loader.dart'; 5 | 6 | import '../../../logic/d_i.dart'; 7 | import '../../../logic/settings/ordered_item.dart'; 8 | import '../../../logic/settings/settings_logic.dart'; 9 | import '../../../services/swagger/generated_code/swagger.swagger.dart'; 10 | import '../../common/notification.dart'; 11 | import 'events_widget.dart'; 12 | 13 | class EventsGrid extends StatefulWidget { 14 | const EventsGrid({ 15 | super.key, 16 | required this.date, 17 | this.person, 18 | }); 19 | 20 | final DateTimeRange date; 21 | final int? person; 22 | 23 | @override 24 | State createState() => _EventsGridState(); 25 | } 26 | 27 | class _EventsGridState extends State { 28 | List? types; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _getData(); 34 | } 35 | 36 | void _getData() async { 37 | try { 38 | var model = await DI.event.eventsType(false); 39 | if (model != null) { 40 | var settings = await SettingsLogic.getEvents(); 41 | 42 | // filter using the user settings 43 | List filtered = []; 44 | for (var item in model) { 45 | OrderedItem? setting = settings.events.firstWhereOrNull((element) => element.id == item.id); 46 | 47 | if (setting == null || setting.visible) filtered.add(item); 48 | } 49 | 50 | setState(() { 51 | types = filtered; 52 | }); 53 | SettingsLogic.updateEvents(model); 54 | } 55 | } catch (ex) { 56 | Notify.showError("$ex"); 57 | } 58 | } 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | return types == null 63 | ? const HelseLoader() 64 | : BlocListener( 65 | listener: (context, state) { 66 | _getData(); 67 | }, 68 | bloc: DI.settings.events, 69 | child: ListView( 70 | shrinkWrap: true, 71 | physics: const BouncingScrollPhysics(), 72 | children: types 73 | ?.map((type) => Column( 74 | children: [ 75 | Divider(color: Theme.of(context).colorScheme.secondary), 76 | EventWidget(type, widget.date, key: Key(type.id?.toString() ?? ""), person: widget.person), 77 | ], 78 | )) 79 | .toList() ?? 80 | [], 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /flutter_frontend/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | def keystoreProperties = new Properties() 26 | def keystorePropertiesFile = rootProject.file('key.properties') 27 | if (keystorePropertiesFile.exists()) { 28 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 29 | } 30 | 31 | android { 32 | namespace "com.fschiltz.helse" 33 | compileSdk flutter.compileSdkVersion 34 | ndkVersion "26.1.10909125" 35 | 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_1_8 38 | targetCompatibility JavaVersion.VERSION_1_8 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = '1.8' 43 | } 44 | 45 | sourceSets { 46 | main.java.srcDirs += 'src/main/kotlin' 47 | } 48 | 49 | defaultConfig { 50 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 51 | applicationId "com.fschiltz.helse" 52 | // You can update the following values to match your application needs. 53 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 54 | minSdkVersion 26 55 | targetSdkVersion flutter.targetSdkVersion 56 | versionCode flutterVersionCode.toInteger() 57 | versionName flutterVersionName 58 | } 59 | 60 | 61 | signingConfigs { 62 | release { 63 | keyAlias keystoreProperties['keyAlias'] 64 | keyPassword keystoreProperties['keyPassword'] 65 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 66 | storePassword keystoreProperties['storePassword'] 67 | } 68 | } 69 | 70 | buildTypes { 71 | release { 72 | // TODO: Add your own signing config for the release build. 73 | // Signing with the debug keys for now, so `flutter run --release` works. 74 | signingConfig signingConfigs.release 75 | } 76 | } 77 | } 78 | 79 | flutter { 80 | source '../..' 81 | } 82 | 83 | dependencies {} 84 | -------------------------------------------------------------------------------- /flutter_frontend/ios/Runner/GeneratedPluginRegistrant.m: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #import "GeneratedPluginRegistrant.h" 8 | 9 | #if __has_include() 10 | #import 11 | #else 12 | @import app_links; 13 | #endif 14 | 15 | #if __has_include() 16 | #import 17 | #else 18 | @import device_info_plus; 19 | #endif 20 | 21 | #if __has_include() 22 | #import 23 | #else 24 | @import file_selector_ios; 25 | #endif 26 | 27 | #if __has_include() 28 | #import 29 | #else 30 | @import fluttertoast; 31 | #endif 32 | 33 | #if __has_include() 34 | #import 35 | #else 36 | @import health; 37 | #endif 38 | 39 | #if __has_include() 40 | #import 41 | #else 42 | @import path_provider_foundation; 43 | #endif 44 | 45 | #if __has_include() 46 | #import 47 | #else 48 | @import permission_handler_apple; 49 | #endif 50 | 51 | #if __has_include() 52 | #import 53 | #else 54 | @import shared_preferences_foundation; 55 | #endif 56 | 57 | #if __has_include() 58 | #import 59 | #else 60 | @import url_launcher_ios; 61 | #endif 62 | 63 | @implementation GeneratedPluginRegistrant 64 | 65 | + (void)registerWithRegistry:(NSObject*)registry { 66 | [AppLinksPlugin registerWithRegistrar:[registry registrarForPlugin:@"AppLinksPlugin"]]; 67 | [FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]]; 68 | [FileSelectorPlugin registerWithRegistrar:[registry registrarForPlugin:@"FileSelectorPlugin"]]; 69 | [FluttertoastPlugin registerWithRegistrar:[registry registrarForPlugin:@"FluttertoastPlugin"]]; 70 | [HealthPlugin registerWithRegistrar:[registry registrarForPlugin:@"HealthPlugin"]]; 71 | [PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]]; 72 | [PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]]; 73 | [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; 74 | [URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]]; 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /Backend/Api/Logic/PatientsLogic.cs: -------------------------------------------------------------------------------- 1 | using Api.Data; 2 | using Api.Helpers; 3 | using Api.Models; 4 | using LinqToDB; 5 | 6 | namespace Api.Logic; 7 | 8 | public static class PatientsLogic 9 | { 10 | /// 11 | /// Get the list of person the caller can manage 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static async Task GetPatientsAsync(IUserContext users, IHealthContext db, HttpContext context) 17 | { 18 | var (error, user) = await users.GetUser(context.User); 19 | if (error is not null) 20 | return error; 21 | 22 | var now = DateTime.UtcNow; 23 | var persons = await db.GetPatients(user.Id, now, RightType.View); 24 | 25 | var models = persons.Select(x => 26 | { 27 | return new Person 28 | { 29 | Id = x.Id, 30 | Birth = x.Birth, 31 | Name = x.Name, 32 | Surname = x.Surname, 33 | Identifier = x.Identifier, 34 | Type = UserType.Patient, 35 | }; 36 | }); 37 | 38 | return TypedResults.Ok(models); 39 | } 40 | 41 | /// 42 | /// Share a patient between caregiver 43 | /// 44 | public async static Task SharePatient(int patient, int caregiver, bool edit, IUserContext users, IHealthContext db, HttpContext context) 45 | { 46 | var (error, user) = await users.GetUser(context.User); 47 | if (error is not null) 48 | return error; 49 | 50 | // to share a patient, the user need to have write access to it 51 | if (!await users.ValidateCaregiverAsync(user, patient, RightType.Edit)) 52 | return TypedResults.Forbid(); 53 | 54 | await users.AddRight(caregiver, patient, RightType.View); 55 | if (edit) 56 | { 57 | await users.AddRight(caregiver, patient, RightType.Edit); 58 | } 59 | 60 | return TypedResults.NoContent(); 61 | } 62 | 63 | public async static Task GetAgendaAsync(DateTime start, DateTime end, IUserContext users, IHealthContext db, HttpContext context) 64 | { 65 | var (error, user) = await users.GetUser(context.User); 66 | if (error is not null) 67 | return error; 68 | 69 | var id = user.PersonId; 70 | var events = await db.GetEvents(user.Id, RightType.View, start, end); 71 | 72 | return TypedResults.Ok(events.Select(x => new Event 73 | { 74 | Id = x.Id, 75 | Type = x.Type, 76 | Description = x.Description, 77 | Stop = x.Stop, 78 | File = x.FileId, 79 | Start = x.Start, 80 | Valid = x.Valid, 81 | })); 82 | } 83 | } -------------------------------------------------------------------------------- /Backend/Api/Logic/SettingsLogic.cs: -------------------------------------------------------------------------------- 1 | using Api.Data; 2 | using Api.Helpers; 3 | using Api.Models; 4 | 5 | namespace Api.Logic; 6 | 7 | /// 8 | /// Management of the sserver settings 9 | /// 10 | public static class SettingsLogic 11 | { 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static async Task GetOauthAsync(IUserContext users, ISettingsContext settings, HttpContext context) 19 | { 20 | var admin = await users.IsAdmin(context.User); 21 | if (admin is not null) 22 | return admin; 23 | 24 | return TypedResults.Ok(await settings.GetSettings(Oauth.Name)); 25 | } 26 | 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 33 | public static async Task GetProxyAsync(IUserContext users, ISettingsContext settings, HttpContext context) 34 | { 35 | var admin = await users.IsAdmin(context.User); 36 | if (admin is not null) 37 | return admin; 38 | 39 | return TypedResults.Ok(await settings.GetSettings(Proxy.Name)); 40 | } 41 | 42 | /// 43 | /// 44 | /// 45 | /// 46 | /// 47 | /// 48 | /// 49 | public static async Task PostOauthAsync(Oauth settings, IUserContext users, ISettingsContext db, HttpContext context, ILoggerFactory logger) 50 | { 51 | var log = logger.CreateLogger(nameof(SettingsLogic)); 52 | 53 | var admin = await users.IsAdmin(context.User); 54 | if (admin is not null) 55 | return admin; 56 | 57 | await db.SaveSettingsAsync(Oauth.Name, settings); 58 | 59 | log.LogInformation("Oauth settings saved"); 60 | 61 | return TypedResults.Created(); 62 | } 63 | 64 | /// 65 | /// 66 | /// 67 | /// 68 | /// 69 | /// 70 | /// 71 | public static async Task PostProxyAsync(Proxy settings, IUserContext users, ISettingsContext db, HttpContext context, ILoggerFactory logger) 72 | { 73 | var log = logger.CreateLogger(nameof(SettingsLogic)); 74 | 75 | var admin = await users.IsAdmin(context.User); 76 | if (admin is not null) 77 | return admin; 78 | 79 | await db.SaveSettingsAsync(Proxy.Name, settings); 80 | 81 | log.LogInformation("Proxy settings saved"); 82 | 83 | return TypedResults.Created(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/administration/events/event_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/services/swagger/generated_code/swagger.swagger.dart'; 3 | import 'package:helse/ui/common/notification.dart'; 4 | 5 | import '../../../common/loader.dart'; 6 | 7 | class EventSettingsView extends StatefulWidget { 8 | const EventSettingsView({super.key}); 9 | 10 | @override 11 | State createState() => _SettingsViewState(); 12 | } 13 | 14 | class _SettingsViewState extends State { 15 | Proxy? _settings; 16 | final GlobalKey _formKey = GlobalKey(); 17 | 18 | void _resetSettings() { 19 | setState(() { 20 | _settings = null; 21 | _dummy = !_dummy; 22 | }); 23 | } 24 | 25 | bool _dummy = false; 26 | 27 | Future _getData(bool reset) async { 28 | // if the users has not changed, no call to the backend 29 | if (_settings != null) return _settings; 30 | 31 | _settings = const Proxy(); 32 | return _settings; 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return FutureBuilder( 38 | future: _getData(_dummy), 39 | builder: (context, snapshot) { 40 | // Checking if future is resolved 41 | if (snapshot.connectionState == ConnectionState.done) { 42 | // If we got an error 43 | if (snapshot.hasError) { 44 | return Center( 45 | child: Text( 46 | '${snapshot.error} occurred', 47 | style: const TextStyle(fontSize: 18), 48 | ), 49 | ); 50 | 51 | // if we got our data 52 | } else if (snapshot.hasData) { 53 | return Form( 54 | key: _formKey, 55 | child: const Column( 56 | children: [ 57 | /*ElevatedButton( 58 | style: ElevatedButton.styleFrom( 59 | minimumSize: const Size.fromHeight(50), 60 | shape: const ContinuousRectangleBorder(), 61 | ), 62 | onPressed: submit, 63 | child: const Text("Save"), 64 | ),*/ 65 | ], 66 | ), 67 | ); 68 | } 69 | } 70 | return const Center( 71 | child: SizedBox(width: 50, height: 50, child: HelseLoader())); 72 | }); 73 | } 74 | 75 | void submit() async { 76 | try { 77 | if (_formKey.currentState?.validate() ?? false) { 78 | // save the settings 79 | // await AppState.settings?.save(); 80 | 81 | Notify.show("Saved Successfully"); 82 | 83 | _resetSettings(); 84 | } 85 | } catch (ex) { 86 | Notify.show("Error: $ex"); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /.github/workflows/build-android.yml: -------------------------------------------------------------------------------- 1 | name: Build Android 2 | on: 3 | workflow_call: 4 | inputs: 5 | push: 6 | required: true 7 | type: boolean 8 | sign: 9 | required: true 10 | type: boolean 11 | upload: 12 | required: true 13 | type: boolean 14 | mode: 15 | required: false 16 | type: string 17 | default: release 18 | 19 | env: 20 | REGISTRY: ghcr.io 21 | IMAGE_NAME: ${{ github.repository }} 22 | PROPERTIES_PATH: "flutter_frontend/android/key.properties" 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4.1.4 29 | 30 | - uses: actions/setup-java@v3 31 | with: 32 | distribution: 'temurin' 33 | java-version: '17' 34 | 35 | - name: Install Flutter 36 | uses: subosito/flutter-action@v2.14.0 37 | with: 38 | channel: 'beta' 39 | # Enables cache for flutter packages 40 | # Speed up the process 41 | cache: true 42 | 43 | # Get Flutter project dependencies 44 | - name: Get dependencies 45 | run: flutter pub get 46 | working-directory: 'flutter_frontend' 47 | 48 | - name: Create certificat 49 | if: ${{ inputs.sign }} 50 | run: | 51 | echo keyPassword=${{ secrets.KEY_STORE }} > ${{env.PROPERTIES_PATH}} 52 | echo storePassword=${{ secrets.KEY_PASSWORD }} >> ${{env.PROPERTIES_PATH}} 53 | echo keyAlias=${{ vars.KEY_ALIAS }} >> ${{env.PROPERTIES_PATH}} 54 | echo storeFile=key.jks >> ${{env.PROPERTIES_PATH}} 55 | 56 | - name: Decoding base64 key into a file 57 | if: ${{ inputs.sign }} 58 | run: echo ${{ secrets.KEY_FILE }} | base64 --decode > flutter_frontend/android/app/key.jks 59 | 60 | 61 | - name: Build release app bundle 62 | run: flutter build apk --${{inputs.mode}} --split-per-abi 63 | working-directory: 'flutter_frontend' 64 | 65 | - name: Finding binaries files 66 | if: ${{ inputs.push }} 67 | id: finding-files 68 | run: | 69 | { 70 | cd flutter_frontend/build/app/outputs/flutter-apk 71 | echo 'FILELIST<> "$GITHUB_ENV" 75 | 76 | - name: Upload artefact 77 | uses: actions/upload-artifact@v4 78 | with: 79 | path: App/build/app/outputs/flutter-apk/*.apk 80 | 81 | - name: Upload binaries to release 82 | if: ${{ inputs.upload }} 83 | run: | 84 | cd flutter_frontend/build/app/outputs/flutter-apk 85 | for i in $FILELIST; do 86 | gh release upload ${{github.event.release.tag_name}} ${i} 87 | done 88 | env: 89 | GH_TOKEN: ${{ github.token }} 90 | 91 | 92 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/date_range_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/helpers/date.dart'; 3 | import 'package:helse/helpers/translation.dart'; 4 | import 'package:helse/logic/settings/settings_logic.dart'; 5 | import 'package:helse/ui/common/date_range_input.dart'; 6 | 7 | enum DatePreset { 8 | today, 9 | week, 10 | month, 11 | trimestre, 12 | halfYear, 13 | year, 14 | yearToDate, 15 | } 16 | 17 | class DateRangePicker extends StatelessWidget { 18 | final void Function(DateTimeRange value) setDate; 19 | final DateTimeRange initial; 20 | final bool large; 21 | 22 | const DateRangePicker( 23 | this.setDate, 24 | this.initial, 25 | this.large, { 26 | super.key, 27 | }); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Row( 32 | mainAxisAlignment: MainAxisAlignment.center, 33 | crossAxisAlignment: CrossAxisAlignment.center, 34 | children: [ 35 | SizedBox( 36 | child: IconButton(onPressed: _previousPeriod, icon: const Icon(Icons.skip_previous_sharp)), 37 | ), 38 | MenuAnchor( 39 | menuChildren: DatePreset.values.map((v) => MenuItemButton(onPressed: () => _setPreset(v), child: Text(Translation.get(v)))).toList(), 40 | builder: (context, controller, child) => IconButton( 41 | iconSize: large ? 24 : 22, 42 | icon: const Icon(Icons.calendar_month_sharp), 43 | onPressed: () { 44 | if (controller.isOpen) { 45 | controller.close(); 46 | } else { 47 | controller.open(); 48 | } 49 | }, 50 | ), 51 | ), 52 | DateRangeInput(_callBack, initial, large, showIcon: false), 53 | SizedBox( 54 | child: IconButton(onPressed: _nextPeriod, icon: const Icon(Icons.skip_next_sharp)), 55 | ), 56 | ], 57 | ); 58 | } 59 | 60 | void _nextPeriod() { 61 | var duration = _getDurationToMove(); 62 | var start = initial.start.add(duration); 63 | var end = initial.end.add(duration); 64 | _callBack(DateTimeRange(start: start, end: end)); 65 | } 66 | 67 | void _previousPeriod() { 68 | var duration = Duration(seconds: _getDurationToMove().inSeconds * -1); 69 | var start = initial.start.add(duration); 70 | var end = initial.end.add(duration); 71 | _callBack(DateTimeRange(start: start, end: end)); 72 | } 73 | 74 | Duration _getDurationToMove() { 75 | return initial.end.difference(initial.start); 76 | } 77 | 78 | void _callBack(DateTimeRange date) { 79 | setDate(date); 80 | } 81 | 82 | void _setPreset(DatePreset? value) { 83 | if (value == null) return; 84 | 85 | setDate(DateHelper.getRange(value)); 86 | 87 | if (value == DatePreset.today || value == DatePreset.week || value == DatePreset.month) { 88 | SettingsLogic.setDateRange(value); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /flutter_frontend/lib/logic/d_i.dart: -------------------------------------------------------------------------------- 1 | import 'package:health/health.dart'; 2 | import 'package:helse/helpers/oauth.dart'; 3 | import 'package:helse/logic/account/authentication_logic.dart'; 4 | import 'package:helse/logic/fit/task_bloc.dart'; 5 | import 'package:helse/logic/settings/settings_logic.dart'; 6 | import 'package:helse/services/account.dart'; 7 | import 'package:helse/services/event_service.dart'; 8 | import 'package:helse/services/helper_service.dart'; 9 | import 'package:helse/services/metric_service.dart'; 10 | import 'package:helse/services/treatment_service.dart'; 11 | import 'package:helse/services/user_service.dart'; 12 | 13 | import 'fit/fit_logic.dart'; 14 | import 'theme_helper.dart'; 15 | 16 | class DI { 17 | static OauthClient? authService; 18 | static MetricService? _metric; 19 | static MetricService get metric { 20 | var a = _metric; 21 | if (a == null) { 22 | throw Exception("Invalid access"); 23 | } 24 | return a; 25 | } 26 | 27 | static HelperService? _helper; 28 | static HelperService get helper { 29 | var a = _helper; 30 | if (a == null) { 31 | throw Exception("Invalid access"); 32 | } 33 | return a; 34 | } 35 | 36 | static EventService? _event; 37 | static EventService get event { 38 | var a = _event; 39 | if (a == null) { 40 | throw Exception("Invalid access"); 41 | } 42 | return a; 43 | } 44 | 45 | static UserService? _user; 46 | static UserService get user { 47 | var a = _user; 48 | if (a == null) { 49 | throw Exception("Invalid access"); 50 | } 51 | return a; 52 | } 53 | 54 | static TreatmentService? treatement; 55 | static SettingsLogic? _settings; 56 | static SettingsLogic get settings { 57 | var a = _settings; 58 | if (a == null) { 59 | throw Exception("Invalid access"); 60 | } 61 | return a; 62 | } 63 | 64 | static Health health = Health(); 65 | 66 | static AuthenticationLogic? _authentication; 67 | static AuthenticationLogic get authentication { 68 | var a = _authentication; 69 | if (a == null) { 70 | throw Exception("Invalid access"); 71 | } 72 | return a; 73 | } 74 | 75 | static TaskBloc? _fit; 76 | static TaskBloc get fit { 77 | var a = _fit; 78 | if (a == null) { 79 | throw Exception("Invalid access"); 80 | } 81 | return a; 82 | } 83 | 84 | static ThemeHelper theme = ThemeHelper(); 85 | 86 | static void init() { 87 | var account = Account(); 88 | authService = OauthClient(account); 89 | _authentication = AuthenticationLogic(account); 90 | _metric = MetricService(account); 91 | _helper = HelperService(account); 92 | _event = EventService(account); 93 | _user = UserService(account); 94 | treatement = TreatmentService(account); 95 | _settings = SettingsLogic(account); 96 | 97 | var fitLogic = FitLogic(account); 98 | _fit = TaskBloc(fitLogic.sync, const Duration(seconds: 30), FitLogic.isEnabled); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /flutter_frontend/lib/helpers/date.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/ui/common/date_range_picker.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | class DateHelper { 6 | static DateTimeRange now() { 7 | var now = DateTime.now(); 8 | return DateTimeRange(start: DateTime(now.year, now.month, now.day), end: DateTime(now.year, now.month, now.day + 1)); 9 | } 10 | 11 | static String format(DateTime? date, {bool? second, required BuildContext context}) { 12 | if (date == null) return ""; 13 | var tag = Localizations.maybeLocaleOf(context)?.toLanguageTag(); 14 | 15 | DateFormat dateTimeFormat = DateFormat.yMMMMd(tag); 16 | if (second == true) { 17 | dateTimeFormat = dateTimeFormat.add_jms(); 18 | } else { 19 | dateTimeFormat = dateTimeFormat.add_jm(); 20 | } 21 | return dateTimeFormat.format(date); 22 | } 23 | 24 | static String formatMonth(DateTime? date, {required BuildContext context}) { 25 | if (date == null) return ""; 26 | var tag = Localizations.maybeLocaleOf(context)?.toLanguageTag(); 27 | DateFormat dateTimeFormat = DateFormat.yMMM(tag); 28 | return dateTimeFormat.format(date); 29 | } 30 | 31 | static String formatDate(DateTime? date, {required BuildContext context}) { 32 | if (date == null) return ""; 33 | var tag = Localizations.maybeLocaleOf(context)?.toLanguageTag(); 34 | DateFormat dateTimeFormat = DateFormat.yMMMMd(tag); 35 | return dateTimeFormat.format(date); 36 | } 37 | 38 | static String formatTime(DateTime? date, {required BuildContext context}) { 39 | if (date == null) return ""; 40 | var tag = Localizations.maybeLocaleOf(context)?.toLanguageTag(); 41 | 42 | DateFormat dateTimeFormat = DateFormat.jms(tag); 43 | return dateTimeFormat.format(date); 44 | } 45 | 46 | static DateTimeRange getRange(DatePreset value) { 47 | switch (value) { 48 | case DatePreset.today: 49 | return now(); 50 | case DatePreset.week: 51 | return currentWeek(); 52 | case DatePreset.month: 53 | return currentMonths(); 54 | case DatePreset.trimestre: 55 | return currentMonths(count: 3); 56 | case DatePreset.halfYear: 57 | return currentMonths(count: 6); 58 | case DatePreset.year: 59 | return currentWeek(count: 365); 60 | case DatePreset.yearToDate: 61 | return yearToDate(); 62 | } 63 | } 64 | 65 | static DateTimeRange currentWeek({int count = 7}) { 66 | var now = DateTime.now(); 67 | 68 | var end = now; 69 | var start = end.add(Duration(days: -1 * count)); 70 | 71 | return DateTimeRange(start: start, end: end); 72 | } 73 | 74 | static DateTimeRange currentMonths({int count = 1}) { 75 | var now = DateTime.now(); 76 | 77 | var end = now; 78 | var start = end.add(Duration(days: 30 * -1 * count)); 79 | 80 | return DateTimeRange(start: start, end: end); 81 | } 82 | 83 | static DateTimeRange yearToDate() { 84 | var now = DateTime.now(); 85 | 86 | var start = DateTime(now.year, 1, 1); 87 | var end = now; 88 | 89 | return DateTimeRange(start: start, end: end); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /flutter_frontend/linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/metrics/metrics_grid.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:helse/logic/settings/ordered_item.dart'; 5 | 6 | import '../../../helpers/pair.dart'; 7 | import '../../../logic/d_i.dart'; 8 | import '../../../logic/settings/settings_logic.dart'; 9 | import '../../../services/swagger/generated_code/swagger.swagger.dart'; 10 | import '../../common/loader.dart'; 11 | import '../../common/notification.dart'; 12 | import 'metric_widget.dart'; 13 | 14 | class MetricsGrid extends StatefulWidget { 15 | final int? person; 16 | const MetricsGrid({ 17 | super.key, 18 | required this.date, 19 | this.person, 20 | }); 21 | 22 | final DateTimeRange date; 23 | 24 | @override 25 | State createState() => _MetricsGridState(); 26 | } 27 | 28 | class _MetricsGridState extends State { 29 | List>? types; 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _getData(); 34 | } 35 | 36 | void _getData() async { 37 | try { 38 | var model = await DI.metric.metricsType(false); 39 | if (model != null) { 40 | var settings = await SettingsLogic.getMetrics(); 41 | // filter using the user settings 42 | 43 | List> filtered = []; 44 | for (var item in model) { 45 | OrderedItem setting = settings.metrics.firstWhereOrNull((element) => element.id == item.id) ?? _getDefault(item); 46 | 47 | if (setting.visible) filtered.add(Pair(item, setting)); 48 | } 49 | 50 | setState(() { 51 | types = filtered; 52 | }); 53 | SettingsLogic.updateMetrics(model); 54 | } 55 | } catch (ex) { 56 | Notify.showError("$ex"); 57 | } 58 | } 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | var cached = types; 63 | return cached == null 64 | ? const HelseLoader() 65 | : BlocListener( 66 | listener: (context, state) { 67 | _getData(); 68 | }, 69 | bloc: DI.settings.metrics, 70 | child: _getGrid(cached), 71 | ); 72 | } 73 | 74 | OrderedItem _getDefault(MetricType item) { 75 | if (item.type == MetricDataType.number) { 76 | return OrderedItem(item.id ?? 0, item.name ?? '', GraphKind.bar, GraphKind.line); 77 | } 78 | 79 | return OrderedItem(item.id ?? 0, item.name ?? '', GraphKind.event, GraphKind.event); 80 | } 81 | 82 | StatelessWidget _getGrid(List> cached) { 83 | if (cached.isEmpty) { 84 | return const Text("No metrics"); 85 | } else { 86 | return GridView.extent( 87 | shrinkWrap: true, 88 | crossAxisSpacing: 2, 89 | mainAxisSpacing: 2, 90 | physics: const BouncingScrollPhysics(), 91 | maxCrossAxisExtent: 200.0, 92 | children: 93 | cached.map((type) => MetricWidget(type.a, type.b, widget.date, key: Key(type.a.id?.toString() ?? ""), person: widget.person)).toList(), 94 | ); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/blocs/calendar/calendar_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/helpers/date.dart'; 3 | import 'package:helse/services/swagger/generated_code/swagger.swagger.dart'; 4 | import 'package:table_calendar/table_calendar.dart'; 5 | 6 | class CalendarView extends StatefulWidget { 7 | final List metrics; 8 | final DateTimeRange date; 9 | 10 | const CalendarView( 11 | this.metrics, 12 | this.date, { 13 | super.key, 14 | }); 15 | 16 | @override 17 | State createState() => _CalendarViewState(); 18 | } 19 | 20 | class _CalendarViewState extends State { 21 | DateTime _focusedDay = DateTime.now(); 22 | DateTime? _selectedDay; 23 | List _selectedEvents = []; 24 | CalendarFormat _calendarFormat = CalendarFormat.month; 25 | 26 | List _getEventsForDay(DateTime day) { 27 | return widget.metrics.where((x) => x.date != null && x.date!.day == day.day && x.date!.month == day.month && x.date!.year == day.year).toList(); 28 | } 29 | 30 | void _onDaySelected(DateTime selectedDay, DateTime focusedDay) { 31 | if (!isSameDay(_selectedDay, selectedDay)) { 32 | setState(() { 33 | _focusedDay = focusedDay; 34 | _selectedDay = selectedDay; 35 | _selectedEvents = _getEventsForDay(selectedDay); 36 | }); 37 | } 38 | } 39 | 40 | @override 41 | void initState() { 42 | super.initState(); 43 | 44 | var firstDay = widget.metrics.firstOrNull?.date; 45 | if (firstDay != null) { 46 | _onDaySelected(firstDay, firstDay); 47 | } else { 48 | _focusedDay = widget.date.start; 49 | } 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | return Column( 55 | children: [ 56 | Expanded( 57 | child: TableCalendar( 58 | firstDay: widget.date.start, 59 | lastDay: widget.date.end, 60 | focusedDay: _focusedDay, 61 | selectedDayPredicate: (day) { 62 | return isSameDay(_selectedDay, day); 63 | }, 64 | onPageChanged: (focusedDay) { 65 | _focusedDay = focusedDay; 66 | }, 67 | calendarFormat: _calendarFormat, 68 | onFormatChanged: (format) => setState(() { 69 | _calendarFormat = format; 70 | }), 71 | eventLoader: (day) { 72 | return _getEventsForDay(day); 73 | }, 74 | availableGestures: AvailableGestures.all, 75 | calendarStyle: const CalendarStyle( 76 | isTodayHighlighted: true, 77 | //selectedDecoration: BoxDecoration(color: Colors.red), 78 | outsideDaysVisible: false, 79 | ), 80 | rangeSelectionMode: RangeSelectionMode.enforced, 81 | onDaySelected: _onDaySelected, 82 | ), 83 | ), 84 | Text("Showing events of ${DateHelper.formatDate(_selectedDay, context: context)}"), 85 | Flexible( 86 | child: ListView.builder( 87 | itemCount: _selectedEvents.length, 88 | itemBuilder: (x, index) => Row( 89 | children: [ 90 | Text("${_selectedEvents[index].$value} at ${DateHelper.formatTime(_selectedEvents[index].date, context: x)}"), 91 | ], 92 | ), 93 | ), 94 | ) 95 | ], 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /flutter_frontend/lib/ui/common/ordered_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:helse/logic/settings/ordered_item.dart'; 3 | import 'package:helse/ui/common/statefull_check.dart'; 4 | 5 | import 'type_input.dart'; 6 | 7 | class OrderedList extends StatelessWidget { 8 | final List items; 9 | final bool withGraph; 10 | 11 | const OrderedList(this.items, {super.key, this.withGraph = false}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | var theme = Theme.of(context); 16 | return ListView( 17 | shrinkWrap: true, 18 | physics: const BouncingScrollPhysics(), 19 | children: items 20 | .map((item) => Padding( 21 | padding: const EdgeInsets.all(4.0), 22 | child: Column( 23 | children: [ 24 | Row(children: [Text(item.name, style: theme.textTheme.titleLarge)]), 25 | Padding( 26 | padding: const EdgeInsets.all(8.0), 27 | child: Row(children: [ 28 | Text("Visible: ", style: theme.textTheme.bodyLarge), 29 | StatefullCheck(item.visible, (value) => item.visible = value), 30 | ]), 31 | ), 32 | if (withGraph) 33 | Padding( 34 | padding: const EdgeInsets.all(8.0), 35 | child: Row(children: [ 36 | Padding( 37 | padding: const EdgeInsets.only(right: 8.0), 38 | child: Text("Graph type", style: theme.textTheme.bodyLarge), 39 | ), 40 | SizedBox( 41 | width: 160, 42 | height: 45, 43 | child: TypeInput( 44 | value: item.graph, 45 | GraphKind.values.map((x) => DropDownItem(x, x.name)).toList(), 46 | (value) => item.graph = value ?? item.graph, 47 | label: 'Type', 48 | ), 49 | ), 50 | ]), 51 | ), 52 | if (withGraph) 53 | Padding( 54 | padding: const EdgeInsets.all(8.0), 55 | child: Row(children: [ 56 | Padding( 57 | padding: const EdgeInsets.only(right: 8.0), 58 | child: Text("Detail graph type", style: theme.textTheme.bodyLarge), 59 | ), 60 | SizedBox( 61 | width: 160, 62 | height: 45, 63 | child: TypeInput( 64 | value: item.detailGraph, 65 | GraphKind.values.map((x) => DropDownItem(x, x.name)).toList(), 66 | (value) => item.detailGraph = value ?? item.detailGraph, 67 | label: 'Type', 68 | ), 69 | ), 70 | ]), 71 | ), 72 | ], 73 | ), 74 | )) 75 | .toList(), 76 | ); 77 | } 78 | } 79 | --------------------------------------------------------------------------------