diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a14742 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +**/build/ +Android/flutter_app/build/ +/coverage/ +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/Android/flutter_app/.metadata b/Android/flutter_app/.metadata new file mode 100644 index 0000000..b5527e1 --- /dev/null +++ b/Android/flutter_app/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "66dd93f9a27ffe2a9bfc8297506ce066ff51265f" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + - platform: android + create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + - platform: ios + create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + - platform: linux + create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + - platform: macos + create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + - platform: web + create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + - platform: windows + create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/Android/flutter_app/README.md b/Android/flutter_app/README.md new file mode 100644 index 0000000..74c007b --- /dev/null +++ b/Android/flutter_app/README.md @@ -0,0 +1,16 @@ +# s_gizi + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/Android/flutter_app/Untitled b/Android/flutter_app/Untitled new file mode 100644 index 0000000..999d9a5 --- /dev/null +++ b/Android/flutter_app/Untitled @@ -0,0 +1 @@ +font_awesome \ No newline at end of file diff --git a/Android/flutter_app/analysis_options.yaml b/Android/flutter_app/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/Android/flutter_app/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/Android/flutter_app/android/.gitignore b/Android/flutter_app/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/Android/flutter_app/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/Android/flutter_app/android/app/build.gradle.kts b/Android/flutter_app/android/app/build.gradle.kts new file mode 100644 index 0000000..a1649d8 --- /dev/null +++ b/Android/flutter_app/android/app/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.s_gizi" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.s_gizi" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true +} +} + +flutter { + source = "../.." +} +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") +} diff --git a/Android/flutter_app/android/app/src/debug/AndroidManifest.xml b/Android/flutter_app/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Android/flutter_app/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Android/flutter_app/android/app/src/main/AndroidManifest.xml b/Android/flutter_app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0f180fa --- /dev/null +++ b/Android/flutter_app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/flutter_app/android/app/src/main/kotlin/com/example/s_gizi/MainActivity.kt b/Android/flutter_app/android/app/src/main/kotlin/com/example/s_gizi/MainActivity.kt new file mode 100644 index 0000000..deed573 --- /dev/null +++ b/Android/flutter_app/android/app/src/main/kotlin/com/example/s_gizi/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.s_gizi + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/Android/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml b/Android/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/Android/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Android/flutter_app/android/app/src/main/res/drawable/launch_background.xml b/Android/flutter_app/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/Android/flutter_app/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Android/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Android/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/Android/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Android/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Android/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/Android/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/Android/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Android/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/Android/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/Android/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Android/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/Android/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Android/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Android/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/Android/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Android/flutter_app/android/app/src/main/res/values-night/styles.xml b/Android/flutter_app/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/Android/flutter_app/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Android/flutter_app/android/app/src/main/res/values/styles.xml b/Android/flutter_app/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/Android/flutter_app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Android/flutter_app/android/app/src/profile/AndroidManifest.xml b/Android/flutter_app/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Android/flutter_app/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Android/flutter_app/android/build.gradle.kts b/Android/flutter_app/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/Android/flutter_app/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/Android/flutter_app/android/gradle.properties b/Android/flutter_app/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/Android/flutter_app/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/Android/flutter_app/android/gradle/wrapper/gradle-wrapper.properties b/Android/flutter_app/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/Android/flutter_app/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/Android/flutter_app/android/settings.gradle.kts b/Android/flutter_app/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/Android/flutter_app/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/Android/flutter_app/assets/data/lms_who_final.json b/Android/flutter_app/assets/data/lms_who_final.json new file mode 100644 index 0000000..54e77da --- /dev/null +++ b/Android/flutter_app/assets/data/lms_who_final.json @@ -0,0 +1,3662 @@ +{ + "male": { + "wfa": { + "0": { + "L": 0.3487, + "M": 3.3464, + "S": 0.14602 + }, + "1": { + "L": 0.2297, + "M": 4.4709, + "S": 0.13395 + }, + "2": { + "L": 0.197, + "M": 5.5675, + "S": 0.12385 + }, + "3": { + "L": 0.1738, + "M": 6.3762, + "S": 0.11727 + }, + "4": { + "L": 0.1553, + "M": 7.0023, + "S": 0.11316 + }, + "5": { + "L": 0.1395, + "M": 7.5105, + "S": 0.1108 + }, + "6": { + "L": 0.1257, + "M": 7.934, + "S": 0.10958 + }, + "7": { + "L": 0.1134, + "M": 8.297, + "S": 0.10902 + }, + "8": { + "L": 0.1021, + "M": 8.6151, + "S": 0.10882 + }, + "9": { + "L": 0.0917, + "M": 8.9014, + "S": 0.10881 + }, + "10": { + "L": 0.082, + "M": 9.1649, + "S": 0.10891 + }, + "11": { + "L": 0.073, + "M": 9.4122, + "S": 0.10906 + }, + "12": { + "L": 0.0644, + "M": 9.6479, + "S": 0.10925 + }, + "13": { + "L": 0.0563, + "M": 9.8749, + "S": 0.10949 + }, + "14": { + "L": 0.0487, + "M": 10.0953, + "S": 0.10976 + }, + "15": { + "L": 0.0413, + "M": 10.3108, + "S": 0.11007 + }, + "16": { + "L": 0.0343, + "M": 10.5228, + "S": 0.11041 + }, + "17": { + "L": 0.0275, + "M": 10.7319, + "S": 0.11079 + }, + "18": { + "L": 0.0211, + "M": 10.9385, + "S": 0.11119 + }, + "19": { + "L": 0.0148, + "M": 11.143, + "S": 0.11164 + }, + "20": { + "L": 0.0087, + "M": 11.3462, + "S": 0.11211 + }, + "21": { + "L": 0.0029, + "M": 11.5486, + "S": 0.11261 + }, + "22": { + "L": -0.0028, + "M": 11.7504, + "S": 0.11314 + }, + "23": { + "L": -0.0083, + "M": 11.9514, + "S": 0.11369 + }, + "24": { + "L": -0.0137, + "M": 12.1515, + "S": 0.11426 + }, + "25": { + "L": -0.0189, + "M": 12.3502, + "S": 0.11485 + }, + "26": { + "L": -0.024, + "M": 12.5466, + "S": 0.11544 + }, + "27": { + "L": -0.0289, + "M": 12.7401, + "S": 0.11604 + }, + "28": { + "L": -0.0337, + "M": 12.9303, + "S": 0.11664 + }, + "29": { + "L": -0.0385, + "M": 13.1169, + "S": 0.11723 + }, + "30": { + "L": -0.0431, + "M": 13.3, + "S": 0.11781 + }, + "31": { + "L": -0.0476, + "M": 13.4798, + "S": 0.11839 + }, + "32": { + "L": -0.052, + "M": 13.6567, + "S": 0.11896 + }, + "33": { + "L": -0.0564, + "M": 13.8309, + "S": 0.11953 + }, + "34": { + "L": -0.0606, + "M": 14.0031, + "S": 0.12008 + }, + "35": { + "L": -0.0648, + "M": 14.1736, + "S": 0.12062 + }, + "36": { + "L": -0.0689, + "M": 14.3429, + "S": 0.12116 + }, + "37": { + "L": -0.0729, + "M": 14.5113, + "S": 0.12168 + }, + "38": { + "L": -0.0769, + "M": 14.6791, + "S": 0.1222 + }, + "39": { + "L": -0.0808, + "M": 14.8466, + "S": 0.12271 + }, + "40": { + "L": -0.0846, + "M": 15.014, + "S": 0.12322 + }, + "41": { + "L": -0.0883, + "M": 15.1813, + "S": 0.12373 + }, + "42": { + "L": -0.092, + "M": 15.3486, + "S": 0.12425 + }, + "43": { + "L": -0.0957, + "M": 15.5158, + "S": 0.12478 + }, + "44": { + "L": -0.0993, + "M": 15.6828, + "S": 0.12531 + }, + "45": { + "L": -0.1028, + "M": 15.8497, + "S": 0.12586 + }, + "46": { + "L": -0.1063, + "M": 16.0163, + "S": 0.12643 + }, + "47": { + "L": -0.1097, + "M": 16.1827, + "S": 0.127 + }, + "48": { + "L": -0.1131, + "M": 16.3489, + "S": 0.12759 + }, + "49": { + "L": -0.1165, + "M": 16.515, + "S": 0.12819 + }, + "50": { + "L": -0.1198, + "M": 16.6811, + "S": 0.1288 + }, + "51": { + "L": -0.123, + "M": 16.8471, + "S": 0.12943 + }, + "52": { + "L": -0.1262, + "M": 17.0132, + "S": 0.13005 + }, + "53": { + "L": -0.1294, + "M": 17.1792, + "S": 0.13069 + }, + "54": { + "L": -0.1325, + "M": 17.3452, + "S": 0.13133 + }, + "55": { + "L": -0.1356, + "M": 17.5111, + "S": 0.13197 + }, + "56": { + "L": -0.1387, + "M": 17.6768, + "S": 0.13261 + }, + "57": { + "L": -0.1417, + "M": 17.8422, + "S": 0.13325 + }, + "58": { + "L": -0.1447, + "M": 18.0073, + "S": 0.13389 + }, + "59": { + "L": -0.1477, + "M": 18.1722, + "S": 0.13453 + }, + "60": { + "L": -0.1506, + "M": 18.3366, + "S": 0.13517 + } + }, + "hfa": { + "0": { + "L": 1.0, + "M": 49.8842, + "S": 0.03795 + }, + "1": { + "L": 1.0, + "M": 54.7244, + "S": 0.03557 + }, + "2": { + "L": 1.0, + "M": 58.4249, + "S": 0.03424 + }, + "3": { + "L": 1.0, + "M": 61.4292, + "S": 0.03328 + }, + "4": { + "L": 1.0, + "M": 63.886, + "S": 0.03257 + }, + "5": { + "L": 1.0, + "M": 65.9026, + "S": 0.03204 + }, + "6": { + "L": 1.0, + "M": 67.6236, + "S": 0.03165 + }, + "7": { + "L": 1.0, + "M": 69.1645, + "S": 0.03139 + }, + "8": { + "L": 1.0, + "M": 70.5994, + "S": 0.03124 + }, + "9": { + "L": 1.0, + "M": 71.9687, + "S": 0.03117 + }, + "10": { + "L": 1.0, + "M": 73.2812, + "S": 0.03118 + }, + "11": { + "L": 1.0, + "M": 74.5388, + "S": 0.03125 + }, + "12": { + "L": 1.0, + "M": 75.7488, + "S": 0.03137 + }, + "13": { + "L": 1.0, + "M": 76.9186, + "S": 0.03154 + }, + "14": { + "L": 1.0, + "M": 78.0497, + "S": 0.03174 + }, + "15": { + "L": 1.0, + "M": 79.1458, + "S": 0.03197 + }, + "16": { + "L": 1.0, + "M": 80.2113, + "S": 0.03222 + }, + "17": { + "L": 1.0, + "M": 81.2487, + "S": 0.0325 + }, + "18": { + "L": 1.0, + "M": 82.2587, + "S": 0.03279 + }, + "19": { + "L": 1.0, + "M": 83.2418, + "S": 0.0331 + }, + "20": { + "L": 1.0, + "M": 84.1996, + "S": 0.03342 + }, + "21": { + "L": 1.0, + "M": 85.1348, + "S": 0.03376 + }, + "22": { + "L": 1.0, + "M": 86.0477, + "S": 0.0341 + }, + "23": { + "L": 1.0, + "M": 86.941, + "S": 0.03445 + }, + "24": { + "L": 1.0, + "M": 87.1161, + "S": 0.03507 + }, + "25": { + "L": 1.0, + "M": 87.972, + "S": 0.03542 + }, + "26": { + "L": 1.0, + "M": 88.8065, + "S": 0.03576 + }, + "27": { + "L": 1.0, + "M": 89.6197, + "S": 0.0361 + }, + "28": { + "L": 1.0, + "M": 90.412, + "S": 0.03642 + }, + "29": { + "L": 1.0, + "M": 91.1828, + "S": 0.03674 + }, + "30": { + "L": 1.0, + "M": 91.9327, + "S": 0.03704 + }, + "31": { + "L": 1.0, + "M": 92.6631, + "S": 0.03733 + }, + "32": { + "L": 1.0, + "M": 93.3753, + "S": 0.03761 + }, + "33": { + "L": 1.0, + "M": 94.0711, + "S": 0.03787 + }, + "34": { + "L": 1.0, + "M": 94.7532, + "S": 0.03812 + }, + "35": { + "L": 1.0, + "M": 95.4236, + "S": 0.03836 + }, + "36": { + "L": 1.0, + "M": 96.0835, + "S": 0.03858 + }, + "37": { + "L": 1.0, + "M": 96.7337, + "S": 0.03879 + }, + "38": { + "L": 1.0, + "M": 97.3749, + "S": 0.039 + }, + "39": { + "L": 1.0, + "M": 98.0073, + "S": 0.03919 + }, + "40": { + "L": 1.0, + "M": 98.631, + "S": 0.03937 + }, + "41": { + "L": 1.0, + "M": 99.2459, + "S": 0.03954 + }, + "42": { + "L": 1.0, + "M": 99.8515, + "S": 0.03971 + }, + "43": { + "L": 1.0, + "M": 100.4485, + "S": 0.03986 + }, + "44": { + "L": 1.0, + "M": 101.0374, + "S": 0.04002 + }, + "45": { + "L": 1.0, + "M": 101.6186, + "S": 0.04016 + }, + "46": { + "L": 1.0, + "M": 102.1933, + "S": 0.04031 + }, + "47": { + "L": 1.0, + "M": 102.7625, + "S": 0.04045 + }, + "48": { + "L": 1.0, + "M": 103.3273, + "S": 0.04059 + }, + "49": { + "L": 1.0, + "M": 103.8886, + "S": 0.04073 + }, + "50": { + "L": 1.0, + "M": 104.4473, + "S": 0.04086 + }, + "51": { + "L": 1.0, + "M": 105.0041, + "S": 0.041 + }, + "52": { + "L": 1.0, + "M": 105.5596, + "S": 0.04113 + }, + "53": { + "L": 1.0, + "M": 106.1138, + "S": 0.04126 + }, + "54": { + "L": 1.0, + "M": 106.6668, + "S": 0.04139 + }, + "55": { + "L": 1.0, + "M": 107.2188, + "S": 0.04152 + }, + "56": { + "L": 1.0, + "M": 107.7697, + "S": 0.04165 + }, + "57": { + "L": 1.0, + "M": 108.3198, + "S": 0.04177 + }, + "58": { + "L": 1.0, + "M": 108.8689, + "S": 0.0419 + }, + "59": { + "L": 1.0, + "M": 109.417, + "S": 0.04202 + }, + "60": { + "L": 1.0, + "M": 109.9638, + "S": 0.04214 + } + }, + "wfl": { + "45.0": { + "L": -0.3521, + "M": 2.441, + "S": 0.09182 + }, + "45.5": { + "L": -0.3521, + "M": 2.5244, + "S": 0.09153 + }, + "46.0": { + "L": -0.3521, + "M": 2.6077, + "S": 0.09124 + }, + "46.5": { + "L": -0.3521, + "M": 2.6913, + "S": 0.09094 + }, + "47.0": { + "L": -0.3521, + "M": 2.7755, + "S": 0.09065 + }, + "47.5": { + "L": -0.3521, + "M": 2.8609, + "S": 0.09036 + }, + "48.0": { + "L": -0.3521, + "M": 2.948, + "S": 0.09007 + }, + "48.5": { + "L": -0.3521, + "M": 3.0377, + "S": 0.08977 + }, + "49.0": { + "L": -0.3521, + "M": 3.1308, + "S": 0.08948 + }, + "49.5": { + "L": -0.3521, + "M": 3.2276, + "S": 0.08919 + }, + "50.0": { + "L": -0.3521, + "M": 3.3278, + "S": 0.0889 + }, + "50.5": { + "L": -0.3521, + "M": 3.4311, + "S": 0.08861 + }, + "51.0": { + "L": -0.3521, + "M": 3.5376, + "S": 0.08831 + }, + "51.5": { + "L": -0.3521, + "M": 3.6477, + "S": 0.08801 + }, + "52.0": { + "L": -0.3521, + "M": 3.762, + "S": 0.08771 + }, + "52.5": { + "L": -0.3521, + "M": 3.8814, + "S": 0.08741 + }, + "53.0": { + "L": -0.3521, + "M": 4.006, + "S": 0.08711 + }, + "53.5": { + "L": -0.3521, + "M": 4.1354, + "S": 0.08681 + }, + "54.0": { + "L": -0.3521, + "M": 4.2693, + "S": 0.08651 + }, + "54.5": { + "L": -0.3521, + "M": 4.4066, + "S": 0.08621 + }, + "55.0": { + "L": -0.3521, + "M": 4.5467, + "S": 0.08592 + }, + "55.5": { + "L": -0.3521, + "M": 4.6892, + "S": 0.08563 + }, + "56.0": { + "L": -0.3521, + "M": 4.8338, + "S": 0.08535 + }, + "56.5": { + "L": -0.3521, + "M": 4.9796, + "S": 0.08507 + }, + "57.0": { + "L": -0.3521, + "M": 5.1259, + "S": 0.08481 + }, + "57.5": { + "L": -0.3521, + "M": 5.2721, + "S": 0.08455 + }, + "58.0": { + "L": -0.3521, + "M": 5.418, + "S": 0.0843 + }, + "58.5": { + "L": -0.3521, + "M": 5.5632, + "S": 0.08406 + }, + "59.0": { + "L": -0.3521, + "M": 5.7074, + "S": 0.08383 + }, + "59.5": { + "L": -0.3521, + "M": 5.8501, + "S": 0.08362 + }, + "60.0": { + "L": -0.3521, + "M": 5.9907, + "S": 0.08342 + }, + "60.5": { + "L": -0.3521, + "M": 6.1284, + "S": 0.08324 + }, + "61.0": { + "L": -0.3521, + "M": 6.2632, + "S": 0.08308 + }, + "61.5": { + "L": -0.3521, + "M": 6.3954, + "S": 0.08292 + }, + "62.0": { + "L": -0.3521, + "M": 6.5251, + "S": 0.08279 + }, + "62.5": { + "L": -0.3521, + "M": 6.6527, + "S": 0.08266 + }, + "63.0": { + "L": -0.3521, + "M": 6.7786, + "S": 0.08255 + }, + "63.5": { + "L": -0.3521, + "M": 6.9028, + "S": 0.08245 + }, + "64.0": { + "L": -0.3521, + "M": 7.0255, + "S": 0.08236 + }, + "64.5": { + "L": -0.3521, + "M": 7.1467, + "S": 0.08229 + }, + "65.0": { + "L": -0.3521, + "M": 7.2666, + "S": 0.08223 + }, + "65.5": { + "L": -0.3521, + "M": 7.3854, + "S": 0.08218 + }, + "66.0": { + "L": -0.3521, + "M": 7.5034, + "S": 0.08215 + }, + "66.5": { + "L": -0.3521, + "M": 7.6206, + "S": 0.08213 + }, + "67.0": { + "L": -0.3521, + "M": 7.737, + "S": 0.08212 + }, + "67.5": { + "L": -0.3521, + "M": 7.8526, + "S": 0.08212 + }, + "68.0": { + "L": -0.3521, + "M": 7.9674, + "S": 0.08214 + }, + "68.5": { + "L": -0.3521, + "M": 8.0816, + "S": 0.08216 + }, + "69.0": { + "L": -0.3521, + "M": 8.1955, + "S": 0.08219 + }, + "69.5": { + "L": -0.3521, + "M": 8.3092, + "S": 0.08224 + }, + "70.0": { + "L": -0.3521, + "M": 8.4227, + "S": 0.08229 + }, + "70.5": { + "L": -0.3521, + "M": 8.5358, + "S": 0.08235 + }, + "71.0": { + "L": -0.3521, + "M": 8.648, + "S": 0.08241 + }, + "71.5": { + "L": -0.3521, + "M": 8.7594, + "S": 0.08248 + }, + "72.0": { + "L": -0.3521, + "M": 8.8697, + "S": 0.08254 + }, + "72.5": { + "L": -0.3521, + "M": 8.9788, + "S": 0.08262 + }, + "73.0": { + "L": -0.3521, + "M": 9.0865, + "S": 0.08269 + }, + "73.5": { + "L": -0.3521, + "M": 9.1927, + "S": 0.08276 + }, + "74.0": { + "L": -0.3521, + "M": 9.2974, + "S": 0.08283 + }, + "74.5": { + "L": -0.3521, + "M": 9.401, + "S": 0.08289 + }, + "75.0": { + "L": -0.3521, + "M": 9.5032, + "S": 0.08295 + }, + "75.5": { + "L": -0.3521, + "M": 9.6041, + "S": 0.08301 + }, + "76.0": { + "L": -0.3521, + "M": 9.7033, + "S": 0.08307 + }, + "76.5": { + "L": -0.3521, + "M": 9.8007, + "S": 0.08311 + }, + "77.0": { + "L": -0.3521, + "M": 9.8963, + "S": 0.08314 + }, + "77.5": { + "L": -0.3521, + "M": 9.9902, + "S": 0.08317 + }, + "78.0": { + "L": -0.3521, + "M": 10.0827, + "S": 0.08318 + }, + "78.5": { + "L": -0.3521, + "M": 10.1741, + "S": 0.08318 + }, + "79.0": { + "L": -0.3521, + "M": 10.2649, + "S": 0.08316 + }, + "79.5": { + "L": -0.3521, + "M": 10.3558, + "S": 0.08313 + }, + "80.0": { + "L": -0.3521, + "M": 10.4475, + "S": 0.08308 + }, + "80.5": { + "L": -0.3521, + "M": 10.5405, + "S": 0.08301 + }, + "81.0": { + "L": -0.3521, + "M": 10.6352, + "S": 0.08293 + }, + "81.5": { + "L": -0.3521, + "M": 10.7322, + "S": 0.08284 + }, + "82.0": { + "L": -0.3521, + "M": 10.8321, + "S": 0.08273 + }, + "82.5": { + "L": -0.3521, + "M": 10.935, + "S": 0.0826 + }, + "83.0": { + "L": -0.3521, + "M": 11.0415, + "S": 0.08246 + }, + "83.5": { + "L": -0.3521, + "M": 11.1516, + "S": 0.08231 + }, + "84.0": { + "L": -0.3521, + "M": 11.2651, + "S": 0.08215 + }, + "84.5": { + "L": -0.3521, + "M": 11.3817, + "S": 0.08198 + }, + "85.0": { + "L": -0.3521, + "M": 11.5007, + "S": 0.08181 + }, + "85.5": { + "L": -0.3521, + "M": 11.6218, + "S": 0.08163 + }, + "86.0": { + "L": -0.3521, + "M": 11.7444, + "S": 0.08145 + }, + "86.5": { + "L": -0.3521, + "M": 11.8678, + "S": 0.08128 + }, + "87.0": { + "L": -0.3521, + "M": 11.9916, + "S": 0.08111 + }, + "87.5": { + "L": -0.3521, + "M": 12.1152, + "S": 0.08096 + }, + "88.0": { + "L": -0.3521, + "M": 12.2382, + "S": 0.08082 + }, + "88.5": { + "L": -0.3521, + "M": 12.3603, + "S": 0.08069 + }, + "89.0": { + "L": -0.3521, + "M": 12.4815, + "S": 0.08058 + }, + "89.5": { + "L": -0.3521, + "M": 12.6017, + "S": 0.08048 + }, + "90.0": { + "L": -0.3521, + "M": 12.7209, + "S": 0.08041 + }, + "90.5": { + "L": -0.3521, + "M": 12.8392, + "S": 0.08034 + }, + "91.0": { + "L": -0.3521, + "M": 12.9569, + "S": 0.0803 + }, + "91.5": { + "L": -0.3521, + "M": 13.0742, + "S": 0.08026 + }, + "92.0": { + "L": -0.3521, + "M": 13.191, + "S": 0.08025 + }, + "92.5": { + "L": -0.3521, + "M": 13.3075, + "S": 0.08025 + }, + "93.0": { + "L": -0.3521, + "M": 13.4239, + "S": 0.08026 + }, + "93.5": { + "L": -0.3521, + "M": 13.5404, + "S": 0.08029 + }, + "94.0": { + "L": -0.3521, + "M": 13.6572, + "S": 0.08034 + }, + "94.5": { + "L": -0.3521, + "M": 13.7746, + "S": 0.0804 + }, + "95.0": { + "L": -0.3521, + "M": 13.8928, + "S": 0.08047 + }, + "95.5": { + "L": -0.3521, + "M": 14.012, + "S": 0.08056 + }, + "96.0": { + "L": -0.3521, + "M": 14.1325, + "S": 0.08067 + }, + "96.5": { + "L": -0.3521, + "M": 14.2544, + "S": 0.08078 + }, + "97.0": { + "L": -0.3521, + "M": 14.3782, + "S": 0.08092 + }, + "97.5": { + "L": -0.3521, + "M": 14.5038, + "S": 0.08106 + }, + "98.0": { + "L": -0.3521, + "M": 14.6316, + "S": 0.08122 + }, + "98.5": { + "L": -0.3521, + "M": 14.7614, + "S": 0.08139 + }, + "99.0": { + "L": -0.3521, + "M": 14.8934, + "S": 0.08157 + }, + "99.5": { + "L": -0.3521, + "M": 15.0275, + "S": 0.08177 + }, + "100.0": { + "L": -0.3521, + "M": 15.1637, + "S": 0.08198 + }, + "100.5": { + "L": -0.3521, + "M": 15.3018, + "S": 0.0822 + }, + "101.0": { + "L": -0.3521, + "M": 15.4419, + "S": 0.08243 + }, + "101.5": { + "L": -0.3521, + "M": 15.5838, + "S": 0.08267 + }, + "102.0": { + "L": -0.3521, + "M": 15.7276, + "S": 0.08292 + }, + "102.5": { + "L": -0.3521, + "M": 15.8732, + "S": 0.08317 + }, + "103.0": { + "L": -0.3521, + "M": 16.0206, + "S": 0.08343 + }, + "103.5": { + "L": -0.3521, + "M": 16.1697, + "S": 0.0837 + }, + "104.0": { + "L": -0.3521, + "M": 16.3204, + "S": 0.08397 + }, + "104.5": { + "L": -0.3521, + "M": 16.4728, + "S": 0.08425 + }, + "105.0": { + "L": -0.3521, + "M": 16.6268, + "S": 0.08453 + }, + "105.5": { + "L": -0.3521, + "M": 16.7826, + "S": 0.08481 + }, + "106.0": { + "L": -0.3521, + "M": 16.9401, + "S": 0.0851 + }, + "106.5": { + "L": -0.3521, + "M": 17.0995, + "S": 0.08539 + }, + "107.0": { + "L": -0.3521, + "M": 17.2607, + "S": 0.08568 + }, + "107.5": { + "L": -0.3521, + "M": 17.4237, + "S": 0.08599 + }, + "108.0": { + "L": -0.3521, + "M": 17.5885, + "S": 0.08629 + }, + "108.5": { + "L": -0.3521, + "M": 17.7553, + "S": 0.0866 + }, + "109.0": { + "L": -0.3521, + "M": 17.9242, + "S": 0.08691 + }, + "109.5": { + "L": -0.3521, + "M": 18.0954, + "S": 0.08723 + }, + "110.0": { + "L": -0.3521, + "M": 18.2689, + "S": 0.08755 + } + }, + "wfh": { + "65.0": { + "L": -0.3521, + "M": 7.4327, + "S": 0.08217 + }, + "65.5": { + "L": -0.3521, + "M": 7.5504, + "S": 0.08214 + }, + "66.0": { + "L": -0.3521, + "M": 7.6673, + "S": 0.08212 + }, + "66.5": { + "L": -0.3521, + "M": 7.7834, + "S": 0.08212 + }, + "67.0": { + "L": -0.3521, + "M": 7.8986, + "S": 0.08213 + }, + "67.5": { + "L": -0.3521, + "M": 8.0132, + "S": 0.08214 + }, + "68.0": { + "L": -0.3521, + "M": 8.1272, + "S": 0.08217 + }, + "68.5": { + "L": -0.3521, + "M": 8.241, + "S": 0.08221 + }, + "69.0": { + "L": -0.3521, + "M": 8.3547, + "S": 0.08226 + }, + "69.5": { + "L": -0.3521, + "M": 8.468, + "S": 0.08231 + }, + "70.0": { + "L": -0.3521, + "M": 8.5808, + "S": 0.08237 + }, + "70.5": { + "L": -0.3521, + "M": 8.6927, + "S": 0.08243 + }, + "71.0": { + "L": -0.3521, + "M": 8.8036, + "S": 0.0825 + }, + "71.5": { + "L": -0.3521, + "M": 8.9135, + "S": 0.08257 + }, + "72.0": { + "L": -0.3521, + "M": 9.0221, + "S": 0.08264 + }, + "72.5": { + "L": -0.3521, + "M": 9.1292, + "S": 0.08272 + }, + "73.0": { + "L": -0.3521, + "M": 9.2347, + "S": 0.08278 + }, + "73.5": { + "L": -0.3521, + "M": 9.339, + "S": 0.08285 + }, + "74.0": { + "L": -0.3521, + "M": 9.442, + "S": 0.08292 + }, + "74.5": { + "L": -0.3521, + "M": 9.5438, + "S": 0.08298 + }, + "75.0": { + "L": -0.3521, + "M": 9.644, + "S": 0.08303 + }, + "75.5": { + "L": -0.3521, + "M": 9.7425, + "S": 0.08308 + }, + "76.0": { + "L": -0.3521, + "M": 9.8392, + "S": 0.08312 + }, + "76.5": { + "L": -0.3521, + "M": 9.9341, + "S": 0.08315 + }, + "77.0": { + "L": -0.3521, + "M": 10.0274, + "S": 0.08317 + }, + "77.5": { + "L": -0.3521, + "M": 10.1194, + "S": 0.08318 + }, + "78.0": { + "L": -0.3521, + "M": 10.2105, + "S": 0.08317 + }, + "78.5": { + "L": -0.3521, + "M": 10.3012, + "S": 0.08315 + }, + "79.0": { + "L": -0.3521, + "M": 10.3923, + "S": 0.08311 + }, + "79.5": { + "L": -0.3521, + "M": 10.4845, + "S": 0.08305 + }, + "80.0": { + "L": -0.3521, + "M": 10.5781, + "S": 0.08298 + }, + "80.5": { + "L": -0.3521, + "M": 10.6737, + "S": 0.0829 + }, + "81.0": { + "L": -0.3521, + "M": 10.7718, + "S": 0.08279 + }, + "81.5": { + "L": -0.3521, + "M": 10.8728, + "S": 0.08268 + }, + "82.0": { + "L": -0.3521, + "M": 10.9772, + "S": 0.08255 + }, + "82.5": { + "L": -0.3521, + "M": 11.0851, + "S": 0.08241 + }, + "83.0": { + "L": -0.3521, + "M": 11.1966, + "S": 0.08225 + }, + "83.5": { + "L": -0.3521, + "M": 11.3114, + "S": 0.08209 + }, + "84.0": { + "L": -0.3521, + "M": 11.429, + "S": 0.08191 + }, + "84.5": { + "L": -0.3521, + "M": 11.549, + "S": 0.08174 + }, + "85.0": { + "L": -0.3521, + "M": 11.6707, + "S": 0.08156 + }, + "85.5": { + "L": -0.3521, + "M": 11.7937, + "S": 0.08138 + }, + "86.0": { + "L": -0.3521, + "M": 11.9173, + "S": 0.08121 + }, + "86.5": { + "L": -0.3521, + "M": 12.0411, + "S": 0.08105 + }, + "87.0": { + "L": -0.3521, + "M": 12.1645, + "S": 0.0809 + }, + "87.5": { + "L": -0.3521, + "M": 12.2871, + "S": 0.08076 + }, + "88.0": { + "L": -0.3521, + "M": 12.4089, + "S": 0.08064 + }, + "88.5": { + "L": -0.3521, + "M": 12.5298, + "S": 0.08054 + }, + "89.0": { + "L": -0.3521, + "M": 12.6495, + "S": 0.08045 + }, + "89.5": { + "L": -0.3521, + "M": 12.7683, + "S": 0.08038 + }, + "90.0": { + "L": -0.3521, + "M": 12.8864, + "S": 0.08032 + }, + "90.5": { + "L": -0.3521, + "M": 13.0038, + "S": 0.08028 + }, + "91.0": { + "L": -0.3521, + "M": 13.1209, + "S": 0.08025 + }, + "91.5": { + "L": -0.3521, + "M": 13.2376, + "S": 0.08024 + }, + "92.0": { + "L": -0.3521, + "M": 13.3541, + "S": 0.08025 + }, + "92.5": { + "L": -0.3521, + "M": 13.4705, + "S": 0.08027 + }, + "93.0": { + "L": -0.3521, + "M": 13.587, + "S": 0.08031 + }, + "93.5": { + "L": -0.3521, + "M": 13.7041, + "S": 0.08036 + }, + "94.0": { + "L": -0.3521, + "M": 13.8217, + "S": 0.08043 + }, + "94.5": { + "L": -0.3521, + "M": 13.9403, + "S": 0.08051 + }, + "95.0": { + "L": -0.3521, + "M": 14.06, + "S": 0.0806 + }, + "95.5": { + "L": -0.3521, + "M": 14.1811, + "S": 0.08071 + }, + "96.0": { + "L": -0.3521, + "M": 14.3037, + "S": 0.08083 + }, + "96.5": { + "L": -0.3521, + "M": 14.4282, + "S": 0.08097 + }, + "97.0": { + "L": -0.3521, + "M": 14.5547, + "S": 0.08112 + }, + "97.5": { + "L": -0.3521, + "M": 14.6832, + "S": 0.08129 + }, + "98.0": { + "L": -0.3521, + "M": 14.814, + "S": 0.08146 + }, + "98.5": { + "L": -0.3521, + "M": 14.9468, + "S": 0.08165 + }, + "99.0": { + "L": -0.3521, + "M": 15.0818, + "S": 0.08185 + }, + "99.5": { + "L": -0.3521, + "M": 15.2187, + "S": 0.08206 + }, + "100.0": { + "L": -0.3521, + "M": 15.3576, + "S": 0.08229 + }, + "100.5": { + "L": -0.3521, + "M": 15.4985, + "S": 0.08252 + }, + "101.0": { + "L": -0.3521, + "M": 15.6412, + "S": 0.08277 + }, + "101.5": { + "L": -0.3521, + "M": 15.7857, + "S": 0.08302 + }, + "102.0": { + "L": -0.3521, + "M": 15.932, + "S": 0.08328 + }, + "102.5": { + "L": -0.3521, + "M": 16.0801, + "S": 0.08354 + }, + "103.0": { + "L": -0.3521, + "M": 16.2298, + "S": 0.08381 + }, + "103.5": { + "L": -0.3521, + "M": 16.3812, + "S": 0.08408 + }, + "104.0": { + "L": -0.3521, + "M": 16.5342, + "S": 0.08436 + }, + "104.5": { + "L": -0.3521, + "M": 16.6889, + "S": 0.08464 + }, + "105.0": { + "L": -0.3521, + "M": 16.8454, + "S": 0.08493 + }, + "105.5": { + "L": -0.3521, + "M": 17.0036, + "S": 0.08521 + }, + "106.0": { + "L": -0.3521, + "M": 17.1637, + "S": 0.08551 + }, + "106.5": { + "L": -0.3521, + "M": 17.3256, + "S": 0.0858 + }, + "107.0": { + "L": -0.3521, + "M": 17.4894, + "S": 0.08611 + }, + "107.5": { + "L": -0.3521, + "M": 17.655, + "S": 0.08641 + }, + "108.0": { + "L": -0.3521, + "M": 17.8226, + "S": 0.08673 + }, + "108.5": { + "L": -0.3521, + "M": 17.9924, + "S": 0.08704 + }, + "109.0": { + "L": -0.3521, + "M": 18.1645, + "S": 0.08736 + }, + "109.5": { + "L": -0.3521, + "M": 18.339, + "S": 0.08768 + }, + "110.0": { + "L": -0.3521, + "M": 18.5158, + "S": 0.088 + }, + "110.5": { + "L": -0.3521, + "M": 18.6948, + "S": 0.08832 + }, + "111.0": { + "L": -0.3521, + "M": 18.8759, + "S": 0.08864 + }, + "111.5": { + "L": -0.3521, + "M": 19.059, + "S": 0.08896 + }, + "112.0": { + "L": -0.3521, + "M": 19.2439, + "S": 0.08928 + }, + "112.5": { + "L": -0.3521, + "M": 19.4304, + "S": 0.0896 + }, + "113.0": { + "L": -0.3521, + "M": 19.6185, + "S": 0.08991 + }, + "113.5": { + "L": -0.3521, + "M": 19.8081, + "S": 0.09022 + }, + "114.0": { + "L": -0.3521, + "M": 19.999, + "S": 0.09054 + }, + "114.5": { + "L": -0.3521, + "M": 20.1912, + "S": 0.09085 + }, + "115.0": { + "L": -0.3521, + "M": 20.3846, + "S": 0.09116 + }, + "115.5": { + "L": -0.3521, + "M": 20.5789, + "S": 0.09147 + }, + "116.0": { + "L": -0.3521, + "M": 20.7741, + "S": 0.09177 + }, + "116.5": { + "L": -0.3521, + "M": 20.97, + "S": 0.09208 + }, + "117.0": { + "L": -0.3521, + "M": 21.1666, + "S": 0.09239 + }, + "117.5": { + "L": -0.3521, + "M": 21.3636, + "S": 0.0927 + }, + "118.0": { + "L": -0.3521, + "M": 21.5611, + "S": 0.093 + }, + "118.5": { + "L": -0.3521, + "M": 21.7588, + "S": 0.09331 + }, + "119.0": { + "L": -0.3521, + "M": 21.9568, + "S": 0.09362 + }, + "119.5": { + "L": -0.3521, + "M": 22.1549, + "S": 0.09393 + }, + "120.0": { + "L": -0.3521, + "M": 22.353, + "S": 0.09424 + } + } + }, + "female": { + "wfa": { + "0": { + "L": 0.3809, + "M": 3.2322, + "S": 0.14171 + }, + "1": { + "L": 0.1714, + "M": 4.1873, + "S": 0.13724 + }, + "2": { + "L": 0.0962, + "M": 5.1282, + "S": 0.13 + }, + "3": { + "L": 0.0402, + "M": 5.8458, + "S": 0.12619 + }, + "4": { + "L": -0.005, + "M": 6.4237, + "S": 0.12402 + }, + "5": { + "L": -0.043, + "M": 6.8985, + "S": 0.12274 + }, + "6": { + "L": -0.0756, + "M": 7.297, + "S": 0.12204 + }, + "7": { + "L": -0.1039, + "M": 7.6422, + "S": 0.12178 + }, + "8": { + "L": -0.1288, + "M": 7.9487, + "S": 0.12181 + }, + "9": { + "L": -0.1507, + "M": 8.2254, + "S": 0.12199 + }, + "10": { + "L": -0.17, + "M": 8.48, + "S": 0.12223 + }, + "11": { + "L": -0.1872, + "M": 8.7192, + "S": 0.12247 + }, + "12": { + "L": -0.2024, + "M": 8.9481, + "S": 0.12268 + }, + "13": { + "L": -0.2158, + "M": 9.1699, + "S": 0.12283 + }, + "14": { + "L": -0.2278, + "M": 9.387, + "S": 0.12294 + }, + "15": { + "L": -0.2384, + "M": 9.6008, + "S": 0.12299 + }, + "16": { + "L": -0.2478, + "M": 9.8124, + "S": 0.12303 + }, + "17": { + "L": -0.2562, + "M": 10.0226, + "S": 0.12306 + }, + "18": { + "L": -0.2637, + "M": 10.2315, + "S": 0.12309 + }, + "19": { + "L": -0.2703, + "M": 10.4393, + "S": 0.12315 + }, + "20": { + "L": -0.2762, + "M": 10.6464, + "S": 0.12323 + }, + "21": { + "L": -0.2815, + "M": 10.8534, + "S": 0.12335 + }, + "22": { + "L": -0.2862, + "M": 11.0608, + "S": 0.1235 + }, + "23": { + "L": -0.2903, + "M": 11.2688, + "S": 0.12369 + }, + "24": { + "L": -0.2941, + "M": 11.4775, + "S": 0.1239 + }, + "25": { + "L": -0.2975, + "M": 11.6864, + "S": 0.12414 + }, + "26": { + "L": -0.3005, + "M": 11.8947, + "S": 0.12441 + }, + "27": { + "L": -0.3032, + "M": 12.1015, + "S": 0.12472 + }, + "28": { + "L": -0.3057, + "M": 12.3059, + "S": 0.12506 + }, + "29": { + "L": -0.308, + "M": 12.5073, + "S": 0.12545 + }, + "30": { + "L": -0.3101, + "M": 12.7055, + "S": 0.12587 + }, + "31": { + "L": -0.312, + "M": 12.9006, + "S": 0.12633 + }, + "32": { + "L": -0.3138, + "M": 13.093, + "S": 0.12683 + }, + "33": { + "L": -0.3155, + "M": 13.2837, + "S": 0.12737 + }, + "34": { + "L": -0.3171, + "M": 13.4731, + "S": 0.12794 + }, + "35": { + "L": -0.3186, + "M": 13.6618, + "S": 0.12855 + }, + "36": { + "L": -0.3201, + "M": 13.8503, + "S": 0.12919 + }, + "37": { + "L": -0.3216, + "M": 14.0385, + "S": 0.12988 + }, + "38": { + "L": -0.323, + "M": 14.2265, + "S": 0.13059 + }, + "39": { + "L": -0.3243, + "M": 14.414, + "S": 0.13135 + }, + "40": { + "L": -0.3257, + "M": 14.601, + "S": 0.13213 + }, + "41": { + "L": -0.327, + "M": 14.7873, + "S": 0.13293 + }, + "42": { + "L": -0.3283, + "M": 14.9727, + "S": 0.13376 + }, + "43": { + "L": -0.3296, + "M": 15.1573, + "S": 0.1346 + }, + "44": { + "L": -0.3309, + "M": 15.341, + "S": 0.13545 + }, + "45": { + "L": -0.3322, + "M": 15.524, + "S": 0.1363 + }, + "46": { + "L": -0.3335, + "M": 15.7064, + "S": 0.13716 + }, + "47": { + "L": -0.3348, + "M": 15.8882, + "S": 0.138 + }, + "48": { + "L": -0.3361, + "M": 16.0697, + "S": 0.13884 + }, + "49": { + "L": -0.3374, + "M": 16.2511, + "S": 0.13968 + }, + "50": { + "L": -0.3387, + "M": 16.4322, + "S": 0.14051 + }, + "51": { + "L": -0.34, + "M": 16.6133, + "S": 0.14132 + }, + "52": { + "L": -0.3414, + "M": 16.7942, + "S": 0.14213 + }, + "53": { + "L": -0.3427, + "M": 16.9748, + "S": 0.14293 + }, + "54": { + "L": -0.344, + "M": 17.1551, + "S": 0.14371 + }, + "55": { + "L": -0.3453, + "M": 17.3347, + "S": 0.14448 + }, + "56": { + "L": -0.3466, + "M": 17.5136, + "S": 0.14525 + }, + "57": { + "L": -0.3479, + "M": 17.6916, + "S": 0.146 + }, + "58": { + "L": -0.3492, + "M": 17.8686, + "S": 0.14675 + }, + "59": { + "L": -0.3505, + "M": 18.0445, + "S": 0.14748 + }, + "60": { + "L": -0.3518, + "M": 18.2193, + "S": 0.14821 + } + }, + "hfa": { + "0": { + "L": 1.0, + "M": 49.1477, + "S": 0.0379 + }, + "1": { + "L": 1.0, + "M": 53.6872, + "S": 0.0364 + }, + "2": { + "L": 1.0, + "M": 57.0673, + "S": 0.03568 + }, + "3": { + "L": 1.0, + "M": 59.8029, + "S": 0.0352 + }, + "4": { + "L": 1.0, + "M": 62.0899, + "S": 0.03486 + }, + "5": { + "L": 1.0, + "M": 64.0301, + "S": 0.03463 + }, + "6": { + "L": 1.0, + "M": 65.7311, + "S": 0.03448 + }, + "7": { + "L": 1.0, + "M": 67.2873, + "S": 0.03441 + }, + "8": { + "L": 1.0, + "M": 68.7498, + "S": 0.0344 + }, + "9": { + "L": 1.0, + "M": 70.1435, + "S": 0.03444 + }, + "10": { + "L": 1.0, + "M": 71.4818, + "S": 0.03452 + }, + "11": { + "L": 1.0, + "M": 72.771, + "S": 0.03464 + }, + "12": { + "L": 1.0, + "M": 74.015, + "S": 0.03479 + }, + "13": { + "L": 1.0, + "M": 75.2176, + "S": 0.03496 + }, + "14": { + "L": 1.0, + "M": 76.3817, + "S": 0.03514 + }, + "15": { + "L": 1.0, + "M": 77.5099, + "S": 0.03534 + }, + "16": { + "L": 1.0, + "M": 78.6055, + "S": 0.03555 + }, + "17": { + "L": 1.0, + "M": 79.671, + "S": 0.03576 + }, + "18": { + "L": 1.0, + "M": 80.7079, + "S": 0.03598 + }, + "19": { + "L": 1.0, + "M": 81.7182, + "S": 0.0362 + }, + "20": { + "L": 1.0, + "M": 82.7036, + "S": 0.03643 + }, + "21": { + "L": 1.0, + "M": 83.6654, + "S": 0.03666 + }, + "22": { + "L": 1.0, + "M": 84.604, + "S": 0.03688 + }, + "23": { + "L": 1.0, + "M": 85.5202, + "S": 0.03711 + }, + "24": { + "L": 1.0, + "M": 85.7153, + "S": 0.03764 + }, + "25": { + "L": 1.0, + "M": 86.5904, + "S": 0.03786 + }, + "26": { + "L": 1.0, + "M": 87.4462, + "S": 0.03808 + }, + "27": { + "L": 1.0, + "M": 88.283, + "S": 0.0383 + }, + "28": { + "L": 1.0, + "M": 89.1004, + "S": 0.03851 + }, + "29": { + "L": 1.0, + "M": 89.8991, + "S": 0.03872 + }, + "30": { + "L": 1.0, + "M": 90.6797, + "S": 0.03893 + }, + "31": { + "L": 1.0, + "M": 91.443, + "S": 0.03913 + }, + "32": { + "L": 1.0, + "M": 92.1906, + "S": 0.03933 + }, + "33": { + "L": 1.0, + "M": 92.9239, + "S": 0.03952 + }, + "34": { + "L": 1.0, + "M": 93.6444, + "S": 0.03971 + }, + "35": { + "L": 1.0, + "M": 94.3533, + "S": 0.03989 + }, + "36": { + "L": 1.0, + "M": 95.0515, + "S": 0.04006 + }, + "37": { + "L": 1.0, + "M": 95.7399, + "S": 0.04024 + }, + "38": { + "L": 1.0, + "M": 96.4187, + "S": 0.04041 + }, + "39": { + "L": 1.0, + "M": 97.0885, + "S": 0.04057 + }, + "40": { + "L": 1.0, + "M": 97.7493, + "S": 0.04073 + }, + "41": { + "L": 1.0, + "M": 98.4015, + "S": 0.04089 + }, + "42": { + "L": 1.0, + "M": 99.0448, + "S": 0.04105 + }, + "43": { + "L": 1.0, + "M": 99.6795, + "S": 0.0412 + }, + "44": { + "L": 1.0, + "M": 100.3058, + "S": 0.04135 + }, + "45": { + "L": 1.0, + "M": 100.9238, + "S": 0.0415 + }, + "46": { + "L": 1.0, + "M": 101.5337, + "S": 0.04164 + }, + "47": { + "L": 1.0, + "M": 102.136, + "S": 0.04179 + }, + "48": { + "L": 1.0, + "M": 102.7312, + "S": 0.04193 + }, + "49": { + "L": 1.0, + "M": 103.3197, + "S": 0.04206 + }, + "50": { + "L": 1.0, + "M": 103.9021, + "S": 0.0422 + }, + "51": { + "L": 1.0, + "M": 104.4786, + "S": 0.04233 + }, + "52": { + "L": 1.0, + "M": 105.0494, + "S": 0.04246 + }, + "53": { + "L": 1.0, + "M": 105.6148, + "S": 0.04259 + }, + "54": { + "L": 1.0, + "M": 106.1748, + "S": 0.04272 + }, + "55": { + "L": 1.0, + "M": 106.7295, + "S": 0.04285 + }, + "56": { + "L": 1.0, + "M": 107.2788, + "S": 0.04298 + }, + "57": { + "L": 1.0, + "M": 107.8227, + "S": 0.0431 + }, + "58": { + "L": 1.0, + "M": 108.3613, + "S": 0.04322 + }, + "59": { + "L": 1.0, + "M": 108.8948, + "S": 0.04334 + }, + "60": { + "L": 1.0, + "M": 109.4233, + "S": 0.04347 + } + }, + "wfl": { + "45.0": { + "L": -0.3833, + "M": 2.4607, + "S": 0.09029 + }, + "45.5": { + "L": -0.3833, + "M": 2.5457, + "S": 0.09033 + }, + "46.0": { + "L": -0.3833, + "M": 2.6306, + "S": 0.09037 + }, + "46.5": { + "L": -0.3833, + "M": 2.7155, + "S": 0.0904 + }, + "47.0": { + "L": -0.3833, + "M": 2.8007, + "S": 0.09044 + }, + "47.5": { + "L": -0.3833, + "M": 2.8867, + "S": 0.09048 + }, + "48.0": { + "L": -0.3833, + "M": 2.9741, + "S": 0.09052 + }, + "48.5": { + "L": -0.3833, + "M": 3.0636, + "S": 0.09056 + }, + "49.0": { + "L": -0.3833, + "M": 3.156, + "S": 0.0906 + }, + "49.5": { + "L": -0.3833, + "M": 3.252, + "S": 0.09064 + }, + "50.0": { + "L": -0.3833, + "M": 3.3518, + "S": 0.09068 + }, + "50.5": { + "L": -0.3833, + "M": 3.4557, + "S": 0.09072 + }, + "51.0": { + "L": -0.3833, + "M": 3.5636, + "S": 0.09076 + }, + "51.5": { + "L": -0.3833, + "M": 3.6754, + "S": 0.0908 + }, + "52.0": { + "L": -0.3833, + "M": 3.7911, + "S": 0.09085 + }, + "52.5": { + "L": -0.3833, + "M": 3.9105, + "S": 0.09089 + }, + "53.0": { + "L": -0.3833, + "M": 4.0332, + "S": 0.09093 + }, + "53.5": { + "L": -0.3833, + "M": 4.1591, + "S": 0.09098 + }, + "54.0": { + "L": -0.3833, + "M": 4.2875, + "S": 0.09102 + }, + "54.5": { + "L": -0.3833, + "M": 4.4179, + "S": 0.09106 + }, + "55.0": { + "L": -0.3833, + "M": 4.5498, + "S": 0.0911 + }, + "55.5": { + "L": -0.3833, + "M": 4.6827, + "S": 0.09114 + }, + "56.0": { + "L": -0.3833, + "M": 4.8162, + "S": 0.09118 + }, + "56.5": { + "L": -0.3833, + "M": 4.95, + "S": 0.09121 + }, + "57.0": { + "L": -0.3833, + "M": 5.0837, + "S": 0.09125 + }, + "57.5": { + "L": -0.3833, + "M": 5.2173, + "S": 0.09128 + }, + "58.0": { + "L": -0.3833, + "M": 5.3507, + "S": 0.0913 + }, + "58.5": { + "L": -0.3833, + "M": 5.4834, + "S": 0.09132 + }, + "59.0": { + "L": -0.3833, + "M": 5.6151, + "S": 0.09134 + }, + "59.5": { + "L": -0.3833, + "M": 5.7454, + "S": 0.09135 + }, + "60.0": { + "L": -0.3833, + "M": 5.8742, + "S": 0.09136 + }, + "60.5": { + "L": -0.3833, + "M": 6.0014, + "S": 0.09137 + }, + "61.0": { + "L": -0.3833, + "M": 6.127, + "S": 0.09137 + }, + "61.5": { + "L": -0.3833, + "M": 6.2511, + "S": 0.09136 + }, + "62.0": { + "L": -0.3833, + "M": 6.3738, + "S": 0.09135 + }, + "62.5": { + "L": -0.3833, + "M": 6.4948, + "S": 0.09133 + }, + "63.0": { + "L": -0.3833, + "M": 6.6144, + "S": 0.09131 + }, + "63.5": { + "L": -0.3833, + "M": 6.7328, + "S": 0.09129 + }, + "64.0": { + "L": -0.3833, + "M": 6.8501, + "S": 0.09126 + }, + "64.5": { + "L": -0.3833, + "M": 6.9662, + "S": 0.09123 + }, + "65.0": { + "L": -0.3833, + "M": 7.0812, + "S": 0.09119 + }, + "65.5": { + "L": -0.3833, + "M": 7.195, + "S": 0.09115 + }, + "66.0": { + "L": -0.3833, + "M": 7.3076, + "S": 0.0911 + }, + "66.5": { + "L": -0.3833, + "M": 7.4189, + "S": 0.09106 + }, + "67.0": { + "L": -0.3833, + "M": 7.5288, + "S": 0.09101 + }, + "67.5": { + "L": -0.3833, + "M": 7.6375, + "S": 0.09096 + }, + "68.0": { + "L": -0.3833, + "M": 7.7448, + "S": 0.0909 + }, + "68.5": { + "L": -0.3833, + "M": 7.8509, + "S": 0.09085 + }, + "69.0": { + "L": -0.3833, + "M": 7.9559, + "S": 0.09079 + }, + "69.5": { + "L": -0.3833, + "M": 8.0599, + "S": 0.09074 + }, + "70.0": { + "L": -0.3833, + "M": 8.163, + "S": 0.09068 + }, + "70.5": { + "L": -0.3833, + "M": 8.2651, + "S": 0.09062 + }, + "71.0": { + "L": -0.3833, + "M": 8.3666, + "S": 0.09056 + }, + "71.5": { + "L": -0.3833, + "M": 8.4676, + "S": 0.0905 + }, + "72.0": { + "L": -0.3833, + "M": 8.5679, + "S": 0.09043 + }, + "72.5": { + "L": -0.3833, + "M": 8.6674, + "S": 0.09037 + }, + "73.0": { + "L": -0.3833, + "M": 8.7661, + "S": 0.09031 + }, + "73.5": { + "L": -0.3833, + "M": 8.8638, + "S": 0.09025 + }, + "74.0": { + "L": -0.3833, + "M": 8.9601, + "S": 0.09018 + }, + "74.5": { + "L": -0.3833, + "M": 9.0552, + "S": 0.09012 + }, + "75.0": { + "L": -0.3833, + "M": 9.149, + "S": 0.09005 + }, + "75.5": { + "L": -0.3833, + "M": 9.2418, + "S": 0.08999 + }, + "76.0": { + "L": -0.3833, + "M": 9.3337, + "S": 0.08992 + }, + "76.5": { + "L": -0.3833, + "M": 9.4252, + "S": 0.08985 + }, + "77.0": { + "L": -0.3833, + "M": 9.5166, + "S": 0.08979 + }, + "77.5": { + "L": -0.3833, + "M": 9.6086, + "S": 0.08972 + }, + "78.0": { + "L": -0.3833, + "M": 9.7015, + "S": 0.08965 + }, + "78.5": { + "L": -0.3833, + "M": 9.7957, + "S": 0.08959 + }, + "79.0": { + "L": -0.3833, + "M": 9.8915, + "S": 0.08952 + }, + "79.5": { + "L": -0.3833, + "M": 9.9892, + "S": 0.08946 + }, + "80.0": { + "L": -0.3833, + "M": 10.0891, + "S": 0.0894 + }, + "80.5": { + "L": -0.3833, + "M": 10.1916, + "S": 0.08934 + }, + "81.0": { + "L": -0.3833, + "M": 10.2965, + "S": 0.08928 + }, + "81.5": { + "L": -0.3833, + "M": 10.4041, + "S": 0.08923 + }, + "82.0": { + "L": -0.3833, + "M": 10.514, + "S": 0.08918 + }, + "82.5": { + "L": -0.3833, + "M": 10.6263, + "S": 0.08914 + }, + "83.0": { + "L": -0.3833, + "M": 10.741, + "S": 0.0891 + }, + "83.5": { + "L": -0.3833, + "M": 10.8578, + "S": 0.08906 + }, + "84.0": { + "L": -0.3833, + "M": 10.9767, + "S": 0.08903 + }, + "84.5": { + "L": -0.3833, + "M": 11.0974, + "S": 0.089 + }, + "85.0": { + "L": -0.3833, + "M": 11.2198, + "S": 0.08898 + }, + "85.5": { + "L": -0.3833, + "M": 11.3435, + "S": 0.08897 + }, + "86.0": { + "L": -0.3833, + "M": 11.4684, + "S": 0.08895 + }, + "86.5": { + "L": -0.3833, + "M": 11.594, + "S": 0.08895 + }, + "87.0": { + "L": -0.3833, + "M": 11.7201, + "S": 0.08895 + }, + "87.5": { + "L": -0.3833, + "M": 11.8461, + "S": 0.08895 + }, + "88.0": { + "L": -0.3833, + "M": 11.972, + "S": 0.08896 + }, + "88.5": { + "L": -0.3833, + "M": 12.0976, + "S": 0.08898 + }, + "89.0": { + "L": -0.3833, + "M": 12.2229, + "S": 0.089 + }, + "89.5": { + "L": -0.3833, + "M": 12.3477, + "S": 0.08903 + }, + "90.0": { + "L": -0.3833, + "M": 12.4723, + "S": 0.08906 + }, + "90.5": { + "L": -0.3833, + "M": 12.5965, + "S": 0.08909 + }, + "91.0": { + "L": -0.3833, + "M": 12.7205, + "S": 0.08913 + }, + "91.5": { + "L": -0.3833, + "M": 12.8443, + "S": 0.08918 + }, + "92.0": { + "L": -0.3833, + "M": 12.9681, + "S": 0.08923 + }, + "92.5": { + "L": -0.3833, + "M": 13.092, + "S": 0.08928 + }, + "93.0": { + "L": -0.3833, + "M": 13.2158, + "S": 0.08934 + }, + "93.5": { + "L": -0.3833, + "M": 13.3399, + "S": 0.08941 + }, + "94.0": { + "L": -0.3833, + "M": 13.4643, + "S": 0.08948 + }, + "94.5": { + "L": -0.3833, + "M": 13.5892, + "S": 0.08955 + }, + "95.0": { + "L": -0.3833, + "M": 13.7146, + "S": 0.08963 + }, + "95.5": { + "L": -0.3833, + "M": 13.8408, + "S": 0.08972 + }, + "96.0": { + "L": -0.3833, + "M": 13.9676, + "S": 0.08981 + }, + "96.5": { + "L": -0.3833, + "M": 14.0953, + "S": 0.0899 + }, + "97.0": { + "L": -0.3833, + "M": 14.2239, + "S": 0.09 + }, + "97.5": { + "L": -0.3833, + "M": 14.3537, + "S": 0.0901 + }, + "98.0": { + "L": -0.3833, + "M": 14.4848, + "S": 0.09021 + }, + "98.5": { + "L": -0.3833, + "M": 14.6174, + "S": 0.09033 + }, + "99.0": { + "L": -0.3833, + "M": 14.7519, + "S": 0.09044 + }, + "99.5": { + "L": -0.3833, + "M": 14.8882, + "S": 0.09057 + }, + "100.0": { + "L": -0.3833, + "M": 15.0267, + "S": 0.09069 + }, + "100.5": { + "L": -0.3833, + "M": 15.1676, + "S": 0.09083 + }, + "101.0": { + "L": -0.3833, + "M": 15.3108, + "S": 0.09096 + }, + "101.5": { + "L": -0.3833, + "M": 15.4564, + "S": 0.0911 + }, + "102.0": { + "L": -0.3833, + "M": 15.6046, + "S": 0.09125 + }, + "102.5": { + "L": -0.3833, + "M": 15.7553, + "S": 0.09139 + }, + "103.0": { + "L": -0.3833, + "M": 15.9087, + "S": 0.09155 + }, + "103.5": { + "L": -0.3833, + "M": 16.0645, + "S": 0.0917 + }, + "104.0": { + "L": -0.3833, + "M": 16.2229, + "S": 0.09186 + }, + "104.5": { + "L": -0.3833, + "M": 16.3837, + "S": 0.09203 + }, + "105.0": { + "L": -0.3833, + "M": 16.547, + "S": 0.09219 + }, + "105.5": { + "L": -0.3833, + "M": 16.7129, + "S": 0.09236 + }, + "106.0": { + "L": -0.3833, + "M": 16.8814, + "S": 0.09254 + }, + "106.5": { + "L": -0.3833, + "M": 17.0527, + "S": 0.09271 + }, + "107.0": { + "L": -0.3833, + "M": 17.2269, + "S": 0.09289 + }, + "107.5": { + "L": -0.3833, + "M": 17.4039, + "S": 0.09307 + }, + "108.0": { + "L": -0.3833, + "M": 17.5839, + "S": 0.09326 + }, + "108.5": { + "L": -0.3833, + "M": 17.7668, + "S": 0.09344 + }, + "109.0": { + "L": -0.3833, + "M": 17.9526, + "S": 0.09363 + }, + "109.5": { + "L": -0.3833, + "M": 18.1412, + "S": 0.09382 + }, + "110.0": { + "L": -0.3833, + "M": 18.3324, + "S": 0.09401 + } + }, + "wfh": { + "65.0": { + "L": -0.3833, + "M": 7.2402, + "S": 0.09113 + }, + "65.5": { + "L": -0.3833, + "M": 7.3523, + "S": 0.09109 + }, + "66.0": { + "L": -0.3833, + "M": 7.463, + "S": 0.09104 + }, + "66.5": { + "L": -0.3833, + "M": 7.5724, + "S": 0.09099 + }, + "67.0": { + "L": -0.3833, + "M": 7.6806, + "S": 0.09094 + }, + "67.5": { + "L": -0.3833, + "M": 7.7874, + "S": 0.09088 + }, + "68.0": { + "L": -0.3833, + "M": 7.893, + "S": 0.09083 + }, + "68.5": { + "L": -0.3833, + "M": 7.9976, + "S": 0.09077 + }, + "69.0": { + "L": -0.3833, + "M": 8.1012, + "S": 0.09071 + }, + "69.5": { + "L": -0.3833, + "M": 8.2039, + "S": 0.09065 + }, + "70.0": { + "L": -0.3833, + "M": 8.3058, + "S": 0.09059 + }, + "70.5": { + "L": -0.3833, + "M": 8.4071, + "S": 0.09053 + }, + "71.0": { + "L": -0.3833, + "M": 8.5078, + "S": 0.09047 + }, + "71.5": { + "L": -0.3833, + "M": 8.6078, + "S": 0.09041 + }, + "72.0": { + "L": -0.3833, + "M": 8.707, + "S": 0.09035 + }, + "72.5": { + "L": -0.3833, + "M": 8.8053, + "S": 0.09028 + }, + "73.0": { + "L": -0.3833, + "M": 8.9025, + "S": 0.09022 + }, + "73.5": { + "L": -0.3833, + "M": 8.9983, + "S": 0.09016 + }, + "74.0": { + "L": -0.3833, + "M": 9.0928, + "S": 0.09009 + }, + "74.5": { + "L": -0.3833, + "M": 9.1862, + "S": 0.09003 + }, + "75.0": { + "L": -0.3833, + "M": 9.2786, + "S": 0.08996 + }, + "75.5": { + "L": -0.3833, + "M": 9.3703, + "S": 0.08989 + }, + "76.0": { + "L": -0.3833, + "M": 9.4617, + "S": 0.08983 + }, + "76.5": { + "L": -0.3833, + "M": 9.5533, + "S": 0.08976 + }, + "77.0": { + "L": -0.3833, + "M": 9.6456, + "S": 0.08969 + }, + "77.5": { + "L": -0.3833, + "M": 9.739, + "S": 0.08963 + }, + "78.0": { + "L": -0.3833, + "M": 9.8338, + "S": 0.08956 + }, + "78.5": { + "L": -0.3833, + "M": 9.9303, + "S": 0.0895 + }, + "79.0": { + "L": -0.3833, + "M": 10.0289, + "S": 0.08943 + }, + "79.5": { + "L": -0.3833, + "M": 10.1298, + "S": 0.08937 + }, + "80.0": { + "L": -0.3833, + "M": 10.2332, + "S": 0.08932 + }, + "80.5": { + "L": -0.3833, + "M": 10.3393, + "S": 0.08926 + }, + "81.0": { + "L": -0.3833, + "M": 10.4477, + "S": 0.08921 + }, + "81.5": { + "L": -0.3833, + "M": 10.5586, + "S": 0.08916 + }, + "82.0": { + "L": -0.3833, + "M": 10.6719, + "S": 0.08912 + }, + "82.5": { + "L": -0.3833, + "M": 10.7874, + "S": 0.08908 + }, + "83.0": { + "L": -0.3833, + "M": 10.9051, + "S": 0.08905 + }, + "83.5": { + "L": -0.3833, + "M": 11.0248, + "S": 0.08902 + }, + "84.0": { + "L": -0.3833, + "M": 11.1462, + "S": 0.08899 + }, + "84.5": { + "L": -0.3833, + "M": 11.2691, + "S": 0.08897 + }, + "85.0": { + "L": -0.3833, + "M": 11.3934, + "S": 0.08896 + }, + "85.5": { + "L": -0.3833, + "M": 11.5186, + "S": 0.08895 + }, + "86.0": { + "L": -0.3833, + "M": 11.6444, + "S": 0.08895 + }, + "86.5": { + "L": -0.3833, + "M": 11.7705, + "S": 0.08895 + }, + "87.0": { + "L": -0.3833, + "M": 11.8965, + "S": 0.08896 + }, + "87.5": { + "L": -0.3833, + "M": 12.0223, + "S": 0.08897 + }, + "88.0": { + "L": -0.3833, + "M": 12.1478, + "S": 0.08899 + }, + "88.5": { + "L": -0.3833, + "M": 12.2729, + "S": 0.08901 + }, + "89.0": { + "L": -0.3833, + "M": 12.3976, + "S": 0.08904 + }, + "89.5": { + "L": -0.3833, + "M": 12.522, + "S": 0.08907 + }, + "90.0": { + "L": -0.3833, + "M": 12.6461, + "S": 0.08911 + }, + "90.5": { + "L": -0.3833, + "M": 12.77, + "S": 0.08915 + }, + "91.0": { + "L": -0.3833, + "M": 12.8939, + "S": 0.0892 + }, + "91.5": { + "L": -0.3833, + "M": 13.0177, + "S": 0.08925 + }, + "92.0": { + "L": -0.3833, + "M": 13.1415, + "S": 0.08931 + }, + "92.5": { + "L": -0.3833, + "M": 13.2654, + "S": 0.08937 + }, + "93.0": { + "L": -0.3833, + "M": 13.3896, + "S": 0.08944 + }, + "93.5": { + "L": -0.3833, + "M": 13.5142, + "S": 0.08951 + }, + "94.0": { + "L": -0.3833, + "M": 13.6393, + "S": 0.08959 + }, + "94.5": { + "L": -0.3833, + "M": 13.765, + "S": 0.08967 + }, + "95.0": { + "L": -0.3833, + "M": 13.8914, + "S": 0.08975 + }, + "95.5": { + "L": -0.3833, + "M": 14.0186, + "S": 0.08984 + }, + "96.0": { + "L": -0.3833, + "M": 14.1466, + "S": 0.08994 + }, + "96.5": { + "L": -0.3833, + "M": 14.2757, + "S": 0.09004 + }, + "97.0": { + "L": -0.3833, + "M": 14.4059, + "S": 0.09015 + }, + "97.5": { + "L": -0.3833, + "M": 14.5376, + "S": 0.09026 + }, + "98.0": { + "L": -0.3833, + "M": 14.671, + "S": 0.09037 + }, + "98.5": { + "L": -0.3833, + "M": 14.8062, + "S": 0.09049 + }, + "99.0": { + "L": -0.3833, + "M": 14.9434, + "S": 0.09062 + }, + "99.5": { + "L": -0.3833, + "M": 15.0828, + "S": 0.09075 + }, + "100.0": { + "L": -0.3833, + "M": 15.2246, + "S": 0.09088 + }, + "100.5": { + "L": -0.3833, + "M": 15.3687, + "S": 0.09102 + }, + "101.0": { + "L": -0.3833, + "M": 15.5154, + "S": 0.09116 + }, + "101.5": { + "L": -0.3833, + "M": 15.6646, + "S": 0.09131 + }, + "102.0": { + "L": -0.3833, + "M": 15.8164, + "S": 0.09146 + }, + "102.5": { + "L": -0.3833, + "M": 15.9707, + "S": 0.09161 + }, + "103.0": { + "L": -0.3833, + "M": 16.1276, + "S": 0.09177 + }, + "103.5": { + "L": -0.3833, + "M": 16.287, + "S": 0.09193 + }, + "104.0": { + "L": -0.3833, + "M": 16.4488, + "S": 0.09209 + }, + "104.5": { + "L": -0.3833, + "M": 16.6131, + "S": 0.09226 + }, + "105.0": { + "L": -0.3833, + "M": 16.78, + "S": 0.09243 + }, + "105.5": { + "L": -0.3833, + "M": 16.9496, + "S": 0.09261 + }, + "106.0": { + "L": -0.3833, + "M": 17.122, + "S": 0.09278 + }, + "106.5": { + "L": -0.3833, + "M": 17.2973, + "S": 0.09296 + }, + "107.0": { + "L": -0.3833, + "M": 17.4755, + "S": 0.09315 + }, + "107.5": { + "L": -0.3833, + "M": 17.6567, + "S": 0.09333 + }, + "108.0": { + "L": -0.3833, + "M": 17.8407, + "S": 0.09352 + }, + "108.5": { + "L": -0.3833, + "M": 18.0277, + "S": 0.09371 + }, + "109.0": { + "L": -0.3833, + "M": 18.2174, + "S": 0.0939 + }, + "109.5": { + "L": -0.3833, + "M": 18.4096, + "S": 0.09409 + }, + "110.0": { + "L": -0.3833, + "M": 18.6043, + "S": 0.09428 + }, + "110.5": { + "L": -0.3833, + "M": 18.8015, + "S": 0.09448 + }, + "111.0": { + "L": -0.3833, + "M": 19.0009, + "S": 0.09467 + }, + "111.5": { + "L": -0.3833, + "M": 19.2024, + "S": 0.09487 + }, + "112.0": { + "L": -0.3833, + "M": 19.406, + "S": 0.09507 + }, + "112.5": { + "L": -0.3833, + "M": 19.6116, + "S": 0.09527 + }, + "113.0": { + "L": -0.3833, + "M": 19.819, + "S": 0.09546 + }, + "113.5": { + "L": -0.3833, + "M": 20.028, + "S": 0.09566 + }, + "114.0": { + "L": -0.3833, + "M": 20.2385, + "S": 0.09586 + }, + "114.5": { + "L": -0.3833, + "M": 20.4502, + "S": 0.09606 + }, + "115.0": { + "L": -0.3833, + "M": 20.6629, + "S": 0.09626 + }, + "115.5": { + "L": -0.3833, + "M": 20.8766, + "S": 0.09646 + }, + "116.0": { + "L": -0.3833, + "M": 21.0909, + "S": 0.09666 + }, + "116.5": { + "L": -0.3833, + "M": 21.3059, + "S": 0.09686 + }, + "117.0": { + "L": -0.3833, + "M": 21.5213, + "S": 0.09707 + }, + "117.5": { + "L": -0.3833, + "M": 21.737, + "S": 0.09727 + }, + "118.0": { + "L": -0.3833, + "M": 21.9529, + "S": 0.09747 + }, + "118.5": { + "L": -0.3833, + "M": 22.169, + "S": 0.09767 + }, + "119.0": { + "L": -0.3833, + "M": 22.3851, + "S": 0.09788 + }, + "119.5": { + "L": -0.3833, + "M": 22.6012, + "S": 0.09808 + }, + "120.0": { + "L": -0.3833, + "M": 22.8173, + "S": 0.09828 + } + } + } +} \ No newline at end of file diff --git a/Android/flutter_app/assets/image/Logo_SplashScreen.png b/Android/flutter_app/assets/image/Logo_SplashScreen.png new file mode 100644 index 0000000..ebf3149 Binary files /dev/null and b/Android/flutter_app/assets/image/Logo_SplashScreen.png differ diff --git a/Android/flutter_app/assets/image/logo_sgizi.png b/Android/flutter_app/assets/image/logo_sgizi.png new file mode 100644 index 0000000..8db39d6 Binary files /dev/null and b/Android/flutter_app/assets/image/logo_sgizi.png differ diff --git a/Android/flutter_app/assets/image/onboarding_consultation.png b/Android/flutter_app/assets/image/onboarding_consultation.png new file mode 100644 index 0000000..16abcb0 Binary files /dev/null and b/Android/flutter_app/assets/image/onboarding_consultation.png differ diff --git a/Android/flutter_app/assets/image/onboarding_food.png b/Android/flutter_app/assets/image/onboarding_food.png new file mode 100644 index 0000000..a9cb56c Binary files /dev/null and b/Android/flutter_app/assets/image/onboarding_food.png differ diff --git a/Android/flutter_app/assets/image/onboarding_monitoring.png b/Android/flutter_app/assets/image/onboarding_monitoring.png new file mode 100644 index 0000000..b7ce6c1 Binary files /dev/null and b/Android/flutter_app/assets/image/onboarding_monitoring.png differ diff --git a/Android/flutter_app/ios/.gitignore b/Android/flutter_app/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/Android/flutter_app/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/Android/flutter_app/ios/Flutter/AppFrameworkInfo.plist b/Android/flutter_app/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/Android/flutter_app/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/Android/flutter_app/ios/Flutter/Debug.xcconfig b/Android/flutter_app/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/Android/flutter_app/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/Android/flutter_app/ios/Flutter/Release.xcconfig b/Android/flutter_app/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/Android/flutter_app/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/Android/flutter_app/ios/Runner.xcodeproj/project.pbxproj b/Android/flutter_app/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..75dddda --- /dev/null +++ b/Android/flutter_app/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/Android/flutter_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Android/flutter_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Android/flutter_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Android/flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Android/flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Android/flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Android/flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Android/flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/Android/flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/Android/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/Android/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/Android/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/flutter_app/ios/Runner.xcworkspace/contents.xcworkspacedata b/Android/flutter_app/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/Android/flutter_app/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Android/flutter_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Android/flutter_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Android/flutter_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Android/flutter_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Android/flutter_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/Android/flutter_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/Android/flutter_app/ios/Runner/AppDelegate.swift b/Android/flutter_app/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/Android/flutter_app/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/Android/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/Android/flutter_app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/Android/flutter_app/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/Android/flutter_app/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/flutter_app/ios/Runner/Base.lproj/Main.storyboard b/Android/flutter_app/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/Android/flutter_app/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/flutter_app/ios/Runner/Info.plist b/Android/flutter_app/ios/Runner/Info.plist new file mode 100644 index 0000000..e1d2756 --- /dev/null +++ b/Android/flutter_app/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + S Gizi + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + s_gizi + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/Android/flutter_app/ios/Runner/Runner-Bridging-Header.h b/Android/flutter_app/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/Android/flutter_app/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/Android/flutter_app/ios/RunnerTests/RunnerTests.swift b/Android/flutter_app/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/Android/flutter_app/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/Android/flutter_app/lib/app_design.dart b/Android/flutter_app/lib/app_design.dart new file mode 100644 index 0000000..1aa2c65 --- /dev/null +++ b/Android/flutter_app/lib/app_design.dart @@ -0,0 +1,493 @@ +import 'package:flutter/material.dart'; + +const String sgiziLogoAsset = 'assets/image/logo_sgizi.png'; + +class SgColors { + const SgColors._(); + + static const primary = Color(0xFF4B8E96); + static const primaryDark = Color(0xFF2F737A); + static const secondary = Color(0xFFA8D5BA); + static const background = Color(0xFFF5F7F6); + static const surface = Colors.white; + static const textPrimary = Color(0xFF1E1E1E); + static const textSecondary = Color(0xFF6B7280); + static const border = Color(0xFFE0E7E4); + static const danger = Color(0xFFE53935); + static const warning = Color(0xFFF59E0B); + static const success = Color(0xFF34A853); +} + +class AppTypography { + const AppTypography._(); + + static const h1 = TextStyle( + fontSize: 24, + height: 1.2, + fontWeight: FontWeight.w800, + color: SgColors.textPrimary, + ); + static const h2 = TextStyle( + fontSize: 18, + height: 1.3, + fontWeight: FontWeight.w700, + color: SgColors.textPrimary, + ); + static const h3 = TextStyle( + fontSize: 15, + height: 1.35, + fontWeight: FontWeight.w600, + color: SgColors.textPrimary, + ); + static const body = TextStyle( + fontSize: 14, + height: 1.5, + fontWeight: FontWeight.w400, + color: SgColors.textSecondary, + ); + static const caption = TextStyle( + fontSize: 12, + height: 1.35, + fontWeight: FontWeight.w500, + color: SgColors.textSecondary, + ); +} + +class AppLogo extends StatelessWidget { + const AppLogo({super.key, this.size = 56, this.showLabel = false}); + + final double size; + final bool showLabel; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(size * 0.24), + child: Image.asset( + sgiziLogoAsset, + width: size, + height: size, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + width: size, + height: size, + decoration: BoxDecoration( + color: SgColors.primary, + borderRadius: BorderRadius.circular(size * 0.24), + ), + child: const Icon(Icons.bolt, color: Colors.white), + ), + ), + ), + if (showLabel) ...[ + const SizedBox(width: 12), + Text( + 'S-Gizi', + style: AppTypography.h1.copyWith( + color: SgColors.primaryDark, + fontSize: 22, + ), + ), + ], + ], + ); + } +} + +class ChildAvatar extends StatelessWidget { + const ChildAvatar({ + super.key, + required this.name, + required this.gender, + this.photoUrl, + this.radius = 28, + }); + + final String name; + final String gender; + final String? photoUrl; + final double radius; + + @override + Widget build(BuildContext context) { + final imageUrl = photoUrl?.trim(); + if (imageUrl != null && imageUrl.isNotEmpty) { + return CircleAvatar( + radius: radius, + backgroundColor: const Color(0xFFD9EEE7), + backgroundImage: NetworkImage(imageUrl), + ); + } + + final isFemale = gender.toLowerCase().startsWith('p'); + final accent = isFemale ? const Color(0xFFFF8FA3) : SgColors.primary; + + return CircleAvatar( + radius: radius, + backgroundColor: accent.withValues(alpha: 0.12), + child: Icon( + isFemale ? Icons.face_3_rounded : Icons.face_rounded, + color: accent, + size: radius, + ), + ); + } +} + +class HealthCard extends StatelessWidget { + const HealthCard({ + super.key, + required this.child, + this.padding = const EdgeInsets.all(16), + this.margin, + this.color = SgColors.surface, + this.borderColor, + this.onTap, + }); + + final Widget child; + final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? margin; + final Color color; + final Color? borderColor; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final card = AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOut, + margin: margin, + padding: padding, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: borderColor ?? SgColors.border), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: child, + ); + + if (onTap == null) { + return card; + } + + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: onTap, + child: card, + ), + ); + } +} + +class StatusBadge extends StatelessWidget { + const StatusBadge({ + super.key, + required this.text, + this.color = SgColors.success, + }); + + final String text; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.16), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: color.withValues(alpha: 0.36)), + ), + child: Text( + text, + style: AppTypography.caption.copyWith( + color: color, + fontWeight: FontWeight.w800, + ), + ), + ); + } +} + +class PrimaryButton extends StatelessWidget { + const PrimaryButton({ + super.key, + required this.label, + required this.onPressed, + this.icon, + this.isOutlined = false, + }); + + final String label; + final VoidCallback? onPressed; + final IconData? icon; + final bool isOutlined; + + @override + Widget build(BuildContext context) { + final child = Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[Icon(icon, size: 20), const SizedBox(width: 8)], + Flexible( + child: Text( + label, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w800), + ), + ), + ], + ); + + if (isOutlined) { + return SizedBox( + width: double.infinity, + height: 56, + child: OutlinedButton( + onPressed: onPressed, + style: OutlinedButton.styleFrom( + foregroundColor: SgColors.primary, + side: const BorderSide(color: SgColors.primary), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + ), + child: child, + ), + ); + } + + return SizedBox( + width: double.infinity, + height: 56, + child: FilledButton( + onPressed: onPressed, + style: FilledButton.styleFrom( + backgroundColor: SgColors.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + elevation: 1, + shadowColor: SgColors.primary.withValues(alpha: 0.32), + ), + child: child, + ), + ); + } +} + +class EmptyState extends StatelessWidget { + const EmptyState({ + super.key, + required this.title, + required this.message, + this.actionLabel, + this.onAction, + }); + + final String title; + final String message; + final String? actionLabel; + final VoidCallback? onAction; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: HealthCard( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircleAvatar( + radius: 32, + backgroundColor: Color(0xFFE9F6F2), + child: Icon(Icons.inbox_outlined, color: SgColors.primary), + ), + const SizedBox(height: 16), + Text(title, style: AppTypography.h2, textAlign: TextAlign.center), + const SizedBox(height: 8), + Text( + message, + style: AppTypography.body, + textAlign: TextAlign.center, + ), + if (actionLabel != null && onAction != null) ...[ + const SizedBox(height: 16), + PrimaryButton(label: actionLabel!, onPressed: onAction), + ], + ], + ), + ), + ), + ); + } +} + +class ErrorState extends StatelessWidget { + const ErrorState({super.key, required this.message, required this.onRetry}); + + final String message; + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: HealthCard( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircleAvatar( + radius: 32, + backgroundColor: Color(0xFFFFEBEE), + child: Icon(Icons.wifi_off_rounded, color: SgColors.danger), + ), + const SizedBox(height: 16), + const Text('Terjadi Kendala', style: AppTypography.h2), + const SizedBox(height: 8), + Text( + message, + style: AppTypography.body, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + PrimaryButton( + label: 'Coba Lagi', + icon: Icons.refresh_rounded, + onPressed: onRetry, + ), + ], + ), + ), + ), + ); + } +} + +class MetricProgress extends StatelessWidget { + const MetricProgress({ + super.key, + required this.label, + required this.description, + required this.status, + required this.value, + required this.icon, + }); + + final String label; + final String description; + final String status; + final double value; + final IconData icon; + + @override + Widget build(BuildContext context) { + final clampedValue = value.clamp(0.0, 1.0).toDouble(); + + return HealthCard( + margin: const EdgeInsets.only(bottom: 12), + child: Column( + children: [ + Row( + children: [ + CircleAvatar( + radius: 22, + backgroundColor: const Color(0xFFE9F6F2), + child: Icon(icon, color: SgColors.primary, size: 22), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTypography.h3), + const SizedBox(height: 2), + Text(description, style: AppTypography.caption), + ], + ), + ), + Text( + status.toUpperCase(), + textAlign: TextAlign.right, + style: AppTypography.caption.copyWith( + color: SgColors.textPrimary, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + const SizedBox(height: 16), + ClipRRect( + borderRadius: BorderRadius.circular(99), + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: clampedValue), + duration: const Duration(milliseconds: 850), + curve: Curves.easeOutCubic, + builder: (context, animatedValue, _) { + return LinearProgressIndicator( + value: animatedValue, + minHeight: 8, + backgroundColor: const Color(0xFFEAF4F1), + valueColor: const AlwaysStoppedAnimation(SgColors.primary), + ); + }, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Text('Kurang', style: AppTypography.caption), + Text( + 'Ideal', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + color: SgColors.primary, + ), + ), + Text('Berlebih', style: AppTypography.caption), + ], + ), + ], + ), + ); + } +} + +PageRouteBuilder fadeRoute(Widget page) { + return PageRouteBuilder( + transitionDuration: const Duration(milliseconds: 260), + reverseTransitionDuration: const Duration(milliseconds: 220), + pageBuilder: (_, __, ___) => page, + transitionsBuilder: (_, animation, __, child) { + return FadeTransition( + opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), + child: SlideTransition( + position: Tween( + begin: const Offset(0.03, 0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)), + child: child, + ), + ); + }, + ); +} diff --git a/Android/flutter_app/lib/app_state.dart b/Android/flutter_app/lib/app_state.dart new file mode 100644 index 0000000..7cc2cd9 --- /dev/null +++ b/Android/flutter_app/lib/app_state.dart @@ -0,0 +1,74 @@ +import 'package:flutter/foundation.dart'; + +import 'models/mobile_child_model.dart'; + +class SgiziAppState extends ChangeNotifier { + SgiziAppState._(); + + static final SgiziAppState instance = SgiziAppState._(); + + String? authToken; + int? activeChildId; + List children = const []; + Map? profileData; + + bool get isAuthenticated => authToken != null && authToken!.isNotEmpty; + MobileChildModel? get activeChild { + final id = activeChildId; + if (id == null || children.isEmpty) return null; + for (final child in children) { + if (child.id == id) return child; + } + return null; + } + + void setToken(String token) { + authToken = token; + notifyListeners(); + } + + void setChildren(List value) { + children = value; + activeChildId ??= value.isNotEmpty ? value.first.id : null; + if (value.every((child) => child.id != activeChildId)) { + activeChildId = value.isNotEmpty ? value.first.id : null; + } + notifyListeners(); + } + + void setProfileData(Map value) { + profileData = value; + notifyListeners(); + } + + void setActiveChild(int id) { + activeChildId = id; + notifyListeners(); + } + + void updateChildMeasurementSnapshot({ + required int childId, + required String latestStatus, + required String latestMeasurementAt, + }) { + children = [ + for (final child in children) + if (child.id == childId) + child.copyWith( + latestStatus: latestStatus, + latestMeasurementAt: latestMeasurementAt, + ) + else + child, + ]; + notifyListeners(); + } + + void logout() { + authToken = null; + activeChildId = null; + children = const []; + profileData = null; + notifyListeners(); + } +} diff --git a/Android/flutter_app/lib/main.dart b/Android/flutter_app/lib/main.dart new file mode 100644 index 0000000..4336449 --- /dev/null +++ b/Android/flutter_app/lib/main.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import 'app_design.dart'; +import 'services/local_notification_service.dart'; +import 'screens/splash_screen.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await LocalNotificationService.instance.init(); + runApp(const GiziApp()); +} + +class GiziApp extends StatelessWidget { + const GiziApp({super.key}); + + @override + Widget build(BuildContext context) { + const seedColor = Color(0xFF4B8E96); + + return MaterialApp( + title: 'S-Gizi', + debugShowCheckedModeBanner: false, + theme: ThemeData( + useMaterial3: true, + fontFamily: 'Roboto', + colorScheme: ColorScheme.fromSeed( + seedColor: seedColor, + primary: seedColor, + secondary: SgColors.secondary, + surface: SgColors.surface, + ), + scaffoldBackgroundColor: SgColors.background, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + }, + ), + appBarTheme: const AppBarTheme( + centerTitle: false, + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: SgColors.surface, + foregroundColor: SgColors.textPrimary, + titleTextStyle: AppTypography.h2, + ), + textTheme: const TextTheme( + headlineLarge: AppTypography.h1, + titleLarge: AppTypography.h2, + titleMedium: AppTypography.h3, + bodyMedium: AppTypography.body, + bodySmall: AppTypography.caption, + ), + cardTheme: CardThemeData( + elevation: 0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: Color(0xFFE0ECE9)), + ), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight(48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: Color(0xFFD3E4E1)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: Color(0xFFD3E4E1)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: seedColor, width: 1.5), + ), + ), + ), + home: const SplashScreen(), + ); + } +} diff --git a/Android/flutter_app/lib/models/api_result_model.dart b/Android/flutter_app/lib/models/api_result_model.dart new file mode 100644 index 0000000..966b2a6 --- /dev/null +++ b/Android/flutter_app/lib/models/api_result_model.dart @@ -0,0 +1,162 @@ +class ApiResultModel { + const ApiResultModel({ + required this.identitas, + required this.zScore, + required this.kategori, + required this.statusGabungan, + required this.rekomendasi, + this.measurement, + }); + + final IdentitasModel identitas; + final ZScoreModel zScore; + final KategoriModel kategori; + final String statusGabungan; + final List rekomendasi; + final AnalysisMeasurementModel? measurement; + + factory ApiResultModel.fromJson(Map json) { + return ApiResultModel( + identitas: IdentitasModel.fromJson( + json['identitas'] as Map? ?? const {}, + ), + zScore: ZScoreModel.fromJson( + json['zscore'] as Map? ?? const {}, + ), + kategori: KategoriModel.fromJson( + json['kategori'] as Map? ?? const {}, + ), + statusGabungan: json['status_gabungan'] as String? ?? '-', + rekomendasi: (json['rekomendasi'] as List? ?? const []) + .whereType>() + .map(RekomendasiModel.fromJson) + .toList(), + measurement: json['measurement'] is Map + ? AnalysisMeasurementModel.fromJson( + json['measurement'] as Map, + ) + : null, + ); + } +} + +class AnalysisMeasurementModel { + const AnalysisMeasurementModel({ + required this.id, + required this.childId, + required this.childName, + required this.tanggalUkur, + required this.caraUkur, + }); + + final int id; + final int childId; + final String childName; + final String tanggalUkur; + final String? caraUkur; + + factory AnalysisMeasurementModel.fromJson(Map json) { + return AnalysisMeasurementModel( + id: (json['id'] as num?)?.toInt() ?? 0, + childId: (json['child_id'] as num?)?.toInt() ?? 0, + childName: json['child_name'] as String? ?? '-', + tanggalUkur: json['tanggal_ukur'] as String? ?? '-', + caraUkur: json['cara_ukur'] as String?, + ); + } +} + +class IdentitasModel { + const IdentitasModel({required this.umurBulan, required this.jenisKelamin}); + + final double umurBulan; + final String jenisKelamin; + + factory IdentitasModel.fromJson(Map json) { + double parseNum(dynamic v) { + if (v is num) return v.toDouble(); + return double.nan; + } + + return IdentitasModel( + umurBulan: parseNum(json['umur_bulan']), + jenisKelamin: json['jenis_kelamin'] as String? ?? '-', + ); + } +} + +class ZScoreModel { + const ZScoreModel({required this.bbu, required this.tbu, required this.bbtb}); + + final double bbu; + final double tbu; + final double bbtb; + + factory ZScoreModel.fromJson(Map json) { + double parseNum(dynamic v) { + if (v is num) return v.toDouble(); + return double.nan; + } + + return ZScoreModel( + bbu: parseNum(json['bbu']), + tbu: parseNum(json['tbu']), + bbtb: parseNum(json['bbtb']), + ); + } +} + +class KategoriModel { + const KategoriModel({ + required this.bbu, + required this.tbu, + required this.bbtb, + }); + + final String bbu; + final String tbu; + final String bbtb; + + factory KategoriModel.fromJson(Map json) { + return KategoriModel( + bbu: json['bbu'] as String? ?? '-', + tbu: json['tbu'] as String? ?? '-', + bbtb: json['bbtb'] as String? ?? '-', + ); + } +} + +class RekomendasiModel { + const RekomendasiModel({ + required this.menu, + required this.kalori, + required this.protein, + required this.lemak, + required this.karbohidrat, + required this.alasan, + }); + + final String menu; + final int kalori; + final int protein; + final int lemak; + final int karbohidrat; + final String alasan; + + factory RekomendasiModel.fromJson(Map json) { + int parseInt(dynamic v) { + if (v is int) return v; + if (v is num) return v.toInt(); + return 0; + } + + return RekomendasiModel( + menu: json['menu'] as String? ?? '-', + kalori: parseInt(json['kalori']), + protein: parseInt(json['protein']), + lemak: parseInt(json['lemak']), + karbohidrat: parseInt(json['karbohidrat']), + alasan: json['alasan'] as String? ?? '-', + ); + } +} diff --git a/Android/flutter_app/lib/models/child_model.dart b/Android/flutter_app/lib/models/child_model.dart new file mode 100644 index 0000000..925faac --- /dev/null +++ b/Android/flutter_app/lib/models/child_model.dart @@ -0,0 +1,72 @@ +class ChildModel { + const ChildModel({ + required this.ageInMonths, + required this.weightKg, + required this.heightCm, + required this.gender, + required this.measurementPosition, + }); + + final double ageInMonths; + final double weightKg; + final double heightCm; + final String gender; + final String measurementPosition; + + // ============================= + // NORMALIZATION + // ============================= + String get normalizedGender { + final g = gender.toLowerCase().trim(); + + if (g == 'l' || g == 'laki' || g == 'laki-laki' || g == 'male') { + return 'male'; + } + + if (g == 'p' || g == 'perempuan' || g == 'female') { + return 'female'; + } + + throw Exception('Gender tidak dikenali: $gender'); + } + + String get normalizedPosition { + final p = measurementPosition.toLowerCase().trim(); + + if (p.contains('berdiri') || p.contains('standing')) { + return 'standing'; + } + + if (p.contains('terlentang') || + p.contains('lying') || + p.contains('recumbent')) { + return 'lying'; + } + + throw Exception('Posisi tidak dikenali: $measurementPosition'); + } + + // ============================= + // WHO RULE + // ============================= + bool get usesLengthReference => ageInMonths < 24; + + bool get needsStandingToRecumbentAdjustment => + usesLengthReference && normalizedPosition == 'standing'; + + double get adjustedHeightCm { + if (needsStandingToRecumbentAdjustment) { + return heightCm + 0.7; + } + return heightCm; + } + + // ============================= + // INDICATORS + // ============================= + String get weightForAgeIndicator => 'wfa'; + + String get heightForAgeIndicator => 'hfa'; + + String get weightForHeightIndicator => usesLengthReference ? 'wfl' : 'wfh'; +} diff --git a/Android/flutter_app/lib/models/mobile_child_model.dart b/Android/flutter_app/lib/models/mobile_child_model.dart new file mode 100644 index 0000000..edf90f9 --- /dev/null +++ b/Android/flutter_app/lib/models/mobile_child_model.dart @@ -0,0 +1,54 @@ +class MobileChildModel { + const MobileChildModel({ + required this.id, + required this.nama, + required this.tanggalLahir, + required this.jenisKelamin, + this.photoUrl, + this.latestStatus, + this.latestMeasurementAt, + }); + + final int id; + final String nama; + final String tanggalLahir; + final String jenisKelamin; + final String? photoUrl; + final String? latestStatus; + final String? latestMeasurementAt; + + factory MobileChildModel.fromJson(Map json) { + return MobileChildModel( + id: (json['id'] as num?)?.toInt() ?? 0, + nama: json['nama'] as String? ?? '-', + tanggalLahir: json['tanggal_lahir'] as String? ?? '-', + jenisKelamin: json['jenis_kelamin'] as String? ?? '-', + photoUrl: + json['photo_url'] as String? ?? + json['foto_url'] as String? ?? + json['avatar_url'] as String?, + latestStatus: json['latest_status'] as String?, + latestMeasurementAt: json['latest_measurement_at'] as String?, + ); + } + + MobileChildModel copyWith({ + int? id, + String? nama, + String? tanggalLahir, + String? jenisKelamin, + String? photoUrl, + String? latestStatus, + String? latestMeasurementAt, + }) { + return MobileChildModel( + id: id ?? this.id, + nama: nama ?? this.nama, + tanggalLahir: tanggalLahir ?? this.tanggalLahir, + jenisKelamin: jenisKelamin ?? this.jenisKelamin, + photoUrl: photoUrl ?? this.photoUrl, + latestStatus: latestStatus ?? this.latestStatus, + latestMeasurementAt: latestMeasurementAt ?? this.latestMeasurementAt, + ); + } +} diff --git a/Android/flutter_app/lib/models/news_article_model.dart b/Android/flutter_app/lib/models/news_article_model.dart new file mode 100644 index 0000000..987ba54 --- /dev/null +++ b/Android/flutter_app/lib/models/news_article_model.dart @@ -0,0 +1,49 @@ +class NewsArticleModel { + const NewsArticleModel({ + required this.id, + required this.title, + required this.description, + required this.content, + required this.category, + required this.createdAt, + this.sourceName, + this.image, + this.url, + }); + + final int id; + final String title; + final String description; + final String content; + final String category; + final String createdAt; + final String? sourceName; + final String? image; + final String? url; + + factory NewsArticleModel.fromJson(Map json) { + return NewsArticleModel( + id: (json['id'] as num?)?.toInt() ?? 0, + title: json['title'] as String? ?? '-', + description: + (json['description'] as String? ?? json['excerpt'] as String? ?? '-') + .trim(), + content: (json['content'] as String? ?? '').trim(), + category: (json['category'] as String? ?? 'Nutrisi Anak').trim(), + createdAt: + (json['created_at'] as String? ?? json['published_at'] as String? ?? '') + .trim(), + sourceName: (json['source_name'] as String? ?? '').trim().isEmpty + ? null + : json['source_name'] as String?, + image: (json['image'] as String? ?? json['image_url'] as String? ?? '') + .trim() + .isEmpty + ? null + : (json['image'] as String? ?? json['image_url'] as String?), + url: (json['url'] as String? ?? '').trim().isEmpty + ? null + : json['url'] as String?, + ); + } +} diff --git a/Android/flutter_app/lib/models/recommendation_response_model.dart b/Android/flutter_app/lib/models/recommendation_response_model.dart new file mode 100644 index 0000000..678986c --- /dev/null +++ b/Android/flutter_app/lib/models/recommendation_response_model.dart @@ -0,0 +1,72 @@ +import 'api_result_model.dart'; + +class RecommendationResponseModel { + const RecommendationResponseModel({ + required this.requestedStatus, + required this.resolvedStatus, + required this.primaryCategory, + required this.matchedCategories, + required this.items, + this.measurement, + }); + + final String requestedStatus; + final String resolvedStatus; + final String primaryCategory; + final List matchedCategories; + final List items; + final RecommendationMeasurementModel? measurement; + + factory RecommendationResponseModel.fromJson(Map json) { + final normalized = + json['normalized_status'] as Map? ?? const {}; + final rawCategories = normalized['matched_categories']; + + return RecommendationResponseModel( + requestedStatus: json['requested_status'] as String? ?? '-', + resolvedStatus: + json['resolved_status'] as String? ?? + json['status'] as String? ?? + '-', + primaryCategory: normalized['primary_category'] as String? ?? 'Normal', + matchedCategories: rawCategories is List + ? rawCategories.whereType().toList() + : const ['Normal'], + items: (json['data'] as List? ?? const []) + .whereType>() + .map(RekomendasiModel.fromJson) + .toList(), + measurement: json['measurement'] is Map + ? RecommendationMeasurementModel.fromJson( + json['measurement'] as Map, + ) + : null, + ); + } +} + +class RecommendationMeasurementModel { + const RecommendationMeasurementModel({ + required this.id, + required this.childId, + required this.childName, + required this.tanggalUkur, + required this.umurBulan, + }); + + final int id; + final int childId; + final String childName; + final String? tanggalUkur; + final double? umurBulan; + + factory RecommendationMeasurementModel.fromJson(Map json) { + return RecommendationMeasurementModel( + id: (json['id'] as num?)?.toInt() ?? 0, + childId: (json['child_id'] as num?)?.toInt() ?? 0, + childName: json['child_name'] as String? ?? '-', + tanggalUkur: json['tanggal_ukur'] as String?, + umurBulan: (json['umur_bulan'] as num?)?.toDouble(), + ); + } +} diff --git a/Android/flutter_app/lib/models/riwayat_response_model.dart b/Android/flutter_app/lib/models/riwayat_response_model.dart new file mode 100644 index 0000000..ed01863 --- /dev/null +++ b/Android/flutter_app/lib/models/riwayat_response_model.dart @@ -0,0 +1,120 @@ +class RiwayatResponseModel { + const RiwayatResponseModel({required this.child, required this.riwayat}); + + final ChildInfoModel child; + final List riwayat; + + factory RiwayatResponseModel.fromJson(Map json) { + final rawList = json['riwayat']; + final items = rawList is List + ? rawList + .whereType>() + .map(RiwayatItemModel.fromJson) + .toList() + : []; + + return RiwayatResponseModel( + child: ChildInfoModel.fromJson( + json['child'] as Map? ?? const {}, + ), + riwayat: items, + ); + } +} + +class ChildInfoModel { + const ChildInfoModel({ + required this.id, + required this.nama, + required this.tanggalLahir, + required this.jenisKelamin, + this.photoUrl, + }); + + final int id; + final String nama; + final String tanggalLahir; + final String jenisKelamin; + final String? photoUrl; + + factory ChildInfoModel.fromJson(Map json) { + return ChildInfoModel( + id: (json['id'] as num?)?.toInt() ?? 0, + nama: json['nama'] as String? ?? '-', + tanggalLahir: json['tanggal_lahir'] as String? ?? '-', + jenisKelamin: json['jenis_kelamin'] as String? ?? '-', + photoUrl: + json['photo_url'] as String? ?? + json['foto_url'] as String? ?? + json['avatar_url'] as String?, + ); + } +} + +class RiwayatKategoriModel { + const RiwayatKategoriModel({ + required this.bbu, + required this.tbu, + required this.bbtb, + }); + + final String? bbu; + final String? tbu; + final String? bbtb; + + factory RiwayatKategoriModel.fromJson(Map json) { + return RiwayatKategoriModel( + bbu: json['bbu'] as String?, + tbu: json['tbu'] as String?, + bbtb: json['bbtb'] as String?, + ); + } +} + +class RiwayatItemModel { + const RiwayatItemModel({ + required this.id, + required this.berat, + required this.tinggi, + required this.tanggalUkur, + required this.caraUkur, + required this.umurBulan, + required this.statusGabungan, + required this.kategori, + required this.zBbu, + required this.zTbu, + required this.zBbtb, + }); + + final int id; + final double berat; + final double tinggi; + final String tanggalUkur; + final String? caraUkur; + final double umurBulan; + final String statusGabungan; + final RiwayatKategoriModel kategori; + final double? zBbu; + final double? zTbu; + final double? zBbtb; + + factory RiwayatItemModel.fromJson(Map json) { + final z = json['z_score'] as Map? ?? const {}; + final kategori = json['kategori'] as Map? ?? const {}; + double? parseNullableNum(dynamic v) => v is num ? v.toDouble() : null; + + return RiwayatItemModel( + id: (json['id'] as num?)?.toInt() ?? 0, + berat: (json['berat'] as num?)?.toDouble() ?? double.nan, + tinggi: (json['tinggi'] as num?)?.toDouble() ?? double.nan, + tanggalUkur: json['tanggal_ukur'] as String? ?? '-', + caraUkur: json['cara_ukur'] as String?, + umurBulan: (json['umur_bulan'] as num?)?.toDouble() ?? double.nan, + statusGabungan: json['status_gabungan'] as String? ?? '-', + kategori: RiwayatKategoriModel.fromJson(kategori), + zBbu: parseNullableNum(z['bbu']), + zTbu: parseNullableNum(z['tbu']), + zBbtb: parseNullableNum(z['bbtb']), + ); + } +} diff --git a/Android/flutter_app/lib/screens/account_info_screen.dart b/Android/flutter_app/lib/screens/account_info_screen.dart new file mode 100644 index 0000000..27531c1 --- /dev/null +++ b/Android/flutter_app/lib/screens/account_info_screen.dart @@ -0,0 +1,292 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import '../services/api_service.dart'; +import 'edit_profile_screen.dart'; + +class AccountInfoScreen extends StatefulWidget { + const AccountInfoScreen({super.key}); + + @override + State createState() => _AccountInfoScreenState(); +} + +class _AccountInfoScreenState extends State { + final _api = ApiService(); + final _state = SgiziAppState.instance; + late Future> _future; + Map? _profileCache; + + @override + void initState() { + super.initState(); + _future = _api.getProfile().then((value) { + _state.setProfileData(value); + return value; + }); + } + + void _retry() => setState(() { + _future = _api.getProfile().then((value) { + _state.setProfileData(value); + return value; + }); + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7F6), + appBar: AppBar( + title: const Text('Informasi Akun'), + backgroundColor: const Color(0xFFF5F7F6), + ), + body: SafeArea( + child: FutureBuilder>( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const _AccountSkeleton(); + } + if (snapshot.hasError) { + return ErrorState( + message: 'Informasi akun gagal dimuat.', + onRetry: _retry, + ); + } + final profile = _profileCache ?? snapshot.data ?? const {}; + final name = (profile['name'] as String? ?? '-').trim(); + final phone = (profile['phone'] as String? ?? '-').trim(); + final email = (profile['email'] as String? ?? '-').trim(); + final joinedAt = (profile['joined_at'] as String? ?? '-').trim(); + + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 14, 20, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HealthCard( + child: Row( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 78, + height: 78, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFFEAF7F7), + border: Border.all(color: const Color(0xFF0B7A86), width: 1.8), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: ClipOval( + child: Image.asset( + 'assets/image/onboarding_consultation.png', + fit: BoxFit.cover, + ), + ), + ), + ), + Positioned( + right: 2, + bottom: 2, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: const Color(0xFF34C759), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ).animate(onPlay: (c) => c.repeat(reverse: true)).moveY( + begin: 0, + end: -2, + duration: 1800.ms, + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: AppTypography.h2, maxLines: 1, overflow: TextOverflow.ellipsis), + const SizedBox(height: 3), + Text(phone, style: AppTypography.body, maxLines: 1, overflow: TextOverflow.ellipsis), + const SizedBox(height: 6), + const StatusBadge(text: 'Akun Terverifikasi', color: Color(0xFF0B7A86)), + ], + ), + ), + const SizedBox(width: 10), + IconButton( + onPressed: () async { + final updated = await Navigator.of(context).push>( + fadeRoute( + EditProfileScreen( + initialName: name, + initialPhone: phone, + initialEmail: email == '-' ? '' : email, + ), + ), + ); + if (updated != null && mounted) { + setState(() { + _profileCache = updated; + _future = Future.value(updated); + }); + _state.setProfileData(updated); + } + }, + icon: const Icon(LucideIcons.pencil, color: Color(0xFF0B7A86)), + ), + ], + ), + ), + const SizedBox(height: 16), + Text( + 'Data Akun', + style: AppTypography.h2.copyWith(color: SgColors.textPrimary), + ), + const SizedBox(height: 10), + HealthCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + _InfoRow( + icon: LucideIcons.user, + title: 'Nama Lengkap', + value: name, + ), + const Divider(height: 1, color: SgColors.border), + _InfoRow( + icon: LucideIcons.messageCircle, + title: 'Nomor Telepon', + value: phone, + ), + const Divider(height: 1, color: SgColors.border), + _InfoRow( + icon: LucideIcons.info, + title: 'Email', + value: email, + ), + const Divider(height: 1, color: SgColors.border), + _InfoRow( + icon: LucideIcons.calendarDays, + title: 'Tanggal Bergabung', + value: joinedAt, + ), + ], + ), + ), + const SizedBox(height: 16), + PrimaryButton( + label: 'Edit Profile', + icon: LucideIcons.arrowRight, + onPressed: () async { + final updated = await Navigator.of(context).push>( + fadeRoute( + EditProfileScreen( + initialName: name, + initialPhone: phone, + initialEmail: email == '-' ? '' : email, + ), + ), + ); + if (updated != null && mounted) { + setState(() { + _profileCache = updated; + _future = Future.value(updated); + }); + _state.setProfileData(updated); + } + }, + ), + ], + ), + ).animate().fadeIn(duration: 220.ms).slideY(begin: 0.02, end: 0); + }, + ), + ), + ); + } +} + +class _InfoRow extends StatelessWidget { + const _InfoRow({ + required this.icon, + required this.title, + required this.value, + }); + + final IconData icon; + final String title; + final String value; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFF7FD6C2).withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(14), + ), + child: Icon(icon, color: const Color(0xFF0B7A86), size: 18), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTypography.caption), + const SizedBox(height: 2), + Text( + value, + style: AppTypography.h3, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _AccountSkeleton extends StatelessWidget { + const _AccountSkeleton(); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: const Color(0xFFE8EEEC), + highlightColor: const Color(0xFFF7FAF9), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 14, 20, 24), + child: Column( + children: [ + Container(height: 120, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20))), + const SizedBox(height: 16), + Container(height: 230, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20))), + ], + ), + ), + ); + } +} + diff --git a/Android/flutter_app/lib/screens/add_child_screen.dart b/Android/flutter_app/lib/screens/add_child_screen.dart new file mode 100644 index 0000000..b4713ef --- /dev/null +++ b/Android/flutter_app/lib/screens/add_child_screen.dart @@ -0,0 +1,703 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import '../models/mobile_child_model.dart'; +import '../services/api_service.dart'; +import '../utils/nutrition_display_utils.dart'; +import 'app_shell.dart'; + +class AddChildScreen extends StatefulWidget { + const AddChildScreen({ + super.key, + bool isFirstSetup = false, + @Deprecated('Gunakan isFirstSetup') bool? isMandatory, + }) : isFirstSetup = isMandatory ?? isFirstSetup; + + final bool isFirstSetup; + + @override + State createState() => _AddChildScreenState(); +} + +class _AddChildScreenState extends State + with SingleTickerProviderStateMixin { + final _api = ApiService(); + final _nameController = TextEditingController(); + final _dateController = TextEditingController(); + final _appState = SgiziAppState.instance; + late final AnimationController _shakeController; + + DateTime? _birthDate; + String? _gender; + bool _loading = false; + bool _showValidation = false; + String? _error; + int? _selectedExistingChildId; + + @override + void initState() { + super.initState(); + _selectedExistingChildId = _appState.activeChildId; + _shakeController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 450), + ); + } + + @override + void dispose() { + _nameController.dispose(); + _dateController.dispose(); + _shakeController.dispose(); + super.dispose(); + } + + bool get _isValid { + return _nameController.text.trim().isNotEmpty && + _birthDate != null && + _gender != null; + } + + Future _save() async { + FocusScope.of(context).unfocus(); + if (!_isValid) { + setState(() { + _showValidation = true; + _error = 'Nama lengkap, jenis kelamin, dan tanggal lahir wajib diisi.'; + }); + _shakeController.forward(from: 0); + return; + } + + setState(() { + _loading = true; + _error = null; + }); + + try { + final child = await _api.createChild({ + 'nama': _nameController.text.trim(), + 'tanggal_lahir': _toApiDate(_birthDate!), + 'jenis_kelamin': _gender, + }); + + final state = SgiziAppState.instance; + state.setChildren([...state.children, child]); + state.setActiveChild(child.id); + if (!mounted) return; + Navigator.of(context).pushReplacement(fadeRoute(const AppShell())); + } catch (error) { + setState(() => _error = error.toString()); + _shakeController.forward(from: 0); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + Future _pickDate() async { + final now = DateTime.now(); + final picked = await showDatePicker( + context: context, + initialDate: _birthDate ?? DateTime(now.year - 3, now.month, now.day), + firstDate: DateTime(now.year - 18, now.month, now.day), + lastDate: now, + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: Theme.of(context).colorScheme.copyWith( + primary: const Color(0xFF0B7A86), + ), + ), + child: child!, + ); + }, + ); + if (picked == null) return; + + setState(() { + _birthDate = picked; + _dateController.text = _formatIndonesiaDate(picked); + _showValidation = false; + _error = null; + }); + } + + @override + Widget build(BuildContext context) { + final children = _appState.children; + return PopScope( + canPop: !widget.isFirstSetup, + child: Scaffold( + backgroundColor: const Color(0xFFF5F7F6), + appBar: AppBar( + automaticallyImplyLeading: !widget.isFirstSetup, + backgroundColor: const Color(0xFFF5F7F6), + title: Text(widget.isFirstSetup ? 'Data Anak' : 'Tambah Anak'), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.more_vert_rounded), + ), + ], + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(18, 10, 18, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'ANAK TERDAFTAR', + style: AppTypography.caption.copyWith( + letterSpacing: 1, + color: SgColors.textSecondary, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 12), + _ChildSelectorRow( + children: children, + selectedId: _selectedExistingChildId, + onSelect: (id) { + setState(() => _selectedExistingChildId = id); + _appState.setActiveChild(id); + }, + ).animate().fadeIn(duration: 300.ms).slideY(begin: 0.1), + const SizedBox(height: 18), + AnimatedBuilder( + animation: _shakeController, + builder: (context, child) { + final t = _shakeController.value; + final dx = math.sin(t * math.pi * 4) * (1 - t) * 8; + return Transform.translate( + offset: Offset(dx, 0), + child: child, + ); + }, + child: HealthCard( + padding: const EdgeInsets.fromLTRB(18, 20, 18, 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Detail Data Anak', style: AppTypography.h2), + const SizedBox(height: 4), + const Text( + 'Lengkapi informasi untuk analisis gizi tepat.', + style: AppTypography.body, + ), + const SizedBox(height: 18), + Center( + child: Column( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 110, + height: 110, + decoration: BoxDecoration( + color: const Color(0xFFEFF9F8), + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFF0B7A86), + width: 2.2, + ), + ), + child: ClipOval( + child: Padding( + padding: const EdgeInsets.all(18), + child: Image.asset( + 'assets/image/logo_sgizi.png', + fit: BoxFit.contain, + ), + ), + ), + ), + Positioned( + right: -2, + bottom: -2, + child: GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Upload foto akan tersedia pada versi berikutnya.', + ), + ), + ); + }, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFF0B7A86), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.12), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + LucideIcons.camera, + color: Colors.white, + size: 16, + ), + ), + ), + ), + ], + ).animate(onPlay: (c) => c.repeat(reverse: true)).scale( + begin: const Offset(0.98, 0.98), + end: const Offset(1.02, 1.02), + duration: 2.seconds, + ), + const SizedBox(height: 10), + Text( + 'Unggah Foto Anak', + style: AppTypography.h3.copyWith( + color: const Color(0xFF0B7A86), + ), + ), + ], + ), + ), + const SizedBox(height: 18), + _FieldLabel('NAMA LENGKAP'), + const SizedBox(height: 8), + TextField( + controller: _nameController, + textInputAction: TextInputAction.next, + onChanged: (_) { + if (_showValidation) setState(() {}); + }, + decoration: _inputDecoration( + hint: 'Contoh: Arkan Syahputra', + icon: PhosphorIconsRegular.user, + showError: + _showValidation && _nameController.text.trim().isEmpty, + ), + ), + const SizedBox(height: 16), + _FieldLabel('JENIS KELAMIN'), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _GenderOption( + label: 'Laki-laki', + active: _gender == 'L', + onTap: () => setState(() => _gender = 'L'), + ), + ), + const SizedBox(width: 10), + Expanded( + child: _GenderOption( + label: 'Perempuan', + active: _gender == 'P', + onTap: () => setState(() => _gender = 'P'), + ), + ), + ], + ), + if (_showValidation && _gender == null) + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + 'Pilih jenis kelamin.', + style: AppTypography.caption.copyWith( + color: SgColors.danger, + ), + ), + ), + const SizedBox(height: 16), + _FieldLabel('TANGGAL LAHIR'), + const SizedBox(height: 8), + TextField( + controller: _dateController, + readOnly: true, + onTap: _pickDate, + decoration: _inputDecoration( + hint: 'Pilih tanggal lahir', + icon: LucideIcons.calendarDays, + showError: _showValidation && _birthDate == null, + ), + ), + const SizedBox(height: 10), + if (_birthDate != null) + Text( + 'Umur: ${formatAgeFromBirthDate(_toApiDate(_birthDate!))}', + style: AppTypography.caption.copyWith( + color: const Color(0xFF0B7A86), + fontWeight: FontWeight.w700, + ), + ), + if (_error != null) ...[ + const SizedBox(height: 10), + Text( + _error!, + style: AppTypography.caption.copyWith( + color: SgColors.danger, + fontWeight: FontWeight.w700, + ), + ), + ], + const SizedBox(height: 18), + _SaveButton( + loading: _loading, + onTap: _loading ? null : _save, + ), + ], + ), + ).animate().fadeIn(delay: 100.ms, duration: 320.ms).slideY( + begin: 0.1, + end: 0, + ), + ), + const SizedBox(height: 14), + HealthCard( + color: const Color(0xFFEFFAF4), + borderColor: const Color(0xFFD5EFD8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 18, + backgroundColor: const Color(0xFFDFF5E6), + child: Icon( + PhosphorIconsFill.shieldCheck, + size: 18, + color: const Color(0xFF28A66D), + ), + ), + const SizedBox(width: 10), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Informasi Keamanan', style: AppTypography.h3), + SizedBox(height: 4), + Text( + 'Data anak Anda tersimpan aman dan hanya digunakan untuk analisis standar WHO.', + style: AppTypography.body, + ), + ], + ), + ), + ], + ), + ).animate().fadeIn(delay: 170.ms, duration: 320.ms), + ], + ), + ), + ), + ), + ); + } + + InputDecoration _inputDecoration({ + required String hint, + required IconData icon, + required bool showError, + }) { + return InputDecoration( + hintText: hint, + prefixIcon: Icon(icon), + filled: true, + fillColor: const Color(0xFFF4F7F6), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: BorderSide( + color: showError ? SgColors.danger : const Color(0xFFE3E9E7), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: BorderSide( + color: showError ? SgColors.danger : const Color(0xFFE3E9E7), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(18), + borderSide: const BorderSide(color: Color(0xFF0B7A86), width: 1.5), + ), + ); + } +} + +class _FieldLabel extends StatelessWidget { + const _FieldLabel(this.text); + final String text; + + @override + Widget build(BuildContext context) { + return Text( + text, + style: AppTypography.caption.copyWith( + letterSpacing: 0.8, + fontWeight: FontWeight.w800, + color: SgColors.textPrimary, + ), + ); + } +} + +class _GenderOption extends StatelessWidget { + const _GenderOption({ + required this.label, + required this.active, + required this.onTap, + }); + + final String label; + final bool active; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(999), + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 44, + alignment: Alignment.center, + decoration: BoxDecoration( + color: active ? const Color(0xFF0B7A86).withValues(alpha: 0.10) : Colors.white, + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: active ? const Color(0xFF0B7A86) : const Color(0xFFE0E7E4), + ), + ), + child: Text( + label, + style: AppTypography.h3.copyWith( + color: active ? const Color(0xFF0B7A86) : SgColors.textSecondary, + ), + ), + ), + ); + } +} + +class _SaveButton extends StatefulWidget { + const _SaveButton({required this.loading, required this.onTap}); + final bool loading; + final VoidCallback? onTap; + + @override + State<_SaveButton> createState() => _SaveButtonState(); +} + +class _SaveButtonState extends State<_SaveButton> { + bool _pressed = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (_) => setState(() => _pressed = true), + onTapCancel: () => setState(() => _pressed = false), + onTapUp: (_) => setState(() => _pressed = false), + child: AnimatedScale( + scale: _pressed ? 0.98 : 1, + duration: const Duration(milliseconds: 160), + child: Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF0B7A86), Color(0xFF1597A4)], + ), + borderRadius: BorderRadius.circular(999), + boxShadow: [ + BoxShadow( + color: const Color(0xFF0B7A86).withValues(alpha: 0.28), + blurRadius: 18, + offset: const Offset(0, 10), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(999), + onTap: widget.onTap, + child: Center( + child: widget.loading + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2.2, + color: Colors.white, + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Simpan & Lanjutkan', + style: AppTypography.h2.copyWith(color: Colors.white), + ), + const SizedBox(width: 10), + const Icon(LucideIcons.arrowRight, color: Colors.white), + ], + ), + ), + ), + ), + ), + ), + ).animate(onPlay: (c) => c.repeat(reverse: true)).moveY( + begin: 0, + end: -2, + duration: 1800.ms, + ); + } +} + +class _ChildSelectorRow extends StatelessWidget { + const _ChildSelectorRow({ + required this.children, + required this.selectedId, + required this.onSelect, + }); + + final List children; + final int? selectedId; + final ValueChanged onSelect; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 112, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: children.length + 1, + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemBuilder: (context, index) { + if (index == children.length) { + return Container( + width: 88, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: const Color(0xFFE2EAE7)), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(LucideIcons.plus, color: Color(0xFF0B7A86)), + SizedBox(height: 6), + Text('Tambah', style: AppTypography.caption), + ], + ), + ); + } + + final child = children[index]; + final active = child.id == selectedId; + return InkWell( + borderRadius: BorderRadius.circular(18), + onTap: () => onSelect(child.id), + child: AnimatedContainer( + duration: const Duration(milliseconds: 220), + width: 92, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: active ? const Color(0xFF0B7A86) : const Color(0xFFE2EAE7), + width: active ? 1.8 : 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: active ? 0.08 : 0.04), + blurRadius: 12, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + children: [ + Stack( + children: [ + ChildAvatar( + name: child.nama, + gender: child.jenisKelamin, + photoUrl: child.photoUrl, + radius: 22, + ), + if (active) + Positioned( + right: 0, + top: 0, + child: Container( + width: 14, + height: 14, + decoration: const BoxDecoration( + color: Color(0xFF0B7A86), + shape: BoxShape.circle, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + child.nama, + style: AppTypography.caption.copyWith( + color: SgColors.textPrimary, + fontWeight: FontWeight.w700, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + formatAgeFromBirthDate(child.tanggalLahir), + style: AppTypography.caption, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + }, + ), + ); + } +} + +String _toApiDate(DateTime value) { + return '${value.year.toString().padLeft(4, '0')}-${value.month.toString().padLeft(2, '0')}-${value.day.toString().padLeft(2, '0')}'; +} + +String _formatIndonesiaDate(DateTime value) { + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des', + ]; + return '${value.day.toString().padLeft(2, '0')} ${months[value.month - 1]} ${value.year}'; +} diff --git a/Android/flutter_app/lib/screens/app_shell.dart b/Android/flutter_app/lib/screens/app_shell.dart new file mode 100644 index 0000000..bf87527 --- /dev/null +++ b/Android/flutter_app/lib/screens/app_shell.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../app_design.dart'; +import 'consultation_chat_screen.dart'; +import 'home_screen.dart'; +import 'nutrition_screen.dart'; +import 'profile_screen.dart'; + +class AppShell extends StatefulWidget { + const AppShell({super.key}); + + @override + State createState() => _AppShellState(); +} + +class _AppShellState extends State { + int _index = 0; + + late final List _screens = [ + HomeScreen(onChangeTab: _setTab), + const NutritionScreen(), + const ConsultationChatScreen(showAppBar: false), + const ProfileScreen(), + ]; + + void _setTab(int index) => setState(() => _index = index); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: AnimatedSwitcher( + duration: const Duration(milliseconds: 220), + switchInCurve: Curves.easeOut, + child: KeyedSubtree(key: ValueKey(_index), child: _screens[_index]), + ), + bottomNavigationBar: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 20, + offset: const Offset(0, -6), + ), + ], + ), + padding: const EdgeInsets.fromLTRB(14, 8, 14, 10), + child: SafeArea( + top: false, + child: Row( + children: [ + _NavItem( + label: 'Home', + active: _index == 0, + icon: PhosphorIconsRegular.house, + onTap: () => _setTab(0), + ), + _NavItem( + label: 'Nutrisi', + active: _index == 1, + icon: LucideIcons.apple, + onTap: () => _setTab(1), + ), + _NavItem( + label: 'Konsultasi', + active: _index == 2, + icon: PhosphorIconsRegular.chatCircleDots, + onTap: () => _setTab(2), + ), + _NavItem( + label: 'Profil', + active: _index == 3, + icon: PhosphorIconsRegular.user, + onTap: () => _setTab(3), + ), + ], + ), + ), + ), + ); + } +} + +class _NavItem extends StatelessWidget { + const _NavItem({ + required this.label, + required this.active, + required this.icon, + required this.onTap, + }); + + final String label; + final bool active; + final IconData icon; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Expanded( + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(14), + child: AnimatedContainer( + duration: const Duration(milliseconds: 220), + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: active ? const Color(0xFFEAF8F7) : Colors.transparent, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedScale( + scale: active ? 1.08 : 1, + duration: const Duration(milliseconds: 200), + child: Icon( + icon, + color: active ? const Color(0xFF0B7A86) : const Color(0xFF8B959C), + ), + ), + const SizedBox(height: 3), + Text( + label, + style: AppTypography.caption.copyWith( + color: active ? const Color(0xFF0B7A86) : const Color(0xFF8B959C), + fontWeight: active ? FontWeight.w800 : FontWeight.w600, + ), + ), + const SizedBox(height: 2), + AnimatedContainer( + duration: const Duration(milliseconds: 220), + height: 3, + width: active ? 20 : 0, + decoration: BoxDecoration( + color: const Color(0xFF0B7A86), + borderRadius: BorderRadius.circular(99), + ), + ), + ], + ), + ).animate(target: active ? 1 : 0).scale( + begin: const Offset(0.98, 0.98), + end: const Offset(1, 1), + duration: 220.ms, + ), + ), + ); + } +} diff --git a/Android/flutter_app/lib/screens/article_detail_screen.dart b/Android/flutter_app/lib/screens/article_detail_screen.dart new file mode 100644 index 0000000..5a36c83 --- /dev/null +++ b/Android/flutter_app/lib/screens/article_detail_screen.dart @@ -0,0 +1,141 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../app_design.dart'; +import '../models/news_article_model.dart'; + +class ArticleDetailScreen extends StatelessWidget { + const ArticleDetailScreen({ + super.key, + required this.article, + required this.related, + }); + + final NewsArticleModel article; + final List related; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SgColors.background, + appBar: AppBar(title: const Text('Detail Artikel')), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: 'article-${article.id}', + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: _ArticleImage(imageUrl: article.image, fallbackIndex: article.id), + ), + ), + const SizedBox(height: 14), + StatusBadge(text: article.category, color: SgColors.primary), + const SizedBox(height: 10), + Text(article.title, style: AppTypography.h2), + const SizedBox(height: 6), + Text( + [ + if (article.sourceName != null && article.sourceName!.isNotEmpty) + article.sourceName, + if (article.createdAt.isNotEmpty) article.createdAt, + ].whereType().join(' • '), + style: AppTypography.caption, + ), + const SizedBox(height: 14), + Text(article.description, style: AppTypography.body), + const SizedBox(height: 12), + Text( + article.content.trim().isEmpty ? article.description : article.content, + style: AppTypography.body.copyWith(color: SgColors.textPrimary), + ), + if (article.url != null && article.url!.isNotEmpty) ...[ + const SizedBox(height: 16), + PrimaryButton( + label: 'Buka Sumber (Selengkapnya)', + icon: Icons.open_in_new_rounded, + onPressed: () async { + final uri = Uri.tryParse(article.url!); + if (uri == null) return; + await launchUrl(uri, mode: LaunchMode.externalApplication); + }, + ), + ], + const SizedBox(height: 20), + Text('Artikel Terkait', style: AppTypography.h3), + const SizedBox(height: 10), + ...related + .where((item) => item.id != article.id) + .take(3) + .map( + (item) => HealthCard( + margin: const EdgeInsets.only(bottom: 10), + onTap: () => Navigator.of(context).push( + fadeRoute(ArticleDetailScreen(article: item, related: related)), + ), + child: Row( + children: [ + SizedBox( + width: 64, + height: 64, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: _ArticleImage( + imageUrl: item.image, + fallbackIndex: item.id, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + item.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _ArticleImage extends StatelessWidget { + const _ArticleImage({required this.imageUrl, required this.fallbackIndex}); + + final String? imageUrl; + final int fallbackIndex; + + @override + Widget build(BuildContext context) { + final fallback = _assetByIndex(fallbackIndex); + if (imageUrl == null || imageUrl!.isEmpty) { + return Image.asset(fallback, fit: BoxFit.cover); + } + return CachedNetworkImage( + imageUrl: imageUrl!, + fit: BoxFit.cover, + errorWidget: (_, __, ___) => Image.asset(fallback, fit: BoxFit.cover), + placeholder: (_, __) => Container(color: const Color(0xFFEAF1EF)), + ); + } +} + +String _assetByIndex(int index) { + const images = [ + 'assets/image/onboarding_food.png', + 'assets/image/onboarding_monitoring.png', + 'assets/image/onboarding_consultation.png', + ]; + return images[index % images.length]; +} diff --git a/Android/flutter_app/lib/screens/articles_screen.dart b/Android/flutter_app/lib/screens/articles_screen.dart new file mode 100644 index 0000000..77021a2 --- /dev/null +++ b/Android/flutter_app/lib/screens/articles_screen.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../models/news_article_model.dart'; +import 'article_detail_screen.dart'; + +class ArticlesScreen extends StatefulWidget { + const ArticlesScreen({ + super.key, + required this.title, + required this.articles, + this.initialCategory = 'Semua', + }); + + final String title; + final List articles; + final String initialCategory; + + @override + State createState() => _ArticlesScreenState(); +} + +class _ArticlesScreenState extends State { + late String _activeCategory = widget.initialCategory; + + @override + Widget build(BuildContext context) { + final filtered = widget.articles + .where( + (a) => + _activeCategory == 'Semua' || + a.category.toLowerCase().contains(_activeCategory.toLowerCase()), + ) + .toList(); + + return Scaffold( + backgroundColor: SgColors.background, + appBar: AppBar( + title: Text(widget.title), + backgroundColor: SgColors.background, + ), + body: SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 40, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (_, index) { + final label = _nutritionCategories[index]; + final isActive = label == _activeCategory; + return InkWell( + borderRadius: BorderRadius.circular(999), + onTap: () => setState(() => _activeCategory = label), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, + ), + decoration: BoxDecoration( + color: isActive ? const Color(0xFF0B7A86) : Colors.white, + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: + isActive ? const Color(0xFF0B7A86) : SgColors.border, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Center( + child: Text( + label, + style: AppTypography.caption.copyWith( + color: isActive ? Colors.white : SgColors.textPrimary, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ); + }, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemCount: _nutritionCategories.length, + ), + ), + const SizedBox(height: 16), + if (filtered.isEmpty) + const EmptyState( + title: 'Belum Ada Artikel', + message: 'Tidak ada artikel untuk kategori ini.', + ) + else + ...filtered.map( + (article) => HealthCard( + margin: const EdgeInsets.only(bottom: 12), + padding: EdgeInsets.zero, + onTap: () { + Navigator.of(context).push( + fadeRoute( + ArticleDetailScreen( + article: article, + related: widget.articles, + ), + ), + ); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: 'article-${article.id}', + child: ClipRRect( + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(20), + ), + child: SizedBox( + width: 104, + height: 96, + child: Image.asset( + _articleAssetByIndex(article.id), + fit: BoxFit.cover, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: const Color(0xFF7FD6C2) + .withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + article.category, + style: AppTypography.caption.copyWith( + color: const Color(0xFF085B63), + fontWeight: FontWeight.w700, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(height: 8), + Text( + article.title, + style: AppTypography.h3.copyWith( + color: SgColors.textPrimary, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + article.description, + style: AppTypography.body, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +const List _nutritionCategories = [ + 'Semua', + 'Stunting', + 'MPASI', + 'Protein', + 'Vitamin', + 'Gizi Seimbang', +]; + +String _articleAssetByIndex(int index) { + const assets = [ + 'assets/image/onboarding_food.png', + 'assets/image/onboarding_monitoring.png', + 'assets/image/onboarding_consultation.png', + ]; + return assets[index % assets.length]; +} + diff --git a/Android/flutter_app/lib/screens/auth_screen.dart b/Android/flutter_app/lib/screens/auth_screen.dart new file mode 100644 index 0000000..f3aa918 --- /dev/null +++ b/Android/flutter_app/lib/screens/auth_screen.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import '../services/api_service.dart'; +import 'check_child_screen.dart'; + +class AuthScreen extends StatefulWidget { + const AuthScreen({super.key}); + + @override + State createState() => _AuthScreenState(); +} + +class _AuthScreenState extends State { + final _api = ApiService(); + final _phoneController = TextEditingController(text: '+62'); + final _otpController = TextEditingController(); + bool _otpSent = false; + bool _loading = false; + String? _error; + + @override + void dispose() { + _phoneController.dispose(); + _otpController.dispose(); + super.dispose(); + } + + Future _sendOtp() async { + await _guard(() async { + await _api.sendOtp(_phoneController.text); + setState(() => _otpSent = true); + }); + } + + Future _verifyOtp() async { + await _guard(() async { + final token = await _api.verifyOtp( + _phoneController.text, + _otpController.text, + ); + SgiziAppState.instance.setToken(token); + if (!mounted) return; + Navigator.of(context).pushReplacement(fadeRoute(const CheckChildScreen())); + }); + } + + Future _guard(Future Function() action) async { + setState(() { + _loading = true; + _error = null; + }); + try { + await action(); + } catch (error) { + setState(() => _error = error.toString()); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SgColors.background, + body: SafeArea( + child: ListView( + padding: const EdgeInsets.all(24), + children: [ + const SizedBox(height: 24), + const AppLogo(size: 68, showLabel: true), + const SizedBox(height: 40), + const Text('Masuk dengan Nomor HP', style: AppTypography.h1), + const SizedBox(height: 8), + const Text( + 'Kami akan mengirim kode OTP untuk menjaga data si Kecil tetap aman.', + style: AppTypography.body, + ), + const SizedBox(height: 28), + HealthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _phoneController, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + labelText: 'Nomor HP', + hintText: '+6281234567890', + prefixIcon: Icon(Icons.phone_iphone_rounded), + ), + ), + if (_otpSent) ...[ + const SizedBox(height: 16), + TextField( + controller: _otpController, + keyboardType: TextInputType.number, + maxLength: 6, + decoration: const InputDecoration( + labelText: 'Kode OTP', + hintText: '123456', + prefixIcon: Icon(Icons.lock_outline_rounded), + counterText: '', + ), + ), + ], + if (_error != null) ...[ + const SizedBox(height: 12), + Text( + _error!, + style: AppTypography.caption.copyWith( + color: SgColors.danger, + ), + ), + ], + const SizedBox(height: 20), + PrimaryButton( + label: _loading + ? 'Memproses...' + : (_otpSent ? 'Verifikasi OTP' : 'Kirim OTP'), + icon: _otpSent + ? Icons.verified_user_outlined + : Icons.sms_outlined, + onPressed: _loading + ? null + : (_otpSent ? _verifyOtp : _sendOtp), + ), + if (_otpSent) ...[ + const SizedBox(height: 12), + PrimaryButton( + label: 'Kirim Ulang OTP', + isOutlined: true, + onPressed: _loading ? null : _sendOtp, + ), + ], + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/Android/flutter_app/lib/screens/check_child_screen.dart b/Android/flutter_app/lib/screens/check_child_screen.dart new file mode 100644 index 0000000..a4ee58e --- /dev/null +++ b/Android/flutter_app/lib/screens/check_child_screen.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import '../services/api_service.dart'; +import 'add_child_screen.dart'; +import 'app_shell.dart'; + +class CheckChildScreen extends StatefulWidget { + const CheckChildScreen({super.key}); + + @override + State createState() => _CheckChildScreenState(); +} + +class _CheckChildScreenState extends State { + final _api = ApiService(); + late Future _future; + + @override + void initState() { + super.initState(); + _future = _load(); + } + + Future _load() async { + final children = await _api.getChildren(); + SgiziAppState.instance.setChildren(children); + if (!mounted) return; + Navigator.of(context).pushReplacement( + fadeRoute( + children.isEmpty + ? const AddChildScreen(isFirstSetup: true) + : const AppShell(), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SgColors.background, + body: FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (snapshot.hasError) { + return ErrorState( + message: + 'Data anak belum dapat dimuat. Coba ulangi koneksi ke server.', + onRetry: () => setState(() => _future = _load()), + ); + } + + return const Center( + child: CircularProgressIndicator(color: SgColors.primary), + ); + }, + ), + ); + } +} diff --git a/Android/flutter_app/lib/screens/children_screen.dart b/Android/flutter_app/lib/screens/children_screen.dart new file mode 100644 index 0000000..b9722b9 --- /dev/null +++ b/Android/flutter_app/lib/screens/children_screen.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import 'add_child_screen.dart'; + +class ChildrenScreen extends StatelessWidget { + const ChildrenScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Pilih Anak')), + backgroundColor: SgColors.background, + body: SafeArea( + child: ListView( + padding: const EdgeInsets.all(24), + children: [ + const Text('Kelola Anak', style: AppTypography.h1), + const SizedBox(height: 8), + const Text( + 'Pilih profil anak untuk melihat perkembangan dan rekomendasi gizi yang sesuai.', + style: AppTypography.body, + ), + const SizedBox(height: 24), + ...SgiziAppState.instance.children.map( + (child) => HealthCard( + margin: const EdgeInsets.only(bottom: 16), + onTap: () { + SgiziAppState.instance.setActiveChild(child.id); + Navigator.pop(context); + }, + color: child.id == SgiziAppState.instance.activeChildId + ? const Color(0xFFEAF7F7) + : Colors.white, + borderColor: child.id == SgiziAppState.instance.activeChildId + ? SgColors.primary + : SgColors.border, + child: Row( + children: [ + Stack( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: + child.id == SgiziAppState.instance.activeChildId + ? const Color(0xFFD9EEE7) + : const Color(0xFFF0F2F1), + child: Icon( + child.jenisKelamin == 'P' + ? Icons.face_3_rounded + : Icons.face_rounded, + color: + child.id == SgiziAppState.instance.activeChildId + ? SgColors.primary + : SgColors.textSecondary, + ), + ), + if (child.id == SgiziAppState.instance.activeChildId) + Positioned( + right: 0, + top: 0, + child: Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: SgColors.primary, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + ), + child: const Icon( + Icons.check, + color: Colors.white, + size: 14, + ), + ), + ), + ], + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + child.nama, + style: AppTypography.h2, + overflow: TextOverflow.ellipsis, + ), + ), + if (child.id == + SgiziAppState.instance.activeChildId) ...[ + const SizedBox(width: 8), + const StatusBadge( + text: 'Aktif', + color: SgColors.primary, + ), + ], + ], + ), + const SizedBox(height: 6), + Text( + child.jenisKelamin == 'P' + ? 'Perempuan' + : 'Laki-laki', + style: AppTypography.body, + ), + const SizedBox(height: 6), + Text( + 'Update terakhir: ${child.latestMeasurementAt ?? '-'}', + style: AppTypography.caption.copyWith( + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ), + const Icon( + Icons.chevron_right_rounded, + color: SgColors.textSecondary, + ), + ], + ), + ), + ), + const SizedBox(height: 8), + HealthCard( + color: const Color(0xFFF1FAF4), + borderColor: SgColors.secondary, + child: Column( + children: [ + const Text('Punya anak lain?', style: AppTypography.h3), + const SizedBox(height: 12), + PrimaryButton( + label: 'Tambah Anak', + icon: Icons.add_rounded, + onPressed: () => Navigator.of( + context, + ).push(fadeRoute(const AddChildScreen())), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/Android/flutter_app/lib/screens/consultation_chat_screen.dart b/Android/flutter_app/lib/screens/consultation_chat_screen.dart new file mode 100644 index 0000000..66b3e92 --- /dev/null +++ b/Android/flutter_app/lib/screens/consultation_chat_screen.dart @@ -0,0 +1,1402 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import '../models/riwayat_response_model.dart'; +import '../services/api_service.dart'; +import '../services/local_notification_service.dart'; +import '../utils/nutrition_display_utils.dart'; + +enum ConsultationStatus { aktif, menunggu, selesai } + +class _Expert { + const _Expert({ + required this.id, + required this.name, + required this.specialization, + required this.focusTag, + required this.assetImage, + required this.online, + required this.rating, + required this.experience, + this.recommended = false, + this.reason = '', + }); + + final String id; + final String name; + final String specialization; + final String focusTag; + final String assetImage; + final bool online; + final double rating; + final String experience; + final bool recommended; + final String reason; + + _Expert copyWith({ + bool? recommended, + String? reason, + }) { + return _Expert( + id: id, + name: name, + specialization: specialization, + focusTag: focusTag, + assetImage: assetImage, + online: online, + rating: rating, + experience: experience, + recommended: recommended ?? this.recommended, + reason: reason ?? this.reason, + ); + } +} + +class _RoomVm { + const _RoomVm({ + required this.id, + required this.keyId, + required this.childName, + required this.expert, + required this.status, + required this.lastMessage, + required this.updatedAt, + required this.unread, + }); + + final int id; + final String keyId; + final String childName; + final _Expert expert; + final ConsultationStatus status; + final String lastMessage; + final DateTime updatedAt; + final int unread; + + _RoomVm copyWith({ + ConsultationStatus? status, + }) { + return _RoomVm( + id: id, + keyId: keyId, + childName: childName, + expert: expert, + status: status ?? this.status, + lastMessage: lastMessage, + updatedAt: updatedAt, + unread: unread, + ); + } +} + +class _MsgVm { + const _MsgVm({ + required this.id, + required this.text, + required this.createdAt, + required this.fromUser, + this.read = true, + this.analysis = false, + }); + + final int id; + final String text; + final DateTime createdAt; + final bool fromUser; + final bool read; + final bool analysis; +} + +class ConsultationChatScreen extends StatefulWidget { + const ConsultationChatScreen({ + super.key, + this.showAppBar = true, + this.roomKey, + }); + + final bool showAppBar; + final String? roomKey; + + @override + State createState() => _ConsultationChatScreenState(); +} + +class _ConsultationChatScreenState extends State { + final _api = ApiService(); + final _app = SgiziAppState.instance; + final _search = TextEditingController(); + final _input = TextEditingController(); + final _scroll = ScrollController(); + + Timer? _roomPoll; + bool _isPolling = false; + bool _loadingMessages = false; + int? _pollingRoomId; + bool _loading = true; + bool _sending = false; + bool _openingChat = false; + RiwayatItemModel? _latest; + + List<_Expert> _experts = const []; + List<_RoomVm> _rooms = const []; + List<_MsgVm> _messages = const []; + _RoomVm? _activeRoom; + + @override + void initState() { + super.initState(); + _reloadEverything(); + } + + @override + void dispose() { + _stopPolling(); + _search.dispose(); + _input.dispose(); + _scroll.dispose(); + super.dispose(); + } + + Future _reloadEverything() async { + _stopPolling(); + setState(() => _loading = true); + + final child = _app.activeChild; + if (child == null) { + setState(() { + _loading = false; + _rooms = const []; + _messages = const []; + _activeRoom = null; + }); + return; + } + + try { + final history = await _api.getRiwayat(childId: child.id); + final sorted = [...history.riwayat]..sort((a, b) { + final bd = DateTime.tryParse(b.tanggalUkur) ?? DateTime(2000); + final ad = DateTime.tryParse(a.tanggalUkur) ?? DateTime(2000); + return bd.compareTo(ad); + }); + _latest = sorted.isEmpty ? null : sorted.first; + } catch (_) { + _latest = null; + } + + _experts = _recommendedExperts(_latest?.statusGabungan ?? ''); + await _loadRooms(); + + if (widget.roomKey != null) { + _activeRoom = _rooms.where((e) => e.keyId == widget.roomKey).firstOrNull; + final roomId = _activeRoom?.id; + if (roomId != null) await _loadMessages(roomId); + } else { + _activeRoom = null; + _messages = const []; + } + + _startPolling(); + setState(() => _loading = false); + } + + Future _loadRooms() async { + final child = _app.activeChild; + if (child == null) { + _rooms = const []; + return; + } + try { + final rows = await _api.getConsultationRooms(childId: child.id); + _rooms = rows.map(_roomFromApi).toList(); + } catch (_) { + _rooms = const []; + } + } + + Future _loadMessages(int roomId) async { + if (_loadingMessages) return false; + _loadingMessages = true; + try { + final rows = await _api.getConsultationMessages(roomId: roomId); + final activeRoomId = _activeRoom?.id; + if (activeRoomId != null && activeRoomId != roomId) return false; + final oldLength = _messages.length; + final next = rows.map(_msgFromApi).toList(); + final changed = next.length != oldLength; + _messages = next; + return changed; + } catch (_) { + _messages = const []; + return true; + } finally { + _loadingMessages = false; + } + } + + void _startPolling() { + final activeRoom = _activeRoom; + if (activeRoom == null) { + _stopPolling(); + return; + } + + final roomId = activeRoom.id; + if (_isPolling && _pollingRoomId == roomId) return; + + _isPolling = true; + _pollingRoomId = roomId; + + _roomPoll?.cancel(); + _roomPoll = Timer.periodic(const Duration(seconds: 3), (_) async { + final room = _activeRoom; + if (!mounted || room == null) return; + if (_loadingMessages) return; + try { + final changed = await _loadMessages(room.id); + if (mounted && changed) { + setState(() {}); + _jumpBottom(); + } + } catch (e) { + debugPrint('Polling Error: $e'); + } + }); + } + + void _stopPolling() { + _roomPoll?.cancel(); + _roomPoll = null; + _isPolling = false; + _pollingRoomId = null; + } + + List<_Expert> _recommendedExperts(String status) { + final n = normalizeStatus(status); + String reasonFor(_Expert e) { + if (n.hasStunting && e.focusTag == 'growth') { + return 'Direkomendasikan berdasarkan hasil analisis tumbuh kembang anak.'; + } + if (n.hasObesitas && e.focusTag == 'obesity') { + return 'Direkomendasikan berdasarkan pola makan dan obesitas anak.'; + } + if ((n.hasUnderweight || n.hasWasting) && e.focusTag == 'weight') { + return 'Direkomendasikan berdasarkan kebutuhan peningkatan berat badan.'; + } + return 'Tersedia untuk konsultasi nutrisi umum anak.'; + } + + bool isMatch(_Expert e) { + if (n.hasStunting) return e.focusTag == 'growth'; + if (n.hasObesitas) return e.focusTag == 'obesity'; + if (n.hasUnderweight || n.hasWasting) return e.focusTag == 'weight'; + return e.focusTag == 'general'; + } + + const base = [ + _Expert( + id: 'exp-1', + name: 'dr. Siti Rahma, S.Gz', + specialization: 'Tumbuh Kembang Anak', + focusTag: 'growth', + assetImage: 'assets/image/onboarding_monitoring.png', + online: true, + rating: 4.9, + experience: '8 tahun', + ), + _Expert( + id: 'exp-2', + name: 'dr. Rina Kurnia, M.Gz', + specialization: 'MPASI & Nutrisi Balita', + focusTag: 'general', + assetImage: 'assets/image/onboarding_food.png', + online: true, + rating: 4.8, + experience: '6 tahun', + ), + _Expert( + id: 'exp-3', + name: 'dr. Aditya Pratama, S.Gz', + specialization: 'Obesitas Anak', + focusTag: 'obesity', + assetImage: 'assets/image/onboarding_consultation.png', + online: false, + rating: 4.7, + experience: '7 tahun', + ), + _Expert( + id: 'exp-4', + name: 'dr. Nabila Utami, S.Gz', + specialization: 'Peningkatan Berat Badan Anak', + focusTag: 'weight', + assetImage: 'assets/image/onboarding_food.png', + online: true, + rating: 4.8, + experience: '5 tahun', + ), + ]; + + return [ + for (final e in base) + e.copyWith( + recommended: isMatch(e), + reason: reasonFor(e), + ), + ]; + } + + List _quickReplies() { + final n = normalizeStatus(_latest?.statusGabungan ?? ''); + if (n.hasStunting) { + return const [ + 'Menu tinggi protein', + 'Cara menaikkan tinggi badan', + 'Anak susah makan', + ]; + } + if (n.hasObesitas) { + return const [ + 'Mengurangi gula anak', + 'Aktivitas fisik anak', + 'Porsi makan sehat', + ]; + } + if (n.hasUnderweight || n.hasWasting) { + return const [ + 'Cara menaikkan berat badan', + 'Menu tinggi kalori sehat', + ]; + } + return const ['Pola makan sehat', 'Konsultasi tumbuh kembang']; + } + + Future _openOrCreateRoom(_Expert expert) async { + if (_openingChat) return; + _openingChat = true; + final child = _app.activeChild; + if (child == null) { + _openingChat = false; + return; + } + try { + final room = await _api.openConsultationRoom( + childId: child.id, + expertId: expert.id, + expertName: expert.name, + specialization: expert.specialization, + assetImage: expert.assetImage, + online: expert.online, + ); + final roomVm = _roomFromApi(room); + _activeRoom = roomVm; + await _loadMessages(roomVm.id); + + final latest = _latest; + if (_messages.isEmpty && latest != null) { + final normalized = normalizeStatus(latest.statusGabungan); + final analysisText = + 'Hasil Analisis Anak\n\n' + 'Nama: ${child.nama}\n' + 'Umur: ${formatAgeFromMonths(latest.umurBulan)}\n' + 'Status Utama: ${normalized.primaryCategory}\n' + 'Status Tambahan: ${normalized.categories.skip(1).join(', ')}\n' + 'BB/U: ${bbuCategoryFromScore(latest.zBbu, fallback: latest.kategori.bbu)}\n' + 'TB/U: ${formatScore(latest.zTbu)}\n' + 'BB/TB: ${formatScore(latest.zBbtb)}'; + await _api.sendConsultationMessage(roomId: roomVm.id, message: analysisText); + await _loadMessages(roomVm.id); + } + + await _loadRooms(); + _startPolling(); + setState(() {}); + _jumpBottom(); + } catch (_) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Gagal membuka room konsultasi.')), + ); + } finally { + _openingChat = false; + } + } + + Future _enterExistingRoom(_RoomVm room) async { + if (_openingChat) return; + _openingChat = true; + try { + _activeRoom = room; + await _loadMessages(room.id); + _startPolling(); + if (mounted) { + setState(() {}); + _jumpBottom(); + } + } finally { + _openingChat = false; + } + } + + ConsultationStatus _statusFromApi(String raw) { + switch (raw.toLowerCase()) { + case 'menunggu': + return ConsultationStatus.menunggu; + case 'selesai': + return ConsultationStatus.selesai; + default: + return ConsultationStatus.aktif; + } + } + + _RoomVm _roomFromApi(Map json) { + return _RoomVm( + id: (json['id'] as num?)?.toInt() ?? 0, + keyId: + '${(json['child_id'] as num?)?.toInt() ?? 0}::${json['expert_id'] ?? ''}', + childName: (json['child_name'] as String?) ?? '-', + expert: _Expert( + id: (json['expert_id'] as String?) ?? '', + name: (json['expert_name'] as String?) ?? '', + specialization: (json['specialization'] as String?) ?? '', + focusTag: 'general', + assetImage: (json['asset_image'] as String?)?.isNotEmpty == true + ? (json['asset_image'] as String) + : 'assets/image/onboarding_consultation.png', + online: (json['online'] as bool?) ?? false, + rating: 4.8, + experience: '5 tahun', + ), + status: _statusFromApi((json['status'] as String?) ?? 'aktif'), + lastMessage: (json['last_message'] as String?) ?? '', + updatedAt: + DateTime.tryParse((json['updated_at'] as String?) ?? '') ?? DateTime.now(), + unread: (json['unread_count'] as num?)?.toInt() ?? 0, + ); + } + + _MsgVm _msgFromApi(Map json) { + final sender = ((json['sender_type'] as String?) ?? 'parent').toLowerCase(); + final text = (json['message'] as String?) ?? ''; + return _MsgVm( + id: (json['id'] as num?)?.toInt() ?? 0, + text: text, + createdAt: + DateTime.tryParse((json['created_at'] as String?) ?? '') ?? DateTime.now(), + fromUser: sender == 'parent', + read: (json['is_read'] as bool?) ?? true, + analysis: text.startsWith('Hasil Analisis Anak'), + ); + } + + @override + Widget build(BuildContext context) { + if (_loading) { + return Scaffold( + appBar: widget.showAppBar + ? AppBar(title: const Text('Konsultasi Ahli Gizi')) + : null, + body: const _ConsultationShimmer(), + ); + } + if (_activeRoom != null) return _buildChatRoom(); + return _buildConsultationHome(); + } + + Widget _buildConsultationHome() { + final child = _app.activeChild; + final query = _search.text.toLowerCase().trim(); + final filteredExperts = _experts.where((e) { + if (query.isEmpty) return true; + return e.name.toLowerCase().contains(query) || + e.specialization.toLowerCase().contains(query); + }).toList(); + final recommended = filteredExperts.where((e) => e.recommended).toList(); + final others = filteredExperts.where((e) => !e.recommended).toList(); + + return Scaffold( + appBar: widget.showAppBar + ? AppBar( + title: const Text('Konsultasi Ahli Gizi'), + actions: [ + IconButton( + onPressed: child == null + ? null + : () => Navigator.of(context).push( + fadeRoute(const _HistoryScreen()), + ), + icon: const Icon(LucideIcons.history), + ), + ], + ) + : null, + body: SafeArea( + child: child == null + ? const EmptyState( + title: 'Belum Ada Anak Aktif', + message: 'Pilih anak aktif terlebih dahulu untuk memulai konsultasi.', + ) + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Konsultasikan tumbuh kembang si kecil bersama ahli gizi terpercaya.', + style: AppTypography.body, + ), + const SizedBox(height: 10), + TextField( + controller: _search, + onChanged: (_) => setState(() {}), + decoration: const InputDecoration( + prefixIcon: Icon(LucideIcons.search), + hintText: 'Cari ahli gizi...', + ), + ), + const SizedBox(height: 12), + Text('Direkomendasikan Untuk Anda', style: AppTypography.h2), + const SizedBox(height: 8), + if (recommended.isEmpty) + const HealthCard( + child: Text('Belum ada ahli rekomendasi untuk status anak saat ini.'), + ) + else + ...recommended.map( + (e) => _RecommendedCard( + expert: e, + onChat: () => _openOrCreateRoom(e), + ), + ), + const SizedBox(height: 8), + Text('Riwayat Konsultasi', style: AppTypography.h2), + const SizedBox(height: 8), + if (_rooms.isEmpty) + const HealthCard(child: Text('Belum ada riwayat konsultasi.')) + else + ..._rooms.take(3).map( + (room) => _HistoryTile( + room: room, + onTap: () => _enterExistingRoom(room), + ), + ), + const SizedBox(height: 8), + Text('Ahli Gizi Lainnya', style: AppTypography.h2), + const SizedBox(height: 8), + ...others.map( + (e) => _ExpertTile( + expert: e, + onChat: () => _openOrCreateRoom(e), + ), + ), + ], + ), + ).animate().fadeIn(duration: 220.ms).slideY(begin: 0.02, end: 0), + ), + ); + } + + Widget _buildChatRoom() { + final room = _activeRoom; + if (room == null) { + return const Scaffold(body: SizedBox.shrink()); + } + final grouped = _groupByDate(_messages); + return Scaffold( + body: SafeArea( + child: Column( + children: [ + _ChatHeader( + room: room, + onBack: () { + _stopPolling(); + setState(() { + _activeRoom = null; + }); + }, + onFinish: () async { + await _api.updateConsultationRoomStatus( + roomId: room.id, + status: 'selesai', + ); + await _loadRooms(); + setState(() { + _activeRoom = room.copyWith(status: ConsultationStatus.selesai); + }); + }, + ), + Expanded( + child: grouped.isEmpty + ? const _ChatEmpty() + : ListView.builder( + controller: _scroll, + physics: const BouncingScrollPhysics(), + cacheExtent: 600, + padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), + itemCount: grouped.length, + itemBuilder: (_, i) { + final item = grouped[i]; + if (item.date != null) return _DateBadge(item.date!); + if (item.message == null) return const SizedBox.shrink(); + return _MessageBubble(message: item.message!, expert: room.expert); + }, + ), + ), + SizedBox( + height: 52, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 12), + itemBuilder: (_, i) { + final quick = _quickReplies()[i]; + final chipWidth = _quickChipWidth(quick); + return SizedBox( + width: chipWidth, + child: Material( + color: const Color(0xFFF3F8F9), + borderRadius: BorderRadius.circular(99), + elevation: 1, + shadowColor: Colors.black.withValues(alpha: 0.06), + child: InkWell( + borderRadius: BorderRadius.circular(99), + onTap: () { + _input.text = quick; + _input.selection = TextSelection.collapsed( + offset: _input.text.length, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + child: Text( + quick, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.caption.copyWith( + color: const Color(0xFF0B7A86), + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ); + }, + separatorBuilder: (_, _) => const SizedBox(width: 8), + itemCount: _quickReplies().length, + ), + ), + Container( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 12), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 14, + offset: const Offset(0, -3), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _input, + minLines: 1, + maxLines: 4, + decoration: const InputDecoration(hintText: 'Tulis pesan...'), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 48, + height: 48, + child: FilledButton( + onPressed: _sending ? null : _sendMessage, + style: FilledButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(48, 48), + maximumSize: const Size(48, 48), + fixedSize: const Size(48, 48), + shape: const CircleBorder(), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Icon(LucideIcons.send, size: 18), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Future _sendMessage() async { + final room = _activeRoom; + if (room == null) return; + final text = _input.text.trim(); + if (text.isEmpty) return; + setState(() { + _sending = true; + }); + try { + await _api.sendConsultationMessage(roomId: room.id, message: text); + _input.clear(); + await _loadMessages(room.id); + await _loadRooms(); + _jumpBottom(); + _triggerServerExpertAutoReply(room); + await LocalNotificationService.instance.show( + id: DateTime.now().millisecondsSinceEpoch % 100000, + title: 'Pesan konsultasi terkirim', + body: room.expert.name, + ); + } catch (_) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Gagal mengirim pesan.')), + ); + } finally { + if (mounted) { + setState(() { + _sending = false; + }); + } + } + } + + void _triggerServerExpertAutoReply(_RoomVm room) { + Future.delayed(const Duration(milliseconds: 1200), () async { + if (!mounted || _activeRoom?.id != room.id) return; + final text = _buildAutoReplyText(room); + try { + await _api.sendConsultationExpertReply(roomId: room.id, message: text); + } catch (_) { + // Abaikan error auto-reply agar tidak mengganggu alur kirim parent. + } + }); + } + + String _buildAutoReplyText(_RoomVm room) { + final status = normalizeStatus(_latest?.statusGabungan ?? '') + .primaryCategory + .toLowerCase(); + if (status.contains('stunting')) { + return 'Terima kasih Bunda. Untuk kondisi stunting, fokuskan protein hewani 2x/hari dan evaluasi porsi selama 7 hari ke depan.'; + } + if (status.contains('obes') || status.contains('gemuk')) { + return 'Baik Bunda, untuk kondisi ini kita atur porsi dan jadwal camilan. Hindari minuman manis kemasan selama pemantauan minggu ini.'; + } + if (status.contains('kurang') || status.contains('under')) { + return 'Siap Bunda, kita tingkatkan asupan energi secara bertahap. Mohon catat frekuensi makan utama dan selingan selama 3 hari.'; + } + return 'Terima kasih sudah berkonsultasi. Saya akan bantu review pola makan anak, silakan kirim jadwal makan harian terbaru.'; + } + + void _jumpBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_scroll.hasClients) return; + final target = _scroll.position.maxScrollExtent; + if ((_scroll.offset - target).abs() < 2) return; + _scroll.animateTo( + target, + duration: const Duration(milliseconds: 240), + curve: Curves.easeOut, + ); + }); + } + + double _quickChipWidth(String text) { + final estimate = 56 + (text.length * 7.2); + return math.min(220, math.max(80, estimate)); + } +} + +class _HistoryScreen extends StatefulWidget { + const _HistoryScreen(); + + @override + State<_HistoryScreen> createState() => _HistoryScreenState(); +} + +class _HistoryScreenState extends State<_HistoryScreen> { + final _api = ApiService(); + final _app = SgiziAppState.instance; + List<_RoomVm> _rooms = const []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final child = _app.activeChild; + if (child == null) { + if (!mounted) return; + setState(() { + _loading = false; + _rooms = const []; + }); + return; + } + try { + final rows = await _api.getConsultationRooms(childId: child.id); + if (!mounted) return; + setState(() { + _rooms = rows.map((json) { + final status = ((json['status'] as String?) ?? 'aktif').toLowerCase(); + ConsultationStatus state = ConsultationStatus.aktif; + if (status == 'menunggu') state = ConsultationStatus.menunggu; + if (status == 'selesai') state = ConsultationStatus.selesai; + return _RoomVm( + id: (json['id'] as num?)?.toInt() ?? 0, + keyId: + '${(json['child_id'] as num?)?.toInt() ?? 0}::${json['expert_id'] ?? ''}', + childName: (json['child_name'] as String?) ?? '-', + expert: _Expert( + id: (json['expert_id'] as String?) ?? '', + name: (json['expert_name'] as String?) ?? '', + specialization: (json['specialization'] as String?) ?? '', + focusTag: 'general', + assetImage: (json['asset_image'] as String?)?.isNotEmpty == true + ? (json['asset_image'] as String) + : 'assets/image/onboarding_consultation.png', + online: (json['online'] as bool?) ?? false, + rating: 4.8, + experience: '5 tahun', + ), + status: state, + lastMessage: (json['last_message'] as String?) ?? '', + updatedAt: DateTime.tryParse((json['updated_at'] as String?) ?? '') ?? + DateTime.now(), + unread: (json['unread_count'] as num?)?.toInt() ?? 0, + ); + }).toList(); + _loading = false; + }); + } catch (_) { + if (!mounted) return; + setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Riwayat Konsultasi')), + body: SafeArea( + child: _loading + ? const _ConsultationShimmer() + : _rooms.isEmpty + ? const EmptyState( + title: 'Belum Ada Konsultasi', + message: + 'Belum ada konsultasi. Mulai konsultasi pertama Anda bersama ahli gizi terpercaya.', + ) + : ListView.separated( + padding: const EdgeInsets.all(12), + itemBuilder: (_, i) => _HistoryTile( + room: _rooms[i], + onTap: () => Navigator.of(context).push( + fadeRoute(ConsultationChatScreen(roomKey: _rooms[i].keyId)), + ), + ), + separatorBuilder: (_, _) => const SizedBox(height: 8), + itemCount: _rooms.length, + ), + ), + ); + } +} + +class _RecommendedCard extends StatelessWidget { + const _RecommendedCard({required this.expert, required this.onChat}); + final _Expert expert; + final VoidCallback onChat; + + @override + Widget build(BuildContext context) { + return HealthCard( + margin: const EdgeInsets.only(bottom: 10), + borderColor: const Color(0xFF0B7A86), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + expert.assetImage, + width: 54, + height: 54, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + expert.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + Text(expert.specialization, style: AppTypography.caption), + Text( + expert.reason, + style: AppTypography.caption.copyWith(color: const Color(0xFF0B7A86)), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + OutlinedButton(onPressed: onChat, child: const Text('Chat')), + ], + ), + ); + } +} + +class _ExpertTile extends StatelessWidget { + const _ExpertTile({required this.expert, required this.onChat}); + final _Expert expert; + final VoidCallback onChat; + + @override + Widget build(BuildContext context) { + return HealthCard( + margin: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + expert.assetImage, + width: 44, + height: 44, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + expert.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + Text(expert.specialization, style: AppTypography.caption), + ], + ), + ), + OutlinedButton(onPressed: onChat, child: const Text('Chat')), + ], + ), + ); + } +} + +class _HistoryTile extends StatelessWidget { + const _HistoryTile({required this.room, required this.onTap}); + final _RoomVm room; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final last = room.lastMessage.isEmpty ? '-' : room.lastMessage; + return HealthCard( + margin: const EdgeInsets.only(bottom: 8), + onTap: onTap, + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + room.expert.assetImage, + width: 44, + height: 44, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + room.expert.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + Text(room.childName, style: AppTypography.caption), + Text( + last, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.body, + ), + ], + ), + ), + _StatusBadge(room.status), + ], + ), + ); + } +} + +class _ChatHeader extends StatelessWidget { + const _ChatHeader({ + required this.room, + required this.onBack, + required this.onFinish, + }); + + final _RoomVm room; + final VoidCallback onBack; + final VoidCallback onFinish; + + @override + Widget build(BuildContext context) { + return ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), + child: Container( + margin: const EdgeInsets.fromLTRB(12, 8, 12, 8), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE1E8E6)), + ), + child: Row( + children: [ + IconButton(onPressed: onBack, icon: const Icon(LucideIcons.chevronLeft)), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.asset( + room.expert.assetImage, + width: 40, + height: 40, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + room.expert.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + Text( + room.expert.specialization, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.caption, + ), + ], + ), + ), + _StatusBadge(room.status), + PopupMenuButton( + onSelected: (value) { + if (value == 'finish') onFinish(); + }, + itemBuilder: (_) => const [ + PopupMenuItem(value: 'finish', child: Text('Selesaikan')), + ], + icon: const Icon(LucideIcons.moreVertical), + ), + ], + ), + ), + ), + ); + } +} + +class _MessageBubble extends StatelessWidget { + const _MessageBubble({required this.message, required this.expert}); + final _MsgVm message; + final _Expert expert; + + @override + Widget build(BuildContext context) { + final isUser = message.fromUser; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!isUser) ...[ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.asset( + expert.assetImage, + width: 26, + height: 26, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 6), + ], + Flexible( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: message.analysis + ? const Color(0xFFE8F7F1) + : isUser + ? const Color(0xFF0B7A86) + : Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: message.analysis + ? const Color(0xFF0B7A86) + : const Color(0xFFE1E8E6), + ), + ), + child: message.analysis + ? _AnalysisBubbleContent(rawText: message.text) + : Text( + message.text, + softWrap: true, + style: AppTypography.body.copyWith( + color: isUser ? Colors.white : SgColors.textPrimary, + ), + ), + ), + ), + ), + const SizedBox(width: 6), + Text(_fmtTime(message.createdAt), style: AppTypography.caption), + ], + ), + ); + } +} + +class _StatusBadge extends StatelessWidget { + const _StatusBadge(this.status); + final ConsultationStatus status; + + @override + Widget build(BuildContext context) { + late final String text; + late final Color color; + switch (status) { + case ConsultationStatus.aktif: + text = 'Aktif'; + color = const Color(0xFF34A853); + break; + case ConsultationStatus.menunggu: + text = 'Menunggu Balasan'; + color = const Color(0xFFF59E0B); + break; + case ConsultationStatus.selesai: + text = 'Selesai'; + color = const Color(0xFF9CA3AF); + break; + } + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(99), + ), + child: Text( + text, + style: AppTypography.caption.copyWith( + color: color, + fontWeight: FontWeight.w700, + ), + ), + ); + } +} + +class _AnalysisBubbleContent extends StatelessWidget { + const _AnalysisBubbleContent({required this.rawText}); + + final String rawText; + + @override + Widget build(BuildContext context) { + final lines = rawText + .split('\n') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty && !e.toLowerCase().contains('hasil analisis anak')) + .toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Hasil Analisis Anak', + style: AppTypography.h3.copyWith(color: Colors.black87), + ), + const SizedBox(height: 8), + ...lines.map((line) { + final idx = line.indexOf(':'); + if (idx == -1) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( + line, + style: AppTypography.body.copyWith(color: Colors.black87), + ), + ); + } + final key = line.substring(0, idx).trim(); + final value = line.substring(idx + 1).trim(); + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: RichText( + text: TextSpan( + style: AppTypography.body.copyWith(color: Colors.black87), + children: [ + TextSpan( + text: '$key: ', + style: const TextStyle(fontWeight: FontWeight.w700), + ), + TextSpan(text: value), + ], + ), + ), + ); + }), + ], + ); + } +} + +class _DateBadge extends StatelessWidget { + const _DateBadge(this.label); + final String label; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: const Color(0xFFEAF1EF), + borderRadius: BorderRadius.circular(99), + ), + child: Text(label, style: AppTypography.caption), + ), + ); + } +} + +class _ChatEmpty extends StatelessWidget { + const _ChatEmpty(); + + @override + Widget build(BuildContext context) { + return const EmptyState( + title: 'Belum Ada Pesan', + message: 'Mulai konsultasi pertama Anda bersama ahli gizi terpercaya.', + ); + } +} + +class _ConsultationShimmer extends StatelessWidget { + const _ConsultationShimmer(); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: const Color(0xFFE8EEEC), + highlightColor: const Color(0xFFF7FAF9), + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + Container(height: 18, color: Colors.white), + const SizedBox(height: 10), + Container(height: 50, color: Colors.white), + const SizedBox(height: 10), + Container(height: 120, color: Colors.white), + ], + ), + ); + } +} + +class _GroupedMessage { + const _GroupedMessage.date(this.date) : message = null; + const _GroupedMessage.message(this.message) : date = null; + + final String? date; + final _MsgVm? message; +} + +List<_GroupedMessage> _groupByDate(List<_MsgVm> source) { + final sorted = [...source]..sort((a, b) => a.createdAt.compareTo(b.createdAt)); + final out = <_GroupedMessage>[]; + DateTime? current; + for (final m in sorted) { + final d = DateTime(m.createdAt.year, m.createdAt.month, m.createdAt.day); + if (current == null || current != d) { + out.add(_GroupedMessage.date(_fmtDay(d))); + current = d; + } + out.add(_GroupedMessage.message(m)); + } + return out; +} + +String _fmtDay(DateTime d) { + final n = DateTime.now(); + final t = DateTime(n.year, n.month, n.day); + if (d == t) return 'Hari Ini'; + if (d == t.subtract(const Duration(days: 1))) return 'Kemarin'; + const m = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des', + ]; + return '${d.day} ${m[d.month - 1]} ${d.year}'; +} + +String _fmtTime(DateTime d) => + '${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; + +extension _FirstOrNull on Iterable { + T? get firstOrNull => isEmpty ? null : first; +} + diff --git a/Android/flutter_app/lib/screens/edit_profile_screen.dart b/Android/flutter_app/lib/screens/edit_profile_screen.dart new file mode 100644 index 0000000..29495ce --- /dev/null +++ b/Android/flutter_app/lib/screens/edit_profile_screen.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lucide_icons/lucide_icons.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import '../services/api_service.dart'; + +class EditProfileScreen extends StatefulWidget { + const EditProfileScreen({ + super.key, + required this.initialName, + required this.initialPhone, + this.initialEmail = '', + }); + + final String initialName; + final String initialPhone; + final String initialEmail; + + @override + State createState() => _EditProfileScreenState(); +} + +class _EditProfileScreenState extends State { + final _api = ApiService(); + late final TextEditingController _nameController; + late final TextEditingController _phoneController; + late final TextEditingController _emailController; + final _formKey = GlobalKey(); + + bool _saving = false; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.initialName); + _phoneController = TextEditingController(text: widget.initialPhone); + _emailController = TextEditingController(text: widget.initialEmail); + } + + @override + void dispose() { + _nameController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + setState(() => _saving = true); + Map updatedProfile; + try { + updatedProfile = await _api.updateProfile( + name: _nameController.text.trim(), + phone: _phoneController.text.trim(), + email: _emailController.text.trim(), + ); + } catch (e) { + if (!mounted) return; + setState(() => _saving = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal menyimpan: $e')), + ); + return; + } + if (!mounted) return; + setState(() => _saving = false); + SgiziAppState.instance.setProfileData(updatedProfile); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Perubahan profil berhasil disimpan.')), + ); + Navigator.of(context).pop(updatedProfile); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7F6), + appBar: AppBar( + title: const Text('Edit Profile'), + backgroundColor: const Color(0xFFF5F7F6), + ), + body: SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 14, 20, 24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HealthCard( + child: Center( + child: Column( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 98, + height: 98, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: const Color(0xFF0B7A86), width: 2), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: ClipOval( + child: Image.asset( + 'assets/image/onboarding_consultation.png', + fit: BoxFit.cover, + ), + ), + ), + ), + Positioned( + right: -2, + bottom: -2, + child: InkWell( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Upload foto akan tersedia di versi berikutnya.')), + ); + }, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: const Color(0xFF0B7A86), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.12), + blurRadius: 12, + offset: const Offset(0, 6), + ), + ], + ), + child: const Icon(LucideIcons.camera, color: Colors.white, size: 16), + ), + ), + ), + ], + ).animate(onPlay: (c) => c.repeat(reverse: true)).scale( + begin: const Offset(0.99, 0.99), + end: const Offset(1.02, 1.02), + duration: 1700.ms, + ), + const SizedBox(height: 10), + Text( + 'Ubah Foto Profile', + style: AppTypography.caption.copyWith( + color: const Color(0xFF0B7A86), + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 14), + _InputField( + controller: _nameController, + label: 'Nama Lengkap', + icon: LucideIcons.user, + validator: (v) => (v == null || v.trim().isEmpty) ? 'Nama wajib diisi.' : null, + ), + const SizedBox(height: 12), + _InputField( + controller: _phoneController, + label: 'Nomor Telepon', + icon: LucideIcons.messageCircle, + validator: (v) => (v == null || v.trim().length < 8) ? 'Nomor telepon tidak valid.' : null, + ), + const SizedBox(height: 12), + _InputField( + controller: _emailController, + label: 'Email', + icon: LucideIcons.info, + validator: (v) { + final value = (v ?? '').trim(); + if (value.isEmpty) return null; + return value.contains('@') ? null : 'Format email tidak valid.'; + }, + ), + const SizedBox(height: 18), + Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF0B7A86), Color(0xFF1597A4)], + ), + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: const Color(0xFF0B7A86).withValues(alpha: 0.30), + blurRadius: 18, + offset: const Offset(0, 10), + ), + ], + ), + child: FilledButton( + onPressed: _saving ? null : _save, + style: FilledButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + ), + child: _saving + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2.2, color: Colors.white), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Simpan Perubahan', + style: AppTypography.h3.copyWith(color: Colors.white), + ), + const SizedBox(width: 8), + const Icon(LucideIcons.arrowRight, color: Colors.white, size: 18), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _InputField extends StatelessWidget { + const _InputField({ + required this.controller, + required this.label, + required this.icon, + this.validator, + }); + + final TextEditingController controller; + final String label; + final IconData icon; + final String? Function(String?)? validator; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + validator: validator, + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon, color: const Color(0xFF0B7A86)), + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: Color(0xFFE3EAE8)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: Color(0xFFE3EAE8)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: const BorderSide(color: Color(0xFF0B7A86), width: 1.6), + ), + ), + ); + } +} + diff --git a/Android/flutter_app/lib/screens/help_screen.dart b/Android/flutter_app/lib/screens/help_screen.dart new file mode 100644 index 0000000..04037e2 --- /dev/null +++ b/Android/flutter_app/lib/screens/help_screen.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../app_design.dart'; + +class HelpScreen extends StatefulWidget { + const HelpScreen({super.key}); + + @override + State createState() => _HelpScreenState(); +} + +class _HelpScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7F6), + appBar: AppBar( + title: const Text('Bantuan'), + backgroundColor: const Color(0xFFF5F7F6), + ), + body: SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 14, 20, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pusat bantuan dan informasi aplikasi S-Gizi.', + style: AppTypography.body.copyWith(color: SgColors.textPrimary), + ), + const SizedBox(height: 14), + const Text('FAQ', style: AppTypography.h2), + const SizedBox(height: 10), + const _FaqCard( + question: 'Bagaimana cara menghitung status gizi anak?', + answer: + 'Masuk ke menu Hitung Gizi, isi data berat, tinggi, usia, lalu simpan untuk melihat hasil analisis WHO.', + ), + const SizedBox(height: 8), + const _FaqCard( + question: 'Bagaimana cara membaca hasil analisis?', + answer: + 'Lihat bagian status gabungan, z-score, dan rekomendasi menu untuk memahami kondisi gizi terbaru anak.', + ), + const SizedBox(height: 8), + const _FaqCard( + question: 'Bagaimana cara konsultasi dengan ahli?', + answer: + 'Buka tab Ahli, pilih topik konsultasi, lalu kirim pertanyaan Anda melalui fitur chat.', + ), + const SizedBox(height: 16), + const Text('Hubungi Admin', style: AppTypography.h2), + const SizedBox(height: 10), + HealthCard( + child: Column( + children: [ + _ContactButton( + icon: LucideIcons.messageCircle, + title: 'WhatsApp Admin', + subtitle: '081249583765', + onTap: () => _launch(context, 'https://wa.me/6281249583765'), + ), + const SizedBox(height: 8), + _ContactButton( + icon: LucideIcons.info, + title: 'Email', + subtitle: 'smartgiziapp@gmail.com', + onTap: () => _launch(context, 'mailto:smartgiziapp@gmail.com'), + ), + const SizedBox(height: 8), + _ContactButton( + icon: LucideIcons.user, + title: 'Telepon', + subtitle: '081249583765', + onTap: () => _launch(context, 'tel:081249583765'), + ), + ], + ), + ), + ], + ), + ).animate().fadeIn(duration: 220.ms).slideY(begin: 0.02, end: 0), + ), + ); + } +} + +class _FaqCard extends StatefulWidget { + const _FaqCard({required this.question, required this.answer}); + + final String question; + final String answer; + + @override + State<_FaqCard> createState() => _FaqCardState(); +} + +class _FaqCardState extends State<_FaqCard> { + bool _expanded = false; + + @override + Widget build(BuildContext context) { + return HealthCard( + child: Column( + children: [ + InkWell( + onTap: () => setState(() => _expanded = !_expanded), + child: Row( + children: [ + Expanded( + child: Text( + widget.question, + style: AppTypography.h3, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + AnimatedRotation( + turns: _expanded ? 0.5 : 0, + duration: const Duration(milliseconds: 220), + child: const Icon(LucideIcons.chevronRight), + ), + ], + ), + ), + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: Padding( + padding: const EdgeInsets.only(top: 10), + child: Text(widget.answer, style: AppTypography.body), + ), + crossFadeState: _expanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 220), + ), + ], + ), + ); + } +} + +class _ContactButton extends StatelessWidget { + const _ContactButton({ + required this.icon, + required this.title, + required this.subtitle, + required this.onTap, + }); + + final IconData icon; + final String title; + final String subtitle; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF7FAF9), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2EAE7)), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFF7FD6C2).withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(14), + ), + child: Icon(icon, color: const Color(0xFF0B7A86), size: 18), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTypography.h3), + Text( + subtitle, + style: AppTypography.caption, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const Icon(LucideIcons.chevronRight, color: SgColors.textSecondary), + ], + ), + ), + ); + } +} + +Future _launch(BuildContext context, String url) async { + final uri = Uri.parse(url); + final ok = await launchUrl(uri, mode: LaunchMode.externalApplication); + if (!ok && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Tidak dapat membuka tautan.')), + ); + } +} + diff --git a/Android/flutter_app/lib/screens/home_screen.dart b/Android/flutter_app/lib/screens/home_screen.dart new file mode 100644 index 0000000..f70a195 --- /dev/null +++ b/Android/flutter_app/lib/screens/home_screen.dart @@ -0,0 +1,804 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import '../models/mobile_child_model.dart'; +import '../models/news_article_model.dart'; +import '../models/riwayat_response_model.dart'; +import '../services/api_service.dart'; +import '../utils/nutrition_display_utils.dart'; +import 'article_detail_screen.dart'; +import 'children_screen.dart'; +import 'consultation_chat_screen.dart'; +import 'input_screen.dart'; +import 'recommendation_screen.dart'; +import 'riwayat_screen.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key, required this.onChangeTab}); + final ValueChanged onChangeTab; + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + final ApiService _apiService = ApiService(); + final SgiziAppState _appState = SgiziAppState.instance; + late Future<_HomeDashboardData> _future; + late Future> _articlesFuture; + + @override + void initState() { + super.initState(); + _appState.addListener(_handleState); + _future = _load(); + _articlesFuture = _loadArticles(); + } + + @override + void dispose() { + _appState.removeListener(_handleState); + super.dispose(); + } + + void _handleState() => setState(() { + _future = _load(); + _articlesFuture = _loadArticles(); + }); + void _retry() => setState(() { + _future = _load(); + _articlesFuture = _loadArticles(); + }); + + Future<_HomeDashboardData> _load() async { + final child = _appState.activeChild; + if (child == null) { + return const _HomeDashboardData(child: null, history: null); + } + final history = await _apiService.getRiwayat(childId: child.id); + return _HomeDashboardData(child: child, history: history); + } + + Future> _loadArticles() async { + // Dashboard artikel: gabungan DB admin + online (Google News). + final db = await _apiService.getArticlesDb().catchError((_) => []); + final news = await _apiService.getNewsArticles().catchError((_) => []); + + final merged = []; + final seen = {}; + + void addAll(List items) { + for (final a in items) { + final key = '${a.title}__${a.category}'.toLowerCase().trim(); + if (key.isEmpty || seen.contains(key)) continue; + seen.add(key); + merged.add(a); + } + } + + // Prioritaskan artikel dari DB admin, lalu tambah dari online. + addAll(db); + addAll(news); + return merged; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7F6), + body: SafeArea( + child: FutureBuilder<_HomeDashboardData>( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const _HomeSkeleton(); + } + if (snapshot.hasError) { + return _HomeError(onRetry: _retry); + } + final data = snapshot.data!; + final child = data.child; + final latest = data.latestMeasurement; + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _TopBar(), + const SizedBox(height: 12), + const Divider( + height: 1, + thickness: 1, + color: Color(0xFFE2E8E6), + ), + const SizedBox(height: 14), + Text( + 'Halo, Bunda 👋', + style: AppTypography.h1.copyWith(fontSize: 34), + ), + const SizedBox(height: 6), + const Text( + 'Ayo pantau tumbuh kembang si kecil hari ini.', + style: AppTypography.body, + ), + const SizedBox(height: 16), + _ActiveChildCard( + child: child, + onTap: () => + Navigator.of(context).push(fadeRoute(const ChildrenScreen())), + ), + const SizedBox(height: 16), + if (child == null) + EmptyState( + title: 'Belum Ada Data Anak', + message: 'Tambahkan data anak agar dashboard aktif.', + actionLabel: 'Tambah Data Anak', + onAction: () => + Navigator.of(context).push(fadeRoute(const ChildrenScreen())), + ) + else + _ModernStatusCard( + latest: latest, + onOpen: () => Navigator.of(context).push( + fadeRoute(RiwayatScreen(childId: child.id)), + ), + ), + const SizedBox(height: 22), + Text( + 'Menu Utama', + style: GoogleFonts.montserrat( + fontSize: 27, + fontWeight: FontWeight.w700, + color: SgColors.textPrimary, + ), + ), + const SizedBox(height: 16), + _MainMenuRow( + onTapInput: () => + Navigator.of(context).push(fadeRoute(const InputScreen())), + onTapRecommendation: latest == null + ? null + : () => Navigator.of(context).push( + fadeRoute( + RecommendationScreen( + childId: child?.id, + riwayatId: latest?.id, + childName: child?.nama, + status: latest?.statusGabungan, + measuredAt: latest?.tanggalUkur, + ), + ), + ), + onTapConsultation: () => Navigator.of(context).push( + fadeRoute(const ConsultationChatScreen()), + ), + ), + const SizedBox(height: 22), + Row( + children: [ + Expanded( + child: Text( + 'Edukasi Si Kecil', + style: GoogleFonts.montserrat( + fontSize: 27, + fontWeight: FontWeight.w700, + color: SgColors.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + TextButton( + onPressed: () => widget.onChangeTab(1), + child: const Text('Lihat Semua'), + ), + ], + ), + const SizedBox(height: 14), + FutureBuilder>( + future: _articlesFuture, + builder: (context, articleSnapshot) { + if (articleSnapshot.connectionState == ConnectionState.waiting) { + return const _ArticleSkeletonList(); + } + if (articleSnapshot.hasError) { + return _ArticleError(onRetry: _retry); + } + final articles = articleSnapshot.data ?? const []; + if (articles.isEmpty) { + return _ArticleEmpty(onRetry: _retry); + } + return SizedBox( + height: 330, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: articles.length > 8 ? 8 : articles.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) { + return _ArticleCard( + article: articles[index], + related: articles, + index: index, + ); + }, + ), + ); + }, + ), + ], + ), + ); + }, + ), + ), + ); + } +} + +class _HomeDashboardData { + const _HomeDashboardData({ + required this.child, + required this.history, + }); + + final MobileChildModel? child; + final RiwayatResponseModel? history; + + RiwayatItemModel? get latestMeasurement { + final records = history?.riwayat; + if (records == null || records.isEmpty) return null; + return records.first; + } +} + +class _TopBar extends StatelessWidget { + const _TopBar(); + @override + Widget build(BuildContext context) { + return Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(18), + child: Image.asset( + 'assets/image/logo_sgizi.png', + width: 66, + height: 66, + fit: BoxFit.cover, + ), + ), + const Spacer(), + Stack( + children: [ + const CircleAvatar( + radius: 22, + backgroundImage: AssetImage('assets/image/onboarding_consultation.png'), + ), + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 11, + height: 11, + decoration: BoxDecoration( + color: const Color(0xFF34C759), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ).animate(onPlay: (c) => c.repeat(reverse: true)).moveY( + begin: 0, + end: -2, + duration: 1800.ms, + ), + ], + ); + } +} + +class _ActiveChildCard extends StatelessWidget { + const _ActiveChildCard({required this.child, required this.onTap}); + final MobileChildModel? child; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return HealthCard( + onTap: onTap, + child: Row( + children: [ + ChildAvatar( + name: child?.nama ?? 'Anak', + gender: child?.jenisKelamin ?? 'L', + photoUrl: child?.photoUrl, + radius: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + child?.nama ?? 'Belum ada anak aktif', + style: AppTypography.h2, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + child == null ? '-' : formatAgeFromBirthDate(child!.tanggalLahir), + style: AppTypography.body, + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF0F4F3), + borderRadius: BorderRadius.circular(999), + ), + child: const Text('Ganti'), + ), + ], + ), + ); + } +} + +class _ModernStatusCard extends StatelessWidget { + const _ModernStatusCard({required this.latest, required this.onOpen}); + final RiwayatItemModel? latest; + final VoidCallback onOpen; + + @override + Widget build(BuildContext context) { + if (latest == null) { + return HealthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('STATUS GIZI TERAKHIR'), + const SizedBox(height: 8), + const Text('Belum ada pengukuran tersimpan untuk anak aktif.'), + const SizedBox(height: 14), + PrimaryButton(label: 'Lihat Detail Analisis', onPressed: onOpen), + ], + ), + ); + } + return HealthCard( + color: const Color(0xFFE3F6F8), + borderColor: const Color(0xFFC8ECF0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'UPDATE: ${formatMeasurementDate(latest!.tanggalUkur).toUpperCase()}', + style: AppTypography.caption.copyWith(fontWeight: FontWeight.w800), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + 'Status Gizi: ${_shortStatus(latest!.statusGabungan)}', + style: AppTypography.h2, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + _statusDescription(latest!.statusGabungan), + style: AppTypography.body.copyWith(color: const Color(0xFF2E5258)), + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded(child: _SmallMetric(label: 'Berat', value: '${latest!.berat.toStringAsFixed(1)} kg')), + const SizedBox(width: 8), + Expanded(child: _SmallMetric(label: 'Tinggi', value: '${latest!.tinggi.toStringAsFixed(1)} cm')), + const SizedBox(width: 8), + Expanded(child: _SmallMetric(label: 'Usia', value: formatAgeFromMonths(latest!.umurBulan))), + ], + ), + const SizedBox(height: 12), + PrimaryButton( + label: 'Lihat Detail Analisis', + icon: LucideIcons.arrowRight, + onPressed: onOpen, + isOutlined: true, + ), + ], + ), + ); + } +} + +class _SmallMetric extends StatelessWidget { + const _SmallMetric({required this.label, required this.value}); + final String label; + final String value; + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.75), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTypography.caption), + Text(value, style: AppTypography.h3, overflow: TextOverflow.ellipsis), + ], + ), + ); + } +} + +class _MainMenuRow extends StatelessWidget { + const _MainMenuRow({ + required this.onTapInput, + required this.onTapRecommendation, + required this.onTapConsultation, + }); + final VoidCallback onTapInput; + final VoidCallback? onTapRecommendation; + final VoidCallback onTapConsultation; + + @override + Widget build(BuildContext context) { + final showRecommendation = onTapRecommendation != null; + return Row( + children: [ + Expanded( + child: _MenuItem( + icon: PhosphorIconsBold.calculator, + label: 'Hitung Gizi', + color: const Color(0xFF77D9E3), + onTap: onTapInput, + ), + ), + const SizedBox(width: 10), + if (showRecommendation) ...[ + Expanded( + child: _MenuItem( + icon: LucideIcons.apple, + label: 'Rekomendasi', + color: const Color(0xFF63D39D), + onTap: onTapRecommendation!, + ), + ), + const SizedBox(width: 10), + ], + Expanded( + child: _MenuItem( + icon: LucideIcons.messageCircle, + label: 'Konsultasi', + color: const Color(0xFFF5A56E), + onTap: onTapConsultation, + ), + ), + ], + ); + } +} + +class _MenuItem extends StatelessWidget { + const _MenuItem({ + required this.icon, + required this.label, + required this.color, + required this.onTap, + }); + final IconData icon; + final String label; + final Color color; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Column( + children: [ + Container( + width: 58, + height: 58, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.35), + blurRadius: 13, + offset: const Offset(0, 8), + ), + ], + ), + child: Icon(icon, color: Colors.white), + ), + const SizedBox(height: 8), + Text(label, style: AppTypography.caption, overflow: TextOverflow.ellipsis), + ], + ), + ); + } +} + +class _ArticleCard extends StatelessWidget { + const _ArticleCard({ + required this.article, + required this.related, + required this.index, + }); + final NewsArticleModel article; + final List related; + final int index; + @override + Widget build(BuildContext context) { + return SizedBox( + width: 250, + child: HealthCard( + padding: EdgeInsets.zero, + onTap: () => Navigator.of(context).push( + fadeRoute(ArticleDetailScreen(article: article, related: related)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + child: Hero( + tag: 'article-${article.id}', + child: _CardImage( + imageUrl: article.image, + fallbackIndex: index, + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), + child: StatusBadge( + text: article.category, + color: const Color(0xFF0B7A86), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(12, 6, 12, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + article.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + const SizedBox(height: 4), + Text( + article.description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.body, + ), + if (article.sourceName != null && + article.sourceName!.trim().isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + 'Sumber: ${article.sourceName!}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.caption.copyWith( + color: const Color(0xFF0B7A86), + fontWeight: FontWeight.w700, + ), + ), + ], + const SizedBox(height: 8), + Row( + children: [ + Text( + 'Baca Selengkapnya', + style: AppTypography.caption.copyWith( + color: const Color(0xFF0B7A86), + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(width: 6), + const Icon( + LucideIcons.arrowRight, + size: 14, + color: Color(0xFF0B7A86), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _CardImage extends StatelessWidget { + const _CardImage({required this.imageUrl, required this.fallbackIndex}); + final String? imageUrl; + final int fallbackIndex; + + @override + Widget build(BuildContext context) { + final fallback = _imageByIndex(fallbackIndex); + if (imageUrl == null || imageUrl!.isEmpty) { + return Image.asset( + fallback, + width: double.infinity, + height: 110, + fit: BoxFit.cover, + ); + } + return CachedNetworkImage( + imageUrl: imageUrl!, + width: double.infinity, + height: 110, + fit: BoxFit.cover, + placeholder: (_, __) => Container(color: const Color(0xFFEAF1EF)), + errorWidget: (_, __, ___) => Image.asset( + fallback, + width: double.infinity, + height: 110, + fit: BoxFit.cover, + ), + ); + } +} + +class _ArticleSkeletonList extends StatelessWidget { + const _ArticleSkeletonList(); + @override + Widget build(BuildContext context) { + return SizedBox( + height: 330, + child: Shimmer.fromColors( + baseColor: const Color(0xFFE8EEEC), + highlightColor: const Color(0xFFF7FAF9), + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: 3, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (_, __) => Container( + width: 250, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ), + ); + } +} + +class _ArticleError extends StatelessWidget { + const _ArticleError({required this.onRetry}); + final VoidCallback onRetry; + @override + Widget build(BuildContext context) { + return HealthCard( + child: Column( + children: [ + const Icon( + PhosphorIconsRegular.warningCircle, + size: 32, + color: SgColors.warning, + ), + const SizedBox(height: 8), + const Text('Gagal memuat artikel edukasi.'), + const SizedBox(height: 8), + TextButton(onPressed: onRetry, child: const Text('Coba Lagi')), + ], + ), + ); + } +} + +class _ArticleEmpty extends StatelessWidget { + const _ArticleEmpty({required this.onRetry}); + final VoidCallback onRetry; + @override + Widget build(BuildContext context) { + return HealthCard( + child: Column( + children: [ + Image.asset('assets/image/onboarding_monitoring.png', height: 72, fit: BoxFit.cover), + const SizedBox(height: 8), + const Text('Belum ada artikel edukasi tersedia.'), + const SizedBox(height: 8), + TextButton(onPressed: onRetry, child: const Text('Refresh')), + ], + ), + ); + } +} + +class _HomeSkeleton extends StatelessWidget { + const _HomeSkeleton(); + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Shimmer.fromColors( + baseColor: const Color(0xFFE8EEEC), + highlightColor: const Color(0xFFF7FAF9), + child: Column( + children: [ + Container(height: 44, color: Colors.white), + const SizedBox(height: 12), + Container(height: 90, color: Colors.white), + const SizedBox(height: 12), + Container(height: 180, color: Colors.white), + const SizedBox(height: 12), + Container(height: 80, color: Colors.white), + const SizedBox(height: 12), + Container(height: 220, color: Colors.white), + ], + ), + ), + ); + } +} + +class _HomeError extends StatelessWidget { + const _HomeError({required this.onRetry}); + final VoidCallback onRetry; + @override + Widget build(BuildContext context) { + return ErrorState( + message: 'Dashboard belum dapat dimuat.', + onRetry: onRetry, + ); + } +} + +String _shortStatus(String value) { + final lower = value.toLowerCase(); + if (lower.contains('normal')) return 'Baik'; + if (lower.contains('stunting') || lower.contains('kurang')) return 'Perlu Perhatian'; + return value; +} + +String _statusDescription(String status) { + final value = status.toLowerCase(); + if (value.contains('stunting')) { + return 'Tinggi badan anak masih perlu perhatian. Fokus pada protein hewani, zat besi, dan pemantauan tinggi rutin.'; + } + if (value.contains('kurang') || value.contains('underweight')) { + return 'Berat badan anak perlu ditingkatkan. Tambahkan asupan energi dan protein secara bertahap.'; + } + if (value.contains('obesitas') || value.contains('lebih')) { + return 'Berat badan perlu dikontrol. Atur porsi seimbang dan perbanyak aktivitas fisik harian anak.'; + } + return 'Berdasarkan pengukuran terakhir, tinggi dan berat badan anak sudah sesuai standar WHO.'; +} + +String _imageByIndex(int index) { + const images = [ + 'assets/image/onboarding_food.png', + 'assets/image/onboarding_monitoring.png', + 'assets/image/onboarding_consultation.png', + ]; + return images[index % images.length]; +} diff --git a/Android/flutter_app/lib/screens/input_screen.dart b/Android/flutter_app/lib/screens/input_screen.dart new file mode 100644 index 0000000..7181537 --- /dev/null +++ b/Android/flutter_app/lib/screens/input_screen.dart @@ -0,0 +1,445 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import 'loading_screen.dart'; + +class InputScreen extends StatefulWidget { + const InputScreen({super.key}); + + @override + State createState() => _InputScreenState(); +} + +class _InputScreenState extends State { + final _formKey = GlobalKey(); + final _birthDateController = TextEditingController(); + final _measurementDateController = TextEditingController(); + final _weightController = TextEditingController(); + final _heightController = TextEditingController(); + + String _jenisKelamin = 'L'; + String _caraUkur = 'standing'; + DateTime? _birthDate; + DateTime? _measurementDate; + + @override + void dispose() { + _birthDateController.dispose(); + _measurementDateController.dispose(); + _weightController.dispose(); + _heightController.dispose(); + super.dispose(); + } + + void _submit() { + FocusScope.of(context).unfocus(); + if (!_formKey.currentState!.validate()) return; + + final measurementDate = _measurementDate; + final childId = SgiziAppState.instance.activeChildId; + if (measurementDate == null || childId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Anak aktif dan tanggal ukur wajib dipilih.'), + ), + ); + return; + } + + Navigator.of(context).push( + fadeRoute( + LoadingScreen( + payload: { + 'child_id': childId, + 'tanggal_ukur': _formatDateApi(measurementDate), + 'berat_badan': _parseNumber(_weightController.text), + 'tinggi_badan': _parseNumber(_heightController.text), + 'cara_ukur': _caraUkur, + }, + ), + ), + ); + } + + double _parseNumber(String input) { + return double.parse(input.trim().replaceAll(',', '.')); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SgColors.background, + appBar: AppBar(title: const Text('Input Data Gizi')), + body: SafeArea( + child: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 112), + children: [ + Row( + children: [ + Text( + 'LANGKAH 1 DARI 2', + style: AppTypography.caption.copyWith( + color: SgColors.primary, + fontWeight: FontWeight.w800, + letterSpacing: 0.8, + ), + ), + const Spacer(), + Text( + 'Informasi Fisik', + style: AppTypography.caption.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ], + ), + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular(999), + child: const LinearProgressIndicator( + value: 0.5, + minHeight: 8, + backgroundColor: Color(0xFFE9EEEC), + valueColor: AlwaysStoppedAnimation(SgColors.primary), + ), + ), + const SizedBox(height: 24), + HealthCard( + child: AnimatedBuilder( + animation: SgiziAppState.instance, + builder: (context, _) { + final child = SgiziAppState.instance.activeChild; + return Row( + children: [ + const CircleAvatar( + radius: 26, + backgroundColor: Color(0xFFE9F6F2), + child: Icon( + Icons.child_care_rounded, + color: SgColors.primary, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Anak Aktif', + style: AppTypography.caption, + ), + Text( + child?.nama ?? 'Belum ada anak aktif', + style: AppTypography.h2, + ), + Text( + child?.tanggalLahir ?? '-', + style: AppTypography.body, + ), + ], + ), + ), + ], + ); + }, + ), + ), + const SizedBox(height: 24), + Text( + 'Tanggal Pengukuran', + style: AppTypography.h3.copyWith(fontSize: 16), + ), + const SizedBox(height: 8), + _buildDateField( + controller: _measurementDateController, + label: 'Pilih tanggal', + onTap: _pickTanggalUkur, + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: _buildNumberCard( + controller: _weightController, + label: 'Berat Badan', + unit: 'kg', + icon: Icons.monitor_weight_outlined, + hint: '0.0', + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildNumberCard( + controller: _heightController, + label: 'Tinggi Badan', + unit: 'cm', + icon: Icons.straighten_rounded, + hint: '0.0', + ), + ), + ], + ), + const SizedBox(height: 24), + Text( + 'Cara Pengukuran Tinggi', + style: AppTypography.h3.copyWith(fontSize: 16), + ), + const SizedBox(height: 12), + _MeasureToggle( + value: _caraUkur, + onChanged: (value) => setState(() => _caraUkur = value), + ), + const SizedBox(height: 24), + HealthCard( + color: const Color(0xFFEFF9F8), + borderColor: const Color(0xFFD8F0ED), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CircleAvatar( + radius: 22, + backgroundColor: Color(0xFFD7F0EF), + child: Icon( + Icons.info_outline_rounded, + color: SgColors.primary, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text('Tips Akurasi:', style: AppTypography.h3), + SizedBox(height: 6), + Text( + 'Gunakan timbangan digital dan pastikan anak tidak menggunakan alas kaki saat pengukuran tinggi badan.', + style: AppTypography.body, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + bottomSheet: Container( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 24), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.96), + border: const Border(top: BorderSide(color: SgColors.border)), + ), + child: SafeArea( + top: false, + child: PrimaryButton( + label: 'Hitung Sekarang', + icon: Icons.chevron_right_rounded, + onPressed: _submit, + ), + ), + ), + ); + } + + Widget _buildDateField({ + required TextEditingController controller, + required String label, + required VoidCallback onTap, + }) { + return TextFormField( + controller: controller, + readOnly: true, + style: AppTypography.h3.copyWith(fontSize: 16), + decoration: InputDecoration( + labelText: label, + suffixIcon: const Icon(Icons.calendar_month_outlined), + ), + validator: (value) => + (value ?? '').trim().isEmpty ? '$label wajib diisi.' : null, + onTap: onTap, + ); + } + + Widget _buildNumberCard({ + required TextEditingController controller, + required String label, + required String unit, + required IconData icon, + required String hint, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTypography.h3), + const SizedBox(height: 10), + TextFormField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + style: AppTypography.h2.copyWith(fontSize: 20), + decoration: InputDecoration( + prefixText: '$unit ', + hintText: hint, + suffixIcon: Icon(icon, size: 20), + ), + validator: (value) => _validatePositiveNumber(value, label: label), + ), + ], + ); + } + + String? _validatePositiveNumber(String? value, {required String label}) { + final text = value?.trim() ?? ''; + if (text.isEmpty) return '$label wajib diisi.'; + + final number = double.tryParse(text.replaceAll(',', '.')); + if (number == null) return '$label harus berupa angka.'; + if (number <= 0) return '$label harus lebih dari 0.'; + + return null; + } + + Future _pickTanggalLahir() async { + final now = DateTime.now(); + final initialDate = + _birthDate ?? DateTime(now.year - 3, now.month, now.day); + + final pickedDate = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: DateTime(now.year - 6), + lastDate: now, + ); + + if (pickedDate == null) return; + + setState(() { + _birthDate = pickedDate; + _birthDateController.text = _formatDateDisplay(pickedDate); + }); + } + + Future _pickTanggalUkur() async { + final now = DateTime.now(); + final initialDate = _measurementDate ?? now; + + final pickedDate = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: DateTime(now.year - 1), + lastDate: DateTime(now.year + 1), + ); + + if (pickedDate == null) return; + + setState(() { + _measurementDate = pickedDate; + _measurementDateController.text = _formatDateDisplay(pickedDate); + }); + } + + String _formatDateDisplay(DateTime date) { + final day = date.day.toString().padLeft(2, '0'); + final month = date.month.toString().padLeft(2, '0'); + final year = date.year.toString(); + return '$year-$month-$day'; + } + + String _formatDateApi(DateTime date) => _formatDateDisplay(date); +} + +class _MeasureToggle extends StatelessWidget { + const _MeasureToggle({required this.value, required this.onChanged}); + + final String value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFFEDEEEE), + borderRadius: BorderRadius.circular(24), + ), + child: Row( + children: [ + _ToggleItem( + selected: value == 'standing', + label: 'Berdiri', + icon: Icons.accessible_forward_rounded, + onTap: () => onChanged('standing'), + ), + _ToggleItem( + selected: value == 'lying', + label: 'Terlentang', + icon: Icons.bed_outlined, + onTap: () => onChanged('lying'), + ), + ], + ), + ); + } +} + +class _ToggleItem extends StatelessWidget { + const _ToggleItem({ + required this.selected, + required this.label, + required this.icon, + required this.onTap, + }); + + final bool selected; + final String label; + final IconData icon; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Expanded( + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOut, + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + color: selected ? SgColors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(18), + boxShadow: selected + ? [ + BoxShadow( + color: SgColors.primary.withValues(alpha: 0.22), + blurRadius: 12, + offset: const Offset(0, 6), + ), + ] + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: selected ? Colors.white : SgColors.textSecondary, + size: 20, + ), + const SizedBox(width: 8), + Text( + label, + style: AppTypography.h3.copyWith( + color: selected ? Colors.white : SgColors.textSecondary, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/Android/flutter_app/lib/screens/loading_screen.dart b/Android/flutter_app/lib/screens/loading_screen.dart new file mode 100644 index 0000000..0edae88 --- /dev/null +++ b/Android/flutter_app/lib/screens/loading_screen.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import '../models/api_result_model.dart'; +import '../services/api_service.dart'; +import 'result_screen.dart'; + +class LoadingScreen extends StatefulWidget { + const LoadingScreen({super.key, required this.payload}); + + final Map payload; + + @override + State createState() => _LoadingScreenState(); +} + +class _LoadingScreenState extends State { + final ApiService _apiService = ApiService(); + late Future _future; + + @override + void initState() { + super.initState(); + _future = _apiService.postHasil(widget.payload); + } + + void _retry() { + setState(() => _future = _apiService.postHasil(widget.payload)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (snapshot.hasError) { + return ErrorState( + message: + 'S-Gizi belum berhasil menghitung data. Periksa koneksi atau server API, lalu coba lagi.', + onRetry: _retry, + ); + } + + if (snapshot.hasData) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final childId = widget.payload['child_id']; + final tanggalUkur = widget.payload['tanggal_ukur']; + if (childId is int && tanggalUkur is String) { + SgiziAppState.instance.updateChildMeasurementSnapshot( + childId: childId, + latestStatus: snapshot.data!.statusGabungan, + latestMeasurementAt: tanggalUkur, + ); + } + if (!mounted) return; + Navigator.of(context).pushReplacement( + fadeRoute(ResultScreen(result: snapshot.data!)), + ); + }); + } + + return const _LoadingContent(); + }, + ), + ); + } +} + +class _LoadingContent extends StatefulWidget { + const _LoadingContent(); + + @override + State<_LoadingContent> createState() => _LoadingContentState(); +} + +class _LoadingContentState extends State<_LoadingContent> + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1600), + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RotationTransition( + turns: Tween(begin: 0, end: 1).animate(_controller), + child: Container( + width: 112, + height: 112, + decoration: BoxDecoration( + color: const Color(0xFFEFF8F7), + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFFE2F1EF), + width: 8, + ), + ), + child: const Icon( + Icons.auto_awesome_rounded, + color: SgColors.primary, + size: 38, + ), + ), + ), + const SizedBox(height: 32), + const Text( + 'Sedang menghitung status gizi...', + style: AppTypography.h1, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Text( + 'Mohon tunggu sebentar, sistem S-Gizi sedang menganalisis data pertumbuhan si Kecil berdasarkan standar kesehatan.', + style: AppTypography.body, + textAlign: TextAlign.center, + ), + const SizedBox(height: 40), + ClipRRect( + borderRadius: BorderRadius.circular(99), + child: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final value = 0.18 + (_controller.value * 0.72); + return LinearProgressIndicator( + value: value, + minHeight: 8, + backgroundColor: const Color(0xFFEFF4F2), + valueColor: const AlwaysStoppedAnimation( + SgColors.primary, + ), + ); + }, + ), + ), + const SizedBox(height: 12), + AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final percent = (20 + (_controller.value * 70)).round(); + return Text( + 'MEMPROSES DATA $percent%', + style: AppTypography.caption.copyWith( + letterSpacing: 1.2, + fontWeight: FontWeight.w800, + ), + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/Android/flutter_app/lib/screens/nutrition_screen.dart b/Android/flutter_app/lib/screens/nutrition_screen.dart new file mode 100644 index 0000000..2c1e670 --- /dev/null +++ b/Android/flutter_app/lib/screens/nutrition_screen.dart @@ -0,0 +1,1369 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../app_design.dart'; +import '../app_state.dart'; +import '../models/api_result_model.dart'; +import '../models/mobile_child_model.dart'; +import '../models/news_article_model.dart'; +import '../models/riwayat_response_model.dart'; +import '../services/api_service.dart'; +import '../utils/nutrition_display_utils.dart'; +import 'article_detail_screen.dart'; +import 'articles_screen.dart'; +import 'recommendation_screen.dart'; +import 'riwayat_screen.dart'; + +class NutritionScreen extends StatefulWidget { + const NutritionScreen({super.key}); + + @override + State createState() => _NutritionScreenState(); +} + +class _NutritionScreenState extends State { + final ApiService _apiService = ApiService(); + final SgiziAppState _appState = SgiziAppState.instance; + late Future<_NutritionViewData> _future; + String _activeCategory = 'Semua'; + int _activeAgeIndex = 0; + int _activeFoodIndex = 0; + + @override + void initState() { + super.initState(); + _appState.addListener(_handleStateChanged); + _future = _load(); + } + + @override + void dispose() { + _appState.removeListener(_handleStateChanged); + super.dispose(); + } + + void _handleStateChanged() { + setState(() => _future = _load()); + } + + Future<_NutritionViewData> _load() async { + final child = _appState.activeChild; + if (child == null) { + return const _NutritionViewData.noActiveChild(); + } + + final history = await _apiService.getRiwayat(childId: child.id); + final articles = await _apiService.getArticlesDb(); + final sorted = [...history.riwayat]..sort((a, b) { + final bDate = DateTime.tryParse(b.tanggalUkur); + final aDate = DateTime.tryParse(a.tanggalUkur); + if (aDate == null && bDate == null) return 0; + if (aDate == null) return 1; + if (bDate == null) return -1; + return bDate.compareTo(aDate); + }); + final latest = sorted.isNotEmpty ? sorted.first : null; + + if (latest == null) { + return _NutritionViewData( + child: child, + latestMeasurement: null, + recommendations: const [], + articles: articles, + ); + } + + final recommendationResponse = await _apiService.getRecommendations( + status: '', + childId: child.id, + riwayatId: latest.id, + ); + + return _NutritionViewData( + child: child, + latestMeasurement: latest, + // Preview rekomendasi berdasarkan riwayat TERAKHIR anak aktif. + recommendations: recommendationResponse.items, + articles: articles, + ); + } + + void _retry() { + setState(() => _future = _load()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _NutriColors.background, + body: SafeArea( + child: FutureBuilder<_NutritionViewData>( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const _NutritionSkeleton(); + } + + if (snapshot.hasError) { + return ErrorState( + message: + 'Halaman nutrisi belum bisa dimuat. Periksa koneksi lalu coba lagi.', + onRetry: _retry, + ); + } + + final data = snapshot.data!; + if (data.child == null) { + return EmptyState( + title: 'Belum Ada Anak Aktif', + message: + 'Pilih anak aktif terlebih dahulu untuk melihat insight nutrisi terbaru.', + actionLabel: 'Muat Ulang', + onAction: _retry, + ); + } + + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _NutritionHeader(), + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _AgeGuideSection( + activeIndex: _activeAgeIndex, + onPageChanged: (index) => + setState(() => _activeAgeIndex = index), + ), + const SizedBox(height: 24), + _CategoryChipsRow( + activeCategory: _activeCategory, + onCategorySelected: (label) => + setState(() => _activeCategory = label), + ), + const SizedBox(height: 16), + _ArticleSection( + data: data, + activeCategory: _activeCategory, + onRetry: _retry, + ), + const SizedBox(height: 24), + _LatestStatusSection(data: data), + const SizedBox(height: 24), + _FoodRecommendationSection( + data: data, + activeIndex: _activeFoodIndex, + onPageChanged: (index) => + setState(() => _activeFoodIndex = index), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ); + } +} + +class _NutritionViewData { + const _NutritionViewData({ + required this.child, + required this.latestMeasurement, + required this.recommendations, + required this.articles, + }); + + const _NutritionViewData.noActiveChild() + : child = null, + latestMeasurement = null, + recommendations = const [], + articles = const []; + + final MobileChildModel? child; + final RiwayatItemModel? latestMeasurement; + final List recommendations; + final List articles; +} + +class _NutritionHeader extends StatelessWidget { + const _NutritionHeader(); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [_NutriColors.primaryDark, _NutriColors.primary], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(32), + ), + ), + child: Stack( + children: [ + Positioned( + right: -40, + top: -20, + child: _FloatingCircle( + size: 160, + color: Colors.white.withValues(alpha: 0.08), + ), + ), + Positioned( + left: -24, + bottom: -40, + child: _FloatingCircle( + size: 140, + color: Colors.white.withValues(alpha: 0.06), + ), + ), + Positioned( + right: 24, + top: 26, + child: Icon( + LucideIcons.activity, + color: Colors.white.withValues(alpha: 0.5), + size: 40, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Text( + 'Nutrisi', + style: AppTypography.h1.copyWith( + color: Colors.white, + fontSize: 26, + ), + ), + const SizedBox(height: 6), + Text( + 'Panduan nutrisi dan edukasi tumbuh kembang si kecil secara optimal.', + style: AppTypography.body.copyWith( + color: Colors.white.withValues(alpha: 0.9), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ) + .animate() + .fadeIn(duration: 500.ms, curve: Curves.easeOut) + .slideY(begin: -0.1, end: 0, duration: 500.ms, curve: Curves.easeOut); + } +} + +class _FloatingCircle extends StatelessWidget { + const _FloatingCircle({required this.size, required this.color}); + + final double size; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + ); + } +} + +class _AgeGuideSection extends StatelessWidget { + const _AgeGuideSection({ + required this.activeIndex, + required this.onPageChanged, + }); + + final int activeIndex; + final ValueChanged onPageChanged; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Panduan Usia', + style: AppTypography.h2.copyWith( + color: SgColors.textPrimary, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: _NutriColors.secondary.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(999), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon( + PhosphorIconsLight.baby, + size: 14, + color: _NutriColors.primary, + ), + SizedBox(width: 6), + Text( + '0–5 Tahun', + style: AppTypography.caption, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + CarouselSlider.builder( + itemCount: _ageGuides.length, + options: CarouselOptions( + height: 292, + viewportFraction: 0.9, + enlargeCenterPage: true, + enlargeFactor: 0.16, + autoPlay: true, + autoPlayInterval: const Duration(seconds: 6), + enableInfiniteScroll: true, + onPageChanged: (index, _) => onPageChanged(index), + ), + itemBuilder: (context, index, realIndex) { + final guide = _ageGuides[index]; + final isActive = index == activeIndex; + return _AgeGuideCard( + guide: guide, + isActive: isActive, + ); + }, + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_ageGuides.length, (index) { + final isActive = index == activeIndex; + return AnimatedContainer( + duration: const Duration(milliseconds: 220), + margin: const EdgeInsets.symmetric(horizontal: 3), + height: 7, + width: isActive ? 18 : 7, + decoration: BoxDecoration( + color: isActive + ? _NutriColors.primary + : _NutriColors.primary.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(99), + ), + ); + }), + ), + ], + ) + .animate(delay: 80.ms) + .fadeIn(duration: 420.ms, curve: Curves.easeOut) + .slideY(begin: 0.03, end: 0, duration: 420.ms, curve: Curves.easeOut); + } +} + +class _CategoryChipsRow extends StatelessWidget { + const _CategoryChipsRow({ + required this.activeCategory, + required this.onCategorySelected, + }); + + final String activeCategory; + final ValueChanged onCategorySelected; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Kategori Edukasi', style: _sectionLabelStyle()), + const SizedBox(height: 10), + SizedBox( + height: 40, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (_, index) { + final label = _nutritionCategories[index]; + final isActive = label == activeCategory; + return _CategoryChip( + label: label, + isActive: isActive, + onTap: () => onCategorySelected(label), + ); + }, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemCount: _nutritionCategories.length, + ), + ), + ], + ) + .animate(delay: 140.ms) + .fadeIn(duration: 380.ms, curve: Curves.easeOut) + .slideY(begin: 0.03, end: 0, duration: 380.ms, curve: Curves.easeOut); + } +} + +class _ArticleSection extends StatelessWidget { + const _ArticleSection({ + required this.data, + required this.activeCategory, + required this.onRetry, + }); + + final _NutritionViewData data; + final String activeCategory; + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + final articles = data.articles + .where( + (a) => + activeCategory == 'Semua' || + a.category.toLowerCase().contains(activeCategory.toLowerCase()), + ) + .toList(); + + final isAll = activeCategory == 'Semua'; + final visible = isAll ? articles.take(3).toList() : articles; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('Artikel Edukasi', style: _sectionLabelStyle()), + const SizedBox(width: 8), + Icon( + LucideIcons.bookOpenCheck, + size: 16, + color: _NutriColors.primary, + ), + const Spacer(), + if (isAll && articles.length > 3) + TextButton( + onPressed: () { + Navigator.of(context).push( + fadeRoute( + ArticlesScreen( + title: 'Artikel Edukasi', + initialCategory: activeCategory, + articles: data.articles, + ), + ), + ); + }, + child: const Text('Lihat Semua'), + ), + ], + ), + const SizedBox(height: 10), + if (data.articles.isEmpty) + _SectionEmptyInfo( + message: + 'Artikel edukasi belum tersedia. Sentuh untuk memuat ulang dari server.', + actionLabel: 'Muat Ulang', + onAction: onRetry, + ) + else if (articles.isEmpty) + const _SectionEmptyInfo( + message: + 'Belum ada artikel untuk kategori ini. Coba pilih kategori lain.', + ) + else + Column( + children: visible + .map( + (article) => _ArticleCard( + article: article, + allArticles: data.articles, + ), + ) + .toList(), + ), + ], + ); + } +} + +class _LatestStatusSection extends StatelessWidget { + const _LatestStatusSection({required this.data}); + + final _NutritionViewData data; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Status Gizi Anak', style: _sectionLabelStyle()), + const SizedBox(height: 12), + if (data.latestMeasurement == null) + _SectionEmptyInfo( + message: + 'Belum ada riwayat pengukuran untuk anak aktif. Tambahkan pengukuran untuk menampilkan status terbaru.', + actionLabel: 'Buka Riwayat', + onAction: () { + Navigator.of(context).push( + fadeRoute(RiwayatScreen(childId: data.child!.id)), + ); + }, + ) + else + _LatestNutritionStatusCard( + child: data.child!, + latest: data.latestMeasurement!, + onOpenHistory: () => Navigator.of(context).push( + fadeRoute(RiwayatScreen(childId: data.child!.id)), + ), + ), + ], + ); + } +} + +class _FoodRecommendationSection extends StatelessWidget { + const _FoodRecommendationSection({ + required this.data, + required this.activeIndex, + required this.onPageChanged, + }); + + final _NutritionViewData data; + final int activeIndex; + final ValueChanged onPageChanged; + + @override + Widget build(BuildContext context) { + // Preview max 3, berasal dari rekomendasi untuk riwayat TERAKHIR anak aktif. + final items = data.recommendations.take(3).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + 'Rekomendasi Untuk Anak', + style: AppTypography.h2.copyWith( + color: SgColors.textPrimary, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + TextButton( + onPressed: () { + Navigator.of(context).push( + fadeRoute( + RecommendationScreen( + childId: data.child!.id, + riwayatId: data.latestMeasurement?.id, + childName: data.child!.nama, + status: data.latestMeasurement?.statusGabungan, + measuredAt: data.latestMeasurement?.tanggalUkur, + ), + ), + ); + }, + child: const Text('Lihat Semua'), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Menu sesuai status gizi terbaru anak.', + style: AppTypography.body, + ), + const SizedBox(height: 12), + if (items.isEmpty) + const _SectionEmptyInfo( + message: + 'Preview rekomendasi belum tersedia. Silakan lengkapi pengukuran terbaru.', + ) + else + Column( + children: [ + CarouselSlider.builder( + itemCount: items.length, + options: CarouselOptions( + height: 364, + viewportFraction: 0.9, + enlargeCenterPage: true, + enlargeFactor: 0.18, + autoPlay: true, + autoPlayInterval: const Duration(seconds: 7), + enableInfiniteScroll: items.length > 1, + onPageChanged: (index, _) => onPageChanged(index), + ), + itemBuilder: (context, index, realIndex) { + final item = items[index]; + return _FoodPreviewCard( + item: item, + assetImage: _foodAssetByIndex(index), + ); + }, + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(items.length, (index) { + final isActive = index == activeIndex; + return AnimatedContainer( + duration: const Duration(milliseconds: 220), + margin: const EdgeInsets.symmetric(horizontal: 3), + height: 7, + width: isActive ? 18 : 7, + decoration: BoxDecoration( + color: isActive + ? _NutriColors.primary + : _NutriColors.primary.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(99), + ), + ); + }), + ), + ], + ), + ], + ) + .animate(delay: 220.ms) + .fadeIn(duration: 420.ms, curve: Curves.easeOut) + .slideY(begin: 0.03, end: 0, duration: 420.ms, curve: Curves.easeOut); + } +} + +class _CategoryChip extends StatelessWidget { + const _CategoryChip({ + required this.label, + required this.isActive, + required this.onTap, + }); + + final String label; + final bool isActive; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(999), + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: isActive ? _NutriColors.primary : Colors.white, + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: isActive ? _NutriColors.primary : SgColors.border, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Center( + child: Text( + label, + style: AppTypography.caption.copyWith( + color: isActive ? Colors.white : SgColors.textPrimary, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ); + } +} + +class _EducationCard extends StatelessWidget { + const _EducationCard({required this.item}); + + final _EducationItem item; + + @override + Widget build(BuildContext context) { + return HealthCard( + margin: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 24, + backgroundColor: const Color(0xFFE9F6F2), + child: Icon(item.icon, color: SgColors.primary), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + style: AppTypography.h3, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + item.description, + style: AppTypography.body, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _AgeGuideCard extends StatelessWidget { + const _AgeGuideCard({required this.guide, required this.isActive}); + + final _AgeGuide guide; + final bool isActive; + + @override + Widget build(BuildContext context) { + final card = AnimatedContainer( + duration: const Duration(milliseconds: 240), + curve: Curves.easeOut, + margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 2), + padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: SgColors.border), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isActive ? 0.10 : 0.05), + blurRadius: isActive ? 30 : 18, + offset: const Offset(0, 12), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: _NutriColors.secondary.withValues(alpha: 0.25), + borderRadius: BorderRadius.circular(999), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + guide.icon, + color: _NutriColors.primary, + size: 18, + ), + const SizedBox(width: 6), + Text( + guide.title, + style: AppTypography.h3.copyWith( + color: SgColors.textPrimary, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 8), + _InfoRow(label: 'Fokus', value: guide.focus), + _InfoRow(label: 'Makanan dianjurkan', value: guide.foodSuggestion), + _InfoRow(label: 'Tips singkat', value: guide.tip), + ], + ), + ); + + return card + .animate(target: isActive ? 1 : 0) + .scale( + begin: const Offset(0.96, 0.96), + end: const Offset(1, 1), + duration: 260.ms, + curve: Curves.easeOut, + ); + } +} + +const List _nutritionCategories = [ + 'Semua', + 'Stunting', + 'MPASI', + 'Protein', + 'Vitamin', + 'Gizi Seimbang', +]; + +class _LatestNutritionStatusCard extends StatelessWidget { + const _LatestNutritionStatusCard({ + required this.child, + required this.latest, + required this.onOpenHistory, + }); + + final MobileChildModel child; + final RiwayatItemModel latest; + final VoidCallback onOpenHistory; + + @override + Widget build(BuildContext context) { + final visual = nutritionStatusVisual(latest.statusGabungan); + return HealthCard( + color: const Color(0xFFEAF7F7), + borderColor: const Color(0xFFCBEAEA), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + child.nama, + style: AppTypography.h2, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + latest.statusGabungan, + style: AppTypography.h3, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Flexible( + child: StatusBadge( + text: visual.badgeLabel, + color: visual.color, + ), + ), + ], + ), + const SizedBox(height: 10), + Text( + 'Pengukuran: ${formatMeasurementDate(latest.tanggalUkur)}', + style: AppTypography.caption, + ), + const SizedBox(height: 12), + Text( + recommendationStatusExplanation(latest.statusGabungan), + style: AppTypography.body.copyWith(color: SgColors.textPrimary), + ), + const SizedBox(height: 12), + Text( + 'Saran: ${_suggestionForStatus(latest.statusGabungan)}', + style: AppTypography.body, + ), + const SizedBox(height: 16), + PrimaryButton( + label: 'Lihat Riwayat Lengkap', + icon: Icons.timeline_rounded, + onPressed: onOpenHistory, + ), + ], + ), + ) + .animate() + .fadeIn(duration: 420.ms, curve: Curves.easeOut) + .slideY(begin: 0.02, end: 0, duration: 420.ms, curve: Curves.easeOut); + } +} + +class _FoodPreviewCard extends StatelessWidget { + const _FoodPreviewCard({required this.item, required this.assetImage}); + + final RekomendasiModel item; + final String assetImage; + + @override + Widget build(BuildContext context) { + return HealthCard( + margin: EdgeInsets.zero, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + child: AspectRatio( + aspectRatio: 16 / 9, + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset( + assetImage, + fit: BoxFit.cover, + ), + Positioned( + left: 12, + top: 12, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.42), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + '${item.kalori} kkal', + style: AppTypography.caption.copyWith( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.menu, + style: AppTypography.h3, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text('${item.protein}g protein', style: AppTypography.caption), + const SizedBox(height: 10), + Text( + 'Mengapa cocok? ${item.alasan}', + style: AppTypography.body, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _SectionEmptyInfo extends StatelessWidget { + const _SectionEmptyInfo({required this.message, this.actionLabel, this.onAction}); + + final String message; + final String? actionLabel; + final VoidCallback? onAction; + + @override + Widget build(BuildContext context) { + return HealthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message, style: AppTypography.body), + if (actionLabel != null && onAction != null) ...[ + const SizedBox(height: 14), + PrimaryButton(label: actionLabel!, onPressed: onAction, isOutlined: true), + ], + ], + ), + ); + } +} + +class _InfoRow extends StatelessWidget { + const _InfoRow({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 132, + child: Text(label, style: AppTypography.caption), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: AppTypography.body.copyWith(color: SgColors.textPrimary), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } +} + +class _NutritionSkeleton extends StatelessWidget { + const _NutritionSkeleton(); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: const Color(0xFFE3EBE8), + highlightColor: const Color(0xFFF3F7F5), + child: ListView( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 28), + children: [ + Container( + width: double.infinity, + height: 140, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(28), + ), + ), + const SizedBox(height: 20), + Container(width: 130, height: 18, color: Colors.white), + const SizedBox(height: 12), + SizedBox( + height: 150, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (_, __) => Container( + width: 260, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(22), + ), + ), + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemCount: 3, + ), + ), + const SizedBox(height: 24), + for (var i = 0; i < 3; i++) ...[ + HealthCard( + child: Container( + height: 86, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + ), + ), + ), + const SizedBox(height: 12), + ], + ], + ), + ); + } +} + +class _ArticleCard extends StatelessWidget { + const _ArticleCard({ + required this.article, + required this.allArticles, + }); + + final NewsArticleModel article; + final List allArticles; + + @override + Widget build(BuildContext context) { + return HealthCard( + margin: const EdgeInsets.only(bottom: 12), + padding: EdgeInsets.zero, + onTap: () { + Navigator.of(context).push( + fadeRoute( + ArticleDetailScreen( + article: article, + related: allArticles, + ), + ), + ); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: 'article-${article.id}', + child: ClipRRect( + borderRadius: + const BorderRadius.horizontal(left: Radius.circular(20)), + child: SizedBox( + width: 104, + height: 96, + child: Image.asset( + _articleAssetByIndex(article.id), + fit: BoxFit.cover, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: _NutriColors.secondary.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + article.category, + style: AppTypography.caption.copyWith( + color: _NutriColors.primaryDark, + fontWeight: FontWeight.w700, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + article.title, + style: AppTypography.h3.copyWith( + color: SgColors.textPrimary, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + article.description, + style: AppTypography.body, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + Text( + 'Baca Selengkapnya', + style: AppTypography.caption.copyWith( + color: _NutriColors.primary, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _EducationItem { + const _EducationItem({ + required this.title, + required this.description, + required this.icon, + required this.categories, + }); + + final String title; + final String description; + final IconData icon; + final List categories; +} + +class _AgeGuide { + const _AgeGuide({ + required this.title, + required this.focus, + required this.foodSuggestion, + required this.tip, + required this.icon, + }); + + final String title; + final String focus; + final String foodSuggestion; + final String tip; + final IconData icon; +} + +const List<_EducationItem> _educationItems = [ + _EducationItem( + title: 'Cegah Stunting Sejak Dini', + description: + 'Pantau tinggi dan berat badan rutin serta penuhi protein hewani harian agar tumbuh kembang optimal.', + icon: Icons.height_rounded, + categories: ['Stunting', 'Protein', 'Gizi Seimbang'], + ), + _EducationItem( + title: 'MPASI Bertahap dan Tepat Tekstur', + description: + 'Mulai MPASI usia 6 bulan, tingkatkan tekstur bertahap, dan jaga frekuensi makan sesuai usia.', + icon: Icons.ramen_dining_rounded, + categories: ['MPASI', 'Gizi Seimbang'], + ), + _EducationItem( + title: 'Protein untuk Pertumbuhan Tinggi Badan', + description: + 'Protein hewani seperti telur, ikan, ayam, dan daging membantu pembentukan jaringan tubuh anak.', + icon: Icons.egg_alt_rounded, + categories: ['Protein', 'Stunting'], + ), + _EducationItem( + title: 'Vitamin dan Mineral Esensial', + description: + 'Lengkapi menu dengan sayur dan buah berwarna agar kebutuhan vitamin A, C, zat besi, dan zinc terpenuhi.', + icon: Icons.eco_rounded, + categories: ['Vitamin', 'Gizi Seimbang'], + ), +]; + +const List<_AgeGuide> _ageGuides = [ + _AgeGuide( + title: 'Usia 0-6 Bulan', + focus: 'ASI eksklusif sesuai kebutuhan bayi.', + foodSuggestion: 'ASI sebagai sumber nutrisi utama.', + tip: 'Susui lebih sering saat bayi menunjukkan tanda lapar.', + icon: Icons.child_friendly_rounded, + ), + _AgeGuide( + title: 'Usia 6-12 Bulan', + focus: 'MPASI bertahap dengan tekstur meningkat.', + foodSuggestion: 'Bubur tim, pure sayur, telur, ikan lembut.', + tip: 'Kenalkan tekstur dari halus ke kasar secara bertahap.', + icon: Icons.soup_kitchen_rounded, + ), + _AgeGuide( + title: 'Usia 1-3 Tahun', + focus: 'Variasi makanan dengan protein cukup.', + foodSuggestion: 'Nasi, lauk hewani, sayur, buah, susu.', + tip: 'Biasakan jam makan teratur dan camilan sehat.', + icon: Icons.set_meal_rounded, + ), + _AgeGuide( + title: 'Usia 3-5 Tahun', + focus: 'Dukung pertumbuhan tinggi badan dan daya tahan tubuh.', + foodSuggestion: 'Ikan, ayam, telur, tempe, sayur hijau, buah.', + tip: 'Tambahkan protein hewani dan ajak anak aktif bergerak.', + icon: Icons.directions_run_rounded, + ), +]; + +String _suggestionForStatus(String status) { + final value = status.toLowerCase(); + if (value.contains('stunting')) { + return 'Tambahkan protein hewani dan pantau tinggi badan rutin.'; + } + if (value.contains('kurang') || value.contains('underweight')) { + return 'Tingkatkan porsi energi-protein, termasuk camilan bergizi.'; + } + if (value.contains('obesitas') || value.contains('lebih')) { + return 'Atur porsi seimbang, kurangi gula tambahan, dan tingkatkan aktivitas fisik.'; + } + return 'Pertahankan pola makan gizi seimbang dan pemantauan berkala.'; +} + +String _imageForIndex(int index) { + const images = [ + 'https://images.unsplash.com/photo-1512058564366-18510be2db19?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1547592166-23ac45744acd?auto=format&fit=crop&w=900&q=80', + ]; + return images[index % images.length]; +} + +TextStyle _sectionLabelStyle() { + return AppTypography.caption.copyWith( + letterSpacing: 1, + fontWeight: FontWeight.w800, + color: _NutriColors.primaryDark, + ); +} + +class _NutriColors { + const _NutriColors._(); + + static const primary = Color(0xFF0B7A86); + static const primaryDark = Color(0xFF085B63); + static const secondary = Color(0xFF7FD6C2); + static const background = Color(0xFFF5F7F6); +} + +String _foodAssetByIndex(int index) { + const assets = [ + 'assets/image/onboarding_food.png', + 'assets/image/onboarding_monitoring.png', + 'assets/image/onboarding_consultation.png', + ]; + return assets[index % assets.length]; +} + +String _articleAssetByIndex(int index) { + const assets = [ + 'assets/image/onboarding_food.png', + 'assets/image/onboarding_monitoring.png', + 'assets/image/onboarding_consultation.png', + ]; + return assets[index % assets.length]; +} diff --git a/Android/flutter_app/lib/screens/onboarding_screen.dart b/Android/flutter_app/lib/screens/onboarding_screen.dart new file mode 100644 index 0000000..f15c832 --- /dev/null +++ b/Android/flutter_app/lib/screens/onboarding_screen.dart @@ -0,0 +1,428 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:bootstrap_icons/bootstrap_icons.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../app_design.dart'; +import 'auth_screen.dart'; + +class OnboardingScreen extends StatefulWidget { + const OnboardingScreen({super.key}); + + @override + State createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State { + final _controller = PageController(); + int _index = 0; + Timer? _autoSlideTimer; + + @override + void initState() { + super.initState(); + _autoSlideTimer = Timer.periodic(const Duration(seconds: 2), (_) { + if (!mounted || !_controller.hasClients) return; + final nextIndex = (_index + 1) % 3; + _controller.animateToPage( + nextIndex, + duration: const Duration(milliseconds: 420), + curve: Curves.easeOutCubic, + ); + }); + } + + @override + void dispose() { + _autoSlideTimer?.cancel(); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + const pages = [ + _OnboardingPage( + imagePath: 'assets/image/onboarding_monitoring.png', + title: 'Monitoring Pertumbuhan Si Kecil Secara Akurat', + subtitle: + 'Pantau perkembangan buah hati Anda dengan standar WHO Z-Score.', + floatingTags: [ + _FloatingTag( + label: 'Status Normal', + icon: BootstrapIcons.heart, + align: Alignment(0.92, -0.48), + ), + _FloatingTag( + label: 'Pertumbuhan +2.4 cm', + icon: LucideIcons.lineChart, + align: Alignment(-0.70, 0.24), + ), + _FloatingTag( + label: 'Anak Aktif', + icon: PhosphorIconsFill.baby, + align: Alignment(-0.75, -0.52), + ), + ], + emphasizedWord: 'Akurat', + ), + _OnboardingPage( + imagePath: 'assets/image/onboarding_food.png', + title: 'Rekomendasi Makanan Personal', + subtitle: + 'Dapatkan saran menu bergizi sesuai kebutuhan tumbuh kembang si kecil.', + floatingTags: [ + _FloatingTag( + label: 'Protein', + icon: PhosphorIconsFill.barbell, + align: Alignment(-0.74, -0.36), + ), + _FloatingTag( + label: 'Menu MPASI', + icon: BootstrapIcons.egg, + align: Alignment(0.76, -0.70), + ), + _FloatingTag( + label: 'Vitamin', + icon: LucideIcons.apple, + align: Alignment(-0.78, 0.28), + ), + ], + emphasizedWord: 'Personal', + ), + _OnboardingPage( + imagePath: 'assets/image/onboarding_consultation.png', + title: 'Konsultasi Ahli Gizi', + subtitle: + 'Diskusikan tumbuh kembang anak langsung dengan ahli gizi terpercaya.', + floatingTags: [ + _FloatingTag( + label: 'Terverifikasi', + icon: PhosphorIconsFill.shieldCheck, + align: Alignment(0.72, 0.28), + ), + _FloatingTag( + label: 'Chat Konsultasi', + icon: LucideIcons.messageCircle, + align: Alignment(0.86, -0.42), + ), + _FloatingTag( + label: 'Ahli Gizi', + icon: PhosphorIconsFill.stethoscope, + align: Alignment(-0.74, 0.40), + ), + ], + emphasizedWord: 'Ahli Gizi', + ), + ]; + + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFF7FCFB), Color(0xFFF1F7F6), SgColors.background], + ), + ), + child: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 28), + child: Column( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.72, + child: PageView( + controller: _controller, + onPageChanged: (value) => setState(() => _index = value), + children: pages, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + pages.length, + (i) => AnimatedContainer( + duration: const Duration(milliseconds: 260), + width: i == _index ? 32 : 10, + height: 10, + margin: const EdgeInsets.symmetric(horizontal: 5), + decoration: BoxDecoration( + gradient: i == _index + ? const LinearGradient( + colors: [SgColors.primaryDark, SgColors.primary], + ) + : null, + color: i == _index ? null : const Color(0xFFCAE8E2), + borderRadius: BorderRadius.circular(99), + ), + ), + ), + ), + const SizedBox(height: 20), + _AnimatedCtaButton( + label: 'Mulai Sekarang', + onPressed: () => _enterApp(context), + ), + const SizedBox(height: 14), + Wrap( + alignment: WrapAlignment.center, + children: [ + Text( + 'Sudah punya akun? ', + style: AppTypography.body.copyWith( + color: const Color(0xFF62707B), + ), + ), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => _enterApp(context), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: Text( + 'Masuk di sini', + style: AppTypography.body.copyWith( + color: SgColors.primary, + fontWeight: FontWeight.w700, + decoration: TextDecoration.underline, + decorationColor: SgColors.primary, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } + + void _enterApp(BuildContext context) { + Navigator.of(context).pushReplacement(fadeRoute(const AuthScreen())); + } +} + +class _OnboardingPage extends StatelessWidget { + const _OnboardingPage({ + required this.imagePath, + required this.title, + required this.subtitle, + required this.floatingTags, + this.emphasizedWord, + }); + + final String imagePath; + final String title; + final String subtitle; + final List<_FloatingTag> floatingTags; + final String? emphasizedWord; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + const SizedBox(height: 20), + _HeroImageWithBadges( + imagePath: imagePath, + floatingTags: const [], + ), + const SizedBox(height: 34), + Text.rich( + _buildTitleSpan(), + textAlign: TextAlign.center, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ).animate().fadeIn(duration: 380.ms).slideY(begin: 0.2, end: 0), + const SizedBox(height: 14), + Text( + subtitle, + textAlign: TextAlign.center, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: AppTypography.body.copyWith( + fontSize: 16, + color: const Color(0xFF5A6875), + height: 1.5, + ), + ).animate().fadeIn(delay: 120.ms, duration: 430.ms), + const SizedBox(height: 18), + ], + ), + ); + } + + TextSpan _buildTitleSpan() { + if (emphasizedWord == null || !title.contains(emphasizedWord!)) { + return TextSpan( + text: title, + style: AppTypography.h1.copyWith(fontSize: 44, height: 1.15), + ); + } + + final word = emphasizedWord!; + final split = title.split(word); + return TextSpan( + style: AppTypography.h1.copyWith(fontSize: 44, height: 1.15), + children: [ + TextSpan(text: split.first), + TextSpan( + text: word, + style: AppTypography.h1.copyWith( + fontSize: 44, + color: SgColors.primaryDark, + fontStyle: FontStyle.italic, + ), + ), + if (split.length > 1) TextSpan(text: split.last), + ], + ); + } +} + +class _HeroImageWithBadges extends StatelessWidget { + const _HeroImageWithBadges({ + required this.imagePath, + required this.floatingTags, + }); + + final String imagePath; + final List<_FloatingTag> floatingTags; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.18, + child: ClipRRect( + borderRadius: BorderRadius.circular(32), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.10), + blurRadius: 24, + offset: const Offset(0, 14), + ), + ], + ), + child: Image.asset( + imagePath, + fit: BoxFit.cover, + filterQuality: FilterQuality.high, + errorBuilder: (_, __, ___) => Container( + color: const Color(0xFFE7F4F1), + alignment: Alignment.center, + child: const Icon( + Icons.broken_image_outlined, + color: SgColors.primary, + size: 42, + ), + ), + ), + ), + ), + ); + } +} + +class _AnimatedCtaButton extends StatefulWidget { + const _AnimatedCtaButton({required this.label, required this.onPressed}); + + final String label; + final VoidCallback onPressed; + + @override + State<_AnimatedCtaButton> createState() => _AnimatedCtaButtonState(); +} + +class _AnimatedCtaButtonState extends State<_AnimatedCtaButton> { + bool _pressed = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (_) => setState(() => _pressed = true), + onTapCancel: () => setState(() => _pressed = false), + onTapUp: (_) => setState(() => _pressed = false), + child: AnimatedScale( + scale: _pressed ? 0.97 : 1, + duration: const Duration(milliseconds: 160), + curve: Curves.easeOut, + child: Container( + width: double.infinity, + height: 64, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF147D89), Color(0xFF2BA6A7)], + ), + borderRadius: BorderRadius.circular(999), + boxShadow: [ + BoxShadow( + color: const Color(0xFF137B87).withValues(alpha: 0.26), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(999), + onTap: widget.onPressed, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: Row( + children: [ + Expanded( + child: Text( + widget.label, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.h2.copyWith(color: Colors.white), + ), + ), + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.20), + ), + child: const Icon( + LucideIcons.arrowRight, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} + +class _FloatingTag { + const _FloatingTag({ + required this.label, + required this.icon, + required this.align, + }); + + final String label; + final IconData icon; + final Alignment align; +} diff --git a/Android/flutter_app/lib/screens/profile_screen.dart b/Android/flutter_app/lib/screens/profile_screen.dart new file mode 100644 index 0000000..5f45558 --- /dev/null +++ b/Android/flutter_app/lib/screens/profile_screen.dart @@ -0,0 +1,1091 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:shimmer/shimmer.dart'; + + +import '../app_design.dart'; +import '../app_state.dart'; +import '../models/mobile_child_model.dart'; +import '../services/api_service.dart'; +import '../utils/nutrition_display_utils.dart'; +import 'add_child_screen.dart'; +import 'account_info_screen.dart'; +import 'auth_screen.dart'; +import 'help_screen.dart'; +import 'security_screen.dart'; + +class ProfileScreen extends StatefulWidget { + const ProfileScreen({super.key}); + + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + final _api = ApiService(); + final _state = SgiziAppState.instance; + + late Future _future; + + @override + void initState() { + super.initState(); + _state.addListener(_onStateChanged); + _future = _load(); + } + + @override + void dispose() { + _state.removeListener(_onStateChanged); + super.dispose(); + } + + void _onStateChanged() { + if (!mounted) return; + setState(() {}); + } + + Future _load() async { + if (!_state.isAuthenticated) return; + final results = await Future.wait([ + _api.getChildren(), + _api.getProfile(), + ]); + final children = results[0] as List; + final profile = results[1] as Map; + _state.setChildren(children); + _state.setProfileData(profile); + } + + void _retry() => setState(() => _future = _load()); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7F6), + body: SafeArea( + child: FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const _ProfileSkeleton(); + } + if (snapshot.hasError) { + return _ProfileError(onRetry: _retry); + } + final profile = _state.profileData; + final name = (profile?['name'] as String? ?? 'Siti Aminah').trim(); + final phone = (profile?['phone'] as String? ?? '-').trim(); + final role = (profile?['role'] as String? ?? 'orang_tua').trim(); + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 14, 20, 28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ProfileHeaderCard( + name: name.isEmpty ? 'Siti Aminah' : name, + subtitle: _profileRoleToLabel(role, phone), + onEditTap: () async { + await Navigator.of(context).push( + fadeRoute(const AccountInfoScreen()), + ); + if (!mounted) return; + _retry(); + }, + ), + const SizedBox(height: 18), + _ChildrenSection( + children: _state.children, + activeChildId: _state.activeChildId, + onAdd: () => Navigator.of(context).push( + fadeRoute(const AddChildScreen()), + ), + onSelect: (id) => _state.setActiveChild(id), + ), + const SizedBox(height: 22), + Text( + 'Pengaturan', + style: AppTypography.h2.copyWith(color: SgColors.textPrimary), + ), + const SizedBox(height: 12), + _SettingsCard( + onTapAccount: () async { + await Navigator.of(context).push( + fadeRoute(const AccountInfoScreen()), + ); + if (!mounted) return; + _retry(); + }, + onTapPrivacy: () => Navigator.of(context).push( + fadeRoute(const SecurityScreen()), + ), + onTapHelp: () => Navigator.of(context).push( + fadeRoute(const HelpScreen()), + ), + ), + const SizedBox(height: 18), + _LogoutCard( + onLogout: () async { + final ok = await _confirmLogout(context); + if (ok != true) return; + _state.logout(); + if (!context.mounted) return; + Navigator.of(context).pushAndRemoveUntil( + fadeRoute(const AuthScreen()), + (_) => false, + ); + }, + ), + const SizedBox(height: 18), + Text( + 'S-GIZI • PREMIUM HEALTHCARE UI', + textAlign: TextAlign.center, + style: AppTypography.caption.copyWith( + letterSpacing: 1, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ) + .animate() + .fadeIn(duration: 260.ms, curve: Curves.easeOut) + .slideY(begin: 0.02, end: 0, duration: 260.ms, curve: Curves.easeOut); + }, + ), + ), + ); + } +} + +class _ProfileHeaderCard extends StatelessWidget { + const _ProfileHeaderCard({ + required this.name, + required this.subtitle, + required this.onEditTap, + }); + + final String name; + final String subtitle; + final VoidCallback onEditTap; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF085B63), Color(0xFF0B7A86)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(28), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.10), + blurRadius: 26, + offset: const Offset(0, 14), + ), + ], + ), + child: Stack( + children: [ + Positioned( + right: -30, + top: -40, + child: Container( + width: 140, + height: 140, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.08), + ), + ), + ), + Positioned( + left: -50, + bottom: -50, + child: Container( + width: 170, + height: 170, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.06), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(18, 18, 18, 18), + child: Row( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 74, + height: 74, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.16), + border: Border.all( + color: Colors.white.withValues(alpha: 0.22), + ), + ), + child: ClipOval( + child: Padding( + padding: const EdgeInsets.all(10), + child: Image.asset( + 'assets/image/onboarding_consultation.png', + fit: BoxFit.cover, + ), + ), + ), + ) + .animate(onPlay: (c) => c.repeat(reverse: true)) + .moveY(begin: 0, end: -2, duration: 1800.ms), + Positioned( + right: 2, + bottom: 2, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: const Color(0xFF34C759), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: AppTypography.h2.copyWith( + color: Colors.white, + fontSize: 20, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + subtitle, + style: AppTypography.body.copyWith( + color: Colors.white.withValues(alpha: 0.9), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.14), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: Colors.white.withValues(alpha: 0.18), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + PhosphorIconsRegular.sealCheck, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 6), + Text( + 'Akun Terverifikasi', + style: AppTypography.caption.copyWith( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(width: 10), + _IconPillButton( + icon: LucideIcons.pencil, + onTap: onEditTap, + ), + ], + ), + ), + ], + ), + ) + .animate() + .fadeIn(duration: 320.ms, curve: Curves.easeOut) + .slideY(begin: -0.03, end: 0, duration: 320.ms, curve: Curves.easeOut); + } +} + +class _IconPillButton extends StatelessWidget { + const _IconPillButton({required this.icon, required this.onTap}); + + final IconData icon; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.white.withValues(alpha: 0.14), + borderRadius: BorderRadius.circular(999), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(999), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999), + border: Border.all(color: Colors.white.withValues(alpha: 0.18)), + ), + child: Icon(icon, color: Colors.white, size: 18), + ), + ), + ); + } +} + +class _ChildrenSection extends StatelessWidget { + const _ChildrenSection({ + required this.children, + required this.activeChildId, + required this.onAdd, + required this.onSelect, + }); + + final List children; + final int? activeChildId; + final VoidCallback onAdd; + final ValueChanged onSelect; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + 'Daftar Anak', + style: AppTypography.h2.copyWith(color: SgColors.textPrimary), + overflow: TextOverflow.ellipsis, + ), + ), + TextButton.icon( + onPressed: onAdd, + icon: const Icon(LucideIcons.plus), + label: const Text('Tambah'), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF0B7A86), + ), + ), + ], + ), + const SizedBox(height: 10), + if (children.isEmpty) + HealthCard( + child: Row( + children: [ + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + color: const Color(0xFF7FD6C2).withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + LucideIcons.baby, + color: Color(0xFF0B7A86), + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Belum ada data anak. Tambahkan anak untuk mulai memantau status gizi.', + style: AppTypography.body, + ), + ), + ], + ), + ) + else + SizedBox( + height: 152, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: children.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final c = children[index]; + final active = c.id == activeChildId; + return _ChildCard( + child: c, + active: active, + onTap: () => onSelect(c.id), + ); + }, + ), + ), + ], + ) + .animate() + .fadeIn(duration: 320.ms, curve: Curves.easeOut) + .slideY(begin: 0.02, end: 0, duration: 320.ms, curve: Curves.easeOut); + } +} + +class _ChildCard extends StatelessWidget { + const _ChildCard({ + required this.child, + required this.active, + required this.onTap, + }); + + final MobileChildModel child; + final bool active; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final status = (child.latestStatus ?? '').trim().isEmpty + ? 'Status: Belum ada' + : 'Status: ${child.latestStatus}'; + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(22), + child: AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOut, + width: 260, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: active ? const Color(0xFF0B7A86) : const Color(0xFFE2E8E6), + width: active ? 1.8 : 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: active ? 0.10 : 0.06), + blurRadius: active ? 26 : 18, + offset: const Offset(0, 12), + ), + ], + ), + child: Row( + children: [ + Stack( + children: [ + ChildAvatar( + name: child.nama, + gender: child.jenisKelamin, + photoUrl: child.photoUrl, + radius: 28, + ), + if (active) + Positioned( + right: 0, + top: 0, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: const Color(0xFF0B7A86), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + child.nama, + style: AppTypography.h3.copyWith(color: SgColors.textPrimary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + '${child.jenisKelamin == 'P' ? 'Perempuan' : 'Laki-laki'} • ${formatAgeFromBirthDate(child.tanggalLahir)}', + style: AppTypography.caption, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: active + ? const Color(0xFF7FD6C2).withValues(alpha: 0.22) + : const Color(0xFFF1F5F4), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + status, + style: AppTypography.caption.copyWith( + color: active ? const Color(0xFF085B63) : SgColors.textSecondary, + fontWeight: FontWeight.w700, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const SizedBox(width: 6), + PopupMenuButton( + onSelected: (value) async { + if (value == 'edit') { + await _showEditChildDialog(context, child); + } + if (value == 'delete') { + await _showDeleteChildDialog(context, child); + } + }, + itemBuilder: (context) => const [ + PopupMenuItem(value: 'edit', child: Text('Edit')), + PopupMenuItem(value: 'delete', child: Text('Hapus')), + ], + icon: Icon( + LucideIcons.moreVertical, + color: active ? const Color(0xFF0B7A86) : SgColors.textSecondary, + ), + ), + ], + ), + ), + ).animate().scale( + begin: const Offset(0.99, 0.99), + end: const Offset(1, 1), + duration: 220.ms, + ); + } +} + +class _SettingsCard extends StatelessWidget { + const _SettingsCard({ + required this.onTapAccount, + required this.onTapPrivacy, + required this.onTapHelp, + }); + + final VoidCallback onTapAccount; + final VoidCallback onTapPrivacy; + final VoidCallback onTapHelp; + + @override + Widget build(BuildContext context) { + return HealthCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + _SettingsTile( + icon: LucideIcons.user, + color: const Color(0xFF0B7A86), + title: 'Informasi Akun', + subtitle: 'Edit nama, foto, dan nomor telepon', + onTap: onTapAccount, + ), + const Divider(height: 1, color: SgColors.border), + _SettingsTile( + icon: LucideIcons.shield, + color: const Color(0xFF34A853), + title: 'Privasi & Keamanan', + subtitle: 'Kebijakan privasi dan keamanan akun', + onTap: onTapPrivacy, + ), + const Divider(height: 1, color: SgColors.border), + _SettingsTile( + icon: LucideIcons.messageCircle, + color: const Color(0xFF3B82F6), + title: 'Bantuan', + subtitle: 'FAQ, pusat bantuan, hubungi admin', + onTap: onTapHelp, + ), + ], + ), + ) + .animate() + .fadeIn(duration: 280.ms, curve: Curves.easeOut) + .slideY(begin: 0.02, end: 0, duration: 280.ms, curve: Curves.easeOut); + } +} + +class _SettingsTile extends StatelessWidget { + const _SettingsTile({ + required this.icon, + required this.color, + required this.title, + required this.subtitle, + required this.onTap, + }); + + final IconData icon; + final Color color; + final String title; + final String subtitle; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + _IconBadge(icon: icon, color: color), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppTypography.h3), + const SizedBox(height: 2), + Text( + subtitle, + style: AppTypography.caption, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const Icon( + LucideIcons.chevronRight, + color: SgColors.textSecondary, + ), + ], + ), + ), + ).animate().scale( + begin: const Offset(1, 1), + end: const Offset(0.99, 0.99), + duration: 140.ms, + ); + } +} + +class _IconBadge extends StatelessWidget { + const _IconBadge({required this.icon, required this.color}); + final IconData icon; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.14), + borderRadius: BorderRadius.circular(16), + ), + child: Icon(icon, color: color, size: 20), + ); + } +} + +class _LogoutCard extends StatelessWidget { + const _LogoutCard({required this.onLogout}); + + final VoidCallback onLogout; + + @override + Widget build(BuildContext context) { + return HealthCard( + color: const Color(0xFFFFF5F5), + borderColor: const Color(0xFFFFCDD2), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: const Color(0xFFE53935).withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + LucideIcons.logOut, + color: Color(0xFFE53935), + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Keluar Aplikasi', + style: AppTypography.h3, + overflow: TextOverflow.ellipsis, + ), + ), + TextButton( + onPressed: onLogout, + style: TextButton.styleFrom(foregroundColor: const Color(0xFFE53935)), + child: const Text('Keluar'), + ), + ], + ), + ) + .animate() + .fadeIn(duration: 240.ms, curve: Curves.easeOut) + .slideY(begin: 0.02, end: 0, duration: 240.ms, curve: Curves.easeOut); + } +} + +class _ProfileSkeleton extends StatelessWidget { + const _ProfileSkeleton(); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: const Color(0xFFE8EEEC), + highlightColor: const Color(0xFFF7FAF9), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 14, 20, 28), + child: Column( + children: [ + Container( + height: 124, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(28), + ), + ), + const SizedBox(height: 18), + Container( + height: 168, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(22), + ), + ), + const SizedBox(height: 22), + Container( + height: 260, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(22), + ), + ), + const SizedBox(height: 18), + Container( + height: 74, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(22), + ), + ), + ], + ), + ), + ); + } +} + +class _ProfileError extends StatelessWidget { + const _ProfileError({required this.onRetry}); + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + return ErrorState( + message: 'Profil tidak dapat dimuat. Periksa koneksi lalu coba lagi.', + onRetry: onRetry, + ); + } +} + +Future _confirmLogout(BuildContext context) { + return showGeneralDialog( + context: context, + barrierLabel: 'logout', + barrierDismissible: true, + barrierColor: Colors.black.withValues(alpha: 0.38), + pageBuilder: (context, _, __) { + return Center( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Padding( + padding: const EdgeInsets.all(20), + child: Material( + color: Colors.transparent, + child: Container( + width: double.infinity, + constraints: const BoxConstraints(maxWidth: 420), + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(22), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.12), + blurRadius: 30, + offset: const Offset(0, 18), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: const [ + Icon( + LucideIcons.logOut, + color: Color(0xFFE53935), + ), + SizedBox(width: 10), + Expanded( + child: Text( + 'Konfirmasi Keluar', + style: AppTypography.h2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 10), + const Text( + 'Apakah Anda yakin ingin keluar?', + style: AppTypography.body, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: SizedBox( + height: 52, + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(false), + style: OutlinedButton.styleFrom( + foregroundColor: SgColors.textPrimary, + side: const BorderSide(color: SgColors.border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: const Text( + 'Batal', + style: TextStyle(fontWeight: FontWeight.w700), + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: SizedBox( + height: 52, + child: FilledButton( + onPressed: () => Navigator.of(context).pop(true), + style: FilledButton.styleFrom( + backgroundColor: const Color(0xFFE53935), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: const Text( + 'Keluar', + style: TextStyle(fontWeight: FontWeight.w800), + ), + ), + ), + ), + ], + ), + ], + ), + ).animate().fadeIn(duration: 180.ms).scale( + begin: const Offset(0.98, 0.98), + end: const Offset(1, 1), + duration: 180.ms, + curve: Curves.easeOut, + ), + ), + ), + ), + ); + }, + transitionDuration: const Duration(milliseconds: 180), + transitionBuilder: (context, animation, _, child) { + return FadeTransition( + opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), + child: child, + ); + }, + ); +} + +void _toast(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); +} + +String _profileRoleToLabel(String role, String fallbackPhone) { + final v = role.toLowerCase(); + if (v.contains('ortu') || v.contains('orang_tua') || v.contains('parent')) { + return 'Orang Tua Aktif'; + } + if (v.contains('admin')) { + return 'Admin'; + } + return fallbackPhone.isEmpty ? 'Akun Aktif' : fallbackPhone; +} + +Future _showEditChildDialog(BuildContext context, MobileChildModel child) async { + final api = ApiService(); + final nameController = TextEditingController(text: child.nama); + String gender = child.jenisKelamin; + DateTime? birthDate = DateTime.tryParse(child.tanggalLahir); + + await showDialog( + context: context, + builder: (dialogContext) { + return StatefulBuilder( + builder: (context, setLocal) { + return AlertDialog( + title: const Text('Edit Data Anak'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameController, + decoration: const InputDecoration(labelText: 'Nama Anak'), + ), + const SizedBox(height: 10), + DropdownButtonFormField( + value: gender, + items: const [ + DropdownMenuItem(value: 'L', child: Text('Laki-laki')), + DropdownMenuItem(value: 'P', child: Text('Perempuan')), + ], + onChanged: (v) { + if (v == null) return; + setLocal(() => gender = v); + }, + decoration: const InputDecoration(labelText: 'Jenis Kelamin'), + ), + const SizedBox(height: 10), + OutlinedButton( + onPressed: () async { + final picked = await showDatePicker( + context: context, + initialDate: birthDate ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now(), + ); + if (picked != null) { + setLocal(() => birthDate = picked); + } + }, + child: Text( + birthDate == null + ? 'Pilih Tanggal Lahir' + : 'Tanggal: ${birthDate!.toIso8601String().split('T').first}', + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Batal'), + ), + FilledButton( + onPressed: () async { + try { + await api.updateChild( + childId: child.id, + data: { + 'nama': nameController.text.trim(), + 'tanggal_lahir': (birthDate ?? DateTime.now()) + .toIso8601String() + .split('T') + .first, + 'jenis_kelamin': gender, + }, + ); + final children = await api.getChildren(); + SgiziAppState.instance.setChildren(children); + if (dialogContext.mounted) Navigator.of(dialogContext).pop(); + } catch (e) { + if (!dialogContext.mounted) return; + ScaffoldMessenger.of(dialogContext).showSnackBar( + SnackBar(content: Text('Gagal edit anak: $e')), + ); + } + }, + child: const Text('Simpan'), + ), + ], + ); + }, + ); + }, + ); +} + +Future _showDeleteChildDialog(BuildContext context, MobileChildModel child) async { + final api = ApiService(); + final confirm = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Hapus Anak'), + content: Text('Yakin ingin menghapus data ${child.nama}?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: const Text('Batal'), + ), + FilledButton( + onPressed: () => Navigator.of(dialogContext).pop(true), + style: FilledButton.styleFrom(backgroundColor: const Color(0xFFE53935)), + child: const Text('Hapus'), + ), + ], + ), + ); + if (confirm != true) return; + + try { + await api.deleteChild(childId: child.id); + final children = await api.getChildren(); + SgiziAppState.instance.setChildren(children); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Gagal hapus anak: $e')), + ); + } +} diff --git a/Android/flutter_app/lib/screens/recommendation_screen.dart b/Android/flutter_app/lib/screens/recommendation_screen.dart new file mode 100644 index 0000000..d6e4da2 --- /dev/null +++ b/Android/flutter_app/lib/screens/recommendation_screen.dart @@ -0,0 +1,537 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../models/api_result_model.dart'; +import '../models/recommendation_response_model.dart'; +import '../services/api_service.dart'; +import '../utils/nutrition_display_utils.dart'; + +class RecommendationScreen extends StatefulWidget { + const RecommendationScreen({ + super.key, + this.status, + this.childId, + this.riwayatId, + this.childName, + this.measuredAt, + }); + + final String? status; + final int? childId; + final int? riwayatId; + final String? childName; + final String? measuredAt; + + @override + State createState() => _RecommendationScreenState(); +} + +class _RecommendationScreenState extends State { + final ApiService _apiService = ApiService(); + late Future<_RecommendationViewData> _future; + + @override + void initState() { + super.initState(); + _future = _load(); + } + + Future<_RecommendationViewData> _load() async { + final requestedStatus = widget.status?.trim() ?? ''; + if (requestedStatus.isEmpty && + widget.childId == null && + widget.riwayatId == null) { + return _RecommendationViewData.empty( + childName: widget.childName, + measuredAt: widget.measuredAt, + ); + } + + final response = await _apiService.getRecommendations( + status: requestedStatus, + childId: widget.childId, + riwayatId: widget.riwayatId, + ); + + return _RecommendationViewData.fromResponse( + response, + fallbackChildName: widget.childName, + fallbackMeasuredAt: widget.measuredAt, + ); + } + + void _retry() { + setState(() => _future = _load()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SgColors.background, + appBar: AppBar( + title: const Row( + children: [ + AppLogo(size: 38), + SizedBox(width: 12), + Expanded(child: Text('Rekomendasi Makanan')), + ], + ), + ), + body: FutureBuilder<_RecommendationViewData>( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const _RecommendationSkeleton(); + } + + if (snapshot.hasError) { + return ErrorState( + message: + 'Rekomendasi makanan belum dapat dimuat. Coba lagi dalam beberapa saat.', + onRetry: _retry, + ); + } + + final data = snapshot.data!; + final normalized = normalizeStatus(data.status ?? 'Normal'); + final visual = nutritionStatusVisual(data.status ?? 'Normal'); + + if ((data.status == null || data.status!.isEmpty) && + data.items.isEmpty) { + return EmptyState( + title: 'Belum Ada Pengukuran', + message: + 'Tambahkan pengukuran terbaru agar rekomendasi makanan bisa disesuaikan dengan status gizi terakhir.', + actionLabel: 'Muat Ulang', + onAction: _retry, + ); + } + + if (data.items.isEmpty) { + return EmptyState( + title: 'Rekomendasi Belum Tersedia', + message: + 'Belum ada menu yang terhubung ke status ${data.status}. Tambahkan data makanan di server lalu coba lagi.', + actionLabel: 'Muat Ulang', + onAction: _retry, + ); + } + + return ListView( + padding: const EdgeInsets.fromLTRB(24, 12, 24, 28), + children: [ + HealthCard( + color: const Color(0xFFEAF7F7), + borderColor: const Color(0xFFCBEAEA), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'STATUS TERAKHIR', + style: AppTypography.caption.copyWith( + color: SgColors.primary, + fontWeight: FontWeight.w800, + letterSpacing: 1.1, + ), + ), + const SizedBox(height: 10), + Text( + data.status ?? '-', + style: AppTypography.h2, + ), + const SizedBox(height: 10), + Wrap( + spacing: 10, + runSpacing: 10, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + StatusBadge( + text: normalized.primaryCategory, + color: visual.color, + ), + ...normalized.categories + .where((category) => category != normalized.primaryCategory) + .map( + (category) => StatusBadge( + text: category, + color: SgColors.primaryDark, + ), + ), + ], + ), + if (data.measuredAt != null && data.measuredAt!.isNotEmpty) ...[ + const SizedBox(height: 12), + Text( + 'Tanggal Pengukuran: ${formatMeasurementDate(data.measuredAt!)}', + style: AppTypography.body.copyWith( + color: SgColors.textPrimary, + ), + ), + ], + if (data.childName != null && data.childName!.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + 'Nama Anak: ${data.childName!}', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.body.copyWith( + color: SgColors.textPrimary, + ), + ), + ], + const SizedBox(height: 12), + Text( + recommendationStatusExplanation(data.status ?? 'Normal'), + style: AppTypography.body, + ), + const SizedBox(height: 8), + Text(normalized.focusSummary, style: AppTypography.body), + ], + ), + ), + const SizedBox(height: 20), + Text( + 'MENAMPILKAN ${data.items.length} MENU', + style: AppTypography.caption.copyWith( + letterSpacing: 1.2, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 20), + ...data.items.asMap().entries.map( + (entry) => _FoodCard( + item: entry.value, + imageUrl: _imageForStatus(data.status ?? 'Normal', entry.key), + ), + ), + ], + ); + }, + ), + ); + } +} + +class _RecommendationViewData { + const _RecommendationViewData({ + required this.items, + required this.status, + required this.childName, + required this.measuredAt, + }); + + factory _RecommendationViewData.fromResponse( + RecommendationResponseModel response, { + String? fallbackChildName, + String? fallbackMeasuredAt, + }) { + return _RecommendationViewData( + items: response.items, + status: response.resolvedStatus, + childName: + (response.measurement?.childName.trim().isNotEmpty ?? false) + ? response.measurement?.childName + : fallbackChildName, + measuredAt: response.measurement?.tanggalUkur ?? fallbackMeasuredAt, + ); + } + + factory _RecommendationViewData.empty({ + String? childName, + String? measuredAt, + }) { + return _RecommendationViewData( + items: const [], + status: null, + childName: childName, + measuredAt: measuredAt, + ); + } + + final List items; + final String? status; + final String? childName; + final String? measuredAt; +} + +class _FoodCard extends StatelessWidget { + const _FoodCard({required this.item, required this.imageUrl}); + + final RekomendasiModel item; + final String imageUrl; + + @override + Widget build(BuildContext context) { + return HealthCard( + margin: const EdgeInsets.only(bottom: 20), + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + child: AspectRatio( + aspectRatio: 16 / 8, + child: Stack( + fit: StackFit.expand, + children: [ + Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: const Color(0xFFDDEFE8), + child: const Icon( + Icons.restaurant_rounded, + size: 44, + color: SgColors.primary, + ), + ), + ), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.05), + Colors.black.withValues(alpha: 0.46), + ], + ), + ), + ), + Positioned( + left: 16, + top: 16, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.42), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + '${item.kalori} KKAL', + style: AppTypography.caption.copyWith( + color: Colors.white, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + Positioned( + left: 16, + right: 16, + bottom: 16, + child: Text( + item.menu, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.h2.copyWith(color: Colors.white), + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 12, + runSpacing: 10, + children: [ + _NutrientChip( + icon: Icons.local_fire_department_outlined, + label: '${item.kalori} kkal', + ), + _NutrientChip( + icon: Icons.egg_alt_outlined, + label: '${item.protein} g protein', + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFEFF9F8), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: const Color(0xFFD8F0ED)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 8, + height: 8, + margin: const EdgeInsets.only(top: 5), + decoration: const BoxDecoration( + color: SgColors.primary, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'MENGAPA COCOK?', + style: AppTypography.caption.copyWith( + color: SgColors.primary, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 6), + Text(item.alasan, style: AppTypography.body), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _NutrientChip extends StatelessWidget { + const _NutrientChip({required this.icon, required this.label}); + + final IconData icon; + final String label; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: const Color(0xFFF7FAF9), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: SgColors.border), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 18, color: SgColors.primaryDark), + const SizedBox(width: 8), + Flexible( + child: Text( + label, + overflow: TextOverflow.ellipsis, + style: AppTypography.caption.copyWith( + color: SgColors.textPrimary, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ); + } +} + +class _RecommendationSkeleton extends StatelessWidget { + const _RecommendationSkeleton(); + + @override + Widget build(BuildContext context) { + return ListView( + padding: const EdgeInsets.all(24), + children: [ + HealthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 180, height: 12, color: const Color(0xFFE9EEEC)), + const SizedBox(height: 12), + Container(width: 140, height: 28, color: const Color(0xFFE9EEEC)), + const SizedBox(height: 12), + Container( + width: double.infinity, + height: 12, + color: const Color(0xFFE9EEEC), + ), + ], + ), + ), + const SizedBox(height: 20), + for (var i = 0; i < 2; i++) ...[ + HealthCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + Container(height: 140, color: const Color(0xFFE9EEEC)), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Container( + width: double.infinity, + height: 12, + color: const Color(0xFFE9EEEC), + ), + const SizedBox(height: 10), + Container( + width: 180, + height: 12, + color: const Color(0xFFE9EEEC), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + ], + ); + } +} + +String _imageForStatus(String status, int index) { + final normalized = normalizeStatus(status); + if (normalized.hasStunting) { + return _stuntingImages[index % _stuntingImages.length]; + } + if (normalized.hasUnderweight || normalized.hasWasting) { + return _underweightImages[index % _underweightImages.length]; + } + if (normalized.hasObesitas) { + return _balancedImages[index % _balancedImages.length]; + } + return _normalImages[index % _normalImages.length]; +} + +const _stuntingImages = [ + 'https://images.unsplash.com/photo-1515003197210-e0cd71810b5f?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?auto=format&fit=crop&w=900&q=80', +]; + +const _underweightImages = [ + 'https://images.unsplash.com/photo-1511690743698-d9d85f2fbf38?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1467003909585-2f8a72700288?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1482049016688-2d3e1b311543?auto=format&fit=crop&w=900&q=80', +]; + +const _balancedImages = [ + 'https://images.unsplash.com/photo-1498837167922-ddd27525d352?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1547592180-85f173990554?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?auto=format&fit=crop&w=900&q=80', +]; + +const _normalImages = [ + 'https://images.unsplash.com/photo-1547592166-23ac45744acd?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1505253716362-afaea1d3d1af?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1512058564366-18510be2db19?auto=format&fit=crop&w=900&q=80', +]; diff --git a/Android/flutter_app/lib/screens/result_screen.dart b/Android/flutter_app/lib/screens/result_screen.dart new file mode 100644 index 0000000..f30b64d --- /dev/null +++ b/Android/flutter_app/lib/screens/result_screen.dart @@ -0,0 +1,302 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../models/api_result_model.dart'; +import '../utils/nutrition_display_utils.dart'; +import 'consultation_chat_screen.dart'; +import 'recommendation_screen.dart'; + +class ResultScreen extends StatefulWidget { + const ResultScreen({super.key, required this.result}); + + final ApiResultModel result; + + @override + State createState() => _ResultScreenState(); +} + +class _ResultScreenState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _fade; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 650), + )..forward(); + _fade = CurvedAnimation(parent: _controller, curve: Curves.easeOut); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final result = widget.result; + final measurement = result.measurement; + final visual = nutritionStatusVisual(result.statusGabungan); + final isNormal = + result.statusGabungan.toLowerCase().contains('normal') || + result.statusGabungan.toLowerCase().contains('baik'); + + return Scaffold( + backgroundColor: const Color(0xFFEFF8F7), + appBar: AppBar(title: const Text('Hasil Analisis')), + body: FadeTransition( + opacity: _fade, + child: ListView( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 180), + children: [ + HealthCard( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: Column( + children: [ + CircleAvatar( + radius: 32, + backgroundColor: visual.color.withValues(alpha: 0.14), + child: Icon( + visual.icon, + color: visual.color, + size: 38, + ), + ), + const SizedBox(height: 24), + Text( + 'STATUS GIZI', + style: AppTypography.caption.copyWith( + letterSpacing: 4, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 10), + Text( + result.statusGabungan, + style: AppTypography.h1.copyWith(fontSize: 32), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + StatusBadge( + text: visual.badgeLabel, + color: visual.color, + ), + ], + ), + ), + const SizedBox(height: 20), + HealthCard( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CircleAvatar( + radius: 25, + backgroundColor: Color(0xFFEAF7F7), + child: Icon( + Icons.child_care_rounded, + color: SgColors.primary, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + measurement?.childName ?? 'Data Anak', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + const SizedBox(height: 4), + Text( + '${result.identitas.umurBulan.toStringAsFixed(0)} Bulan | ${_genderLabel(result.identitas.jenisKelamin)}', + style: AppTypography.caption, + ), + ], + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const Text('TERAKHIR UKUR', style: AppTypography.caption), + const SizedBox(height: 4), + Text( + measurement == null + ? 'Hari ini' + : formatMeasurementDate(measurement.tanggalUkur), + style: AppTypography.h3, + ), + ], + ), + ], + ), + ), + const SizedBox(height: 24), + Row( + children: [ + const Icon( + Icons.trending_up_rounded, + color: SgColors.primary, + size: 20, + ), + const SizedBox(width: 8), + Text('Detail Indikator Gizi', style: AppTypography.h2), + ], + ), + const SizedBox(height: 16), + MetricProgress( + label: 'Berat Badan / Umur (BB/U)', + description: 'Mengukur berat terhadap usia', + status: result.kategori.bbu, + value: _scoreToProgress(result.zScore.bbu), + icon: Icons.monitor_weight_outlined, + ), + MetricProgress( + label: 'Tinggi Badan / Umur (TB/U)', + description: 'Mengukur tinggi terhadap usia', + status: result.kategori.tbu, + value: _scoreToProgress(result.zScore.tbu), + icon: Icons.straighten_rounded, + ), + MetricProgress( + label: 'Berat / Tinggi (BB/TB)', + description: 'Proporsi tubuh ideal', + status: result.kategori.bbtb, + value: _scoreToProgress(result.zScore.bbtb), + icon: Icons.verified_outlined, + ), + const SizedBox(height: 8), + HealthCard( + color: const Color(0xFFF5FBFA), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + Icons.info_outline_rounded, + color: SgColors.primary, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Catatan Nutrisi', style: AppTypography.h3), + const SizedBox(height: 8), + Text( + recommendationStatusExplanation(result.statusGabungan), + style: AppTypography.body, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + bottomSheet: Container( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 24), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.96), + border: const Border(top: BorderSide(color: SgColors.border)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 16, + offset: const Offset(0, -8), + ), + ], + ), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isNormal) ...[ + HealthCard( + color: const Color(0xFFFFFBEB), + borderColor: const Color(0xFFF7E7C1), + child: Row( + children: const [ + Icon( + Icons.warning_amber_rounded, + color: SgColors.warning, + ), + SizedBox(width: 12), + Expanded( + child: Text( + 'Status perlu perhatian. Konsultasi ahli gizi menjadi prioritas.', + style: AppTypography.body, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + ], + if (!isNormal) + PrimaryButton( + label: 'Konsultasi Ahli Gizi', + icon: Icons.chat_bubble_outline_rounded, + onPressed: () { + Navigator.of( + context, + ).push(fadeRoute(const ConsultationChatScreen())); + }, + ), + if (!isNormal) const SizedBox(height: 12), + PrimaryButton( + label: 'Lihat Rekomendasi Menu', + icon: Icons.restaurant_menu_rounded, + onPressed: () { + Navigator.of(context).push( + fadeRoute( + RecommendationScreen( + status: result.statusGabungan, + childId: measurement?.childId, + riwayatId: measurement?.id, + childName: measurement?.childName, + measuredAt: measurement?.tanggalUkur, + ), + ), + ); + }, + ), + if (isNormal) ...[ + const SizedBox(height: 12), + PrimaryButton( + label: 'Konsultasi Ahli Gizi', + icon: Icons.chat_bubble_outline_rounded, + isOutlined: true, + onPressed: () { + Navigator.of( + context, + ).push(fadeRoute(const ConsultationChatScreen())); + }, + ), + ], + ], + ), + ), + ), + ); + } + + double _scoreToProgress(double score) { + if (score.isNaN) return 0.62; + return ((score + 3) / 6).clamp(0.08, 0.96).toDouble(); + } + + String _genderLabel(String gender) { + if (gender.toLowerCase().startsWith('p')) return 'Perempuan'; + return 'Laki-laki'; + } +} diff --git a/Android/flutter_app/lib/screens/riwayat_detail_screen.dart b/Android/flutter_app/lib/screens/riwayat_detail_screen.dart new file mode 100644 index 0000000..469cf46 --- /dev/null +++ b/Android/flutter_app/lib/screens/riwayat_detail_screen.dart @@ -0,0 +1,306 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../models/riwayat_response_model.dart'; +import '../utils/nutrition_display_utils.dart'; +import 'consultation_chat_screen.dart'; +import 'recommendation_screen.dart'; + +class RiwayatDetailScreen extends StatelessWidget { + const RiwayatDetailScreen({ + super.key, + required this.child, + required this.item, + }); + + final ChildInfoModel child; + final RiwayatItemModel item; + + @override + Widget build(BuildContext context) { + final visual = nutritionStatusVisual(item.statusGabungan); + final bbuCategory = bbuCategoryFromScore( + item.zBbu, + fallback: item.kategori.bbu, + ); + final tbuCategory = tbuCategoryFromScore( + item.zTbu, + fallback: item.kategori.tbu, + ); + final bbtbCategory = bbtbCategoryFromScore( + item.zBbtb, + fallback: item.kategori.bbtb, + ); + + return Scaffold( + backgroundColor: SgColors.background, + appBar: AppBar(title: const Text('Detail Analisis')), + body: ListView( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 160), + children: [ + HealthCard( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'STATUS UTAMA', + style: AppTypography.caption.copyWith( + color: SgColors.primary, + letterSpacing: 1.2, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 24, + backgroundColor: visual.color.withValues(alpha: 0.14), + child: Icon(visual.icon, color: visual.color, size: 24), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Status Gizi: ${item.statusGabungan}', + style: AppTypography.h2, + ), + const SizedBox(height: 10), + StatusBadge( + text: visual.badgeLabel, + color: visual.color, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 18), + const Divider(height: 1, color: SgColors.border), + const SizedBox(height: 18), + Row( + children: [ + ChildAvatar( + name: child.nama, + gender: child.jenisKelamin, + photoUrl: child.photoUrl, + radius: 28, + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + child.nama, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + const SizedBox(height: 4), + Text( + formatAgeFromMonths(item.umurBulan), + style: AppTypography.caption, + ), + const SizedBox(height: 2), + Text( + formatMeasurementDate(item.tanggalUkur), + style: AppTypography.caption, + ), + ], + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + _SectionCard( + title: 'Data Pengukuran', + child: Column( + children: [ + _InfoRow(label: 'Berat', value: '${item.berat.toStringAsFixed(1)} kg'), + _InfoRow(label: 'Tinggi', value: '${item.tinggi.toStringAsFixed(1)} cm'), + _InfoRow( + label: 'Cara', + value: measurementMethodLabel(item.caraUkur, item.umurBulan), + ), + ], + ), + ), + const SizedBox(height: 16), + _SectionCard( + title: 'Detail Z-Score', + child: Column( + children: [ + _ScoreRow( + label: 'BB/U', + score: formatScore(item.zBbu), + category: bbuCategory, + ), + _ScoreRow( + label: 'TB/U', + score: formatScore(item.zTbu), + category: tbuCategory, + ), + _ScoreRow( + label: 'BB/TB', + score: formatScore(item.zBbtb), + category: bbtbCategory, + ), + ], + ), + ), + const SizedBox(height: 16), + _SectionCard( + title: 'Interpretasi', + child: Text( + buildInterpretation( + status: item.statusGabungan, + bbuCategory: bbuCategory, + tbuCategory: tbuCategory, + bbtbCategory: bbtbCategory, + ), + style: AppTypography.body.copyWith(color: SgColors.textPrimary), + ), + ), + ], + ), + bottomSheet: Container( + padding: const EdgeInsets.fromLTRB(24, 14, 24, 24), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.96), + border: const Border(top: BorderSide(color: SgColors.border)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 16, + offset: const Offset(0, -8), + ), + ], + ), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PrimaryButton( + label: 'Lihat Rekomendasi', + icon: Icons.restaurant_menu_rounded, + onPressed: () { + Navigator.of(context).push( + fadeRoute( + RecommendationScreen( + status: item.statusGabungan, + childId: child.id, + riwayatId: item.id, + childName: child.nama, + measuredAt: item.tanggalUkur, + ), + ), + ); + }, + ), + const SizedBox(height: 12), + PrimaryButton( + label: 'Konsultasi', + icon: Icons.chat_bubble_outline_rounded, + isOutlined: true, + onPressed: () { + Navigator.of( + context, + ).push(fadeRoute(const ConsultationChatScreen())); + }, + ), + ], + ), + ), + ), + ); + } +} + +class _SectionCard extends StatelessWidget { + const _SectionCard({required this.title, required this.child}); + + final String title; + final Widget child; + + @override + Widget build(BuildContext context) { + return HealthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title.toUpperCase(), style: AppTypography.h3), + const SizedBox(height: 16), + child, + ], + ), + ); + } +} + +class _InfoRow extends StatelessWidget { + const _InfoRow({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + SizedBox(width: 72, child: Text(label, style: AppTypography.body)), + Expanded( + child: Text( + value, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + ), + ], + ), + ); + } +} + +class _ScoreRow extends StatelessWidget { + const _ScoreRow({ + required this.label, + required this.score, + required this.category, + }); + + final String label; + final String score; + final String category; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 14), + child: Row( + children: [ + SizedBox(width: 64, child: Text(label, style: AppTypography.body)), + Expanded( + child: Text( + '$score -> $category', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + ), + ], + ), + ); + } +} diff --git a/Android/flutter_app/lib/screens/riwayat_screen.dart b/Android/flutter_app/lib/screens/riwayat_screen.dart new file mode 100644 index 0000000..067b303 --- /dev/null +++ b/Android/flutter_app/lib/screens/riwayat_screen.dart @@ -0,0 +1,422 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import '../models/riwayat_response_model.dart'; +import '../services/api_service.dart'; +import '../utils/nutrition_display_utils.dart'; +import 'riwayat_detail_screen.dart'; + +class RiwayatScreen extends StatefulWidget { + const RiwayatScreen({super.key, required this.childId}); + + final int childId; + + @override + State createState() => _RiwayatScreenState(); +} + +class _RiwayatScreenState extends State { + final ApiService _apiService = ApiService(); + late Future _future; + + @override + void initState() { + super.initState(); + _future = _apiService.getRiwayat(childId: widget.childId); + } + + void _retry() { + setState(() { + _future = _apiService.getRiwayat(childId: widget.childId); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SgColors.background, + appBar: AppBar(title: const Text('Riwayat Gizi')), + body: FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const _HistorySkeleton(); + } + if (snapshot.hasError) { + return ErrorState( + message: + 'Riwayat belum dapat dimuat. Pastikan server API aktif dan koneksi tersedia.', + onRetry: _retry, + ); + } + + final data = snapshot.data!; + if (data.riwayat.isEmpty) { + return EmptyState( + title: 'Belum Ada Riwayat', + message: + 'Input pengukuran pertama untuk mulai melihat timeline pertumbuhan anak.', + actionLabel: 'Muat Ulang', + onAction: _retry, + ); + } + + return ListView( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 32), + children: [ + HealthCard( + color: const Color(0xFFEAF7F7), + borderColor: const Color(0xFFCBEAEA), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChildAvatar( + name: data.child.nama, + gender: data.child.jenisKelamin, + photoUrl: data.child.photoUrl, + radius: 28, + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data.child.nama, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.h2, + ), + const SizedBox(height: 4), + Text( + '${genderLabel(data.child.jenisKelamin)} | ${formatAgeFromBirthDate(data.child.tanggalLahir)}', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.caption, + ), + const SizedBox(height: 8), + Text( + 'Timeline pengukuran tersusun dari tanggal ukur terbaru.', + style: AppTypography.body, + ), + ], + ), + ), + const SizedBox(width: 12), + StatusBadge(text: '${data.riwayat.length} Entri'), + ], + ), + ), + const SizedBox(height: 24), + ...data.riwayat.asMap().entries.map((entry) { + return _TimelineItem( + child: data.child, + item: entry.value, + isLast: entry.key == data.riwayat.length - 1, + ); + }), + ], + ); + }, + ), + ); + } +} + +class _TimelineItem extends StatelessWidget { + const _TimelineItem({ + required this.child, + required this.item, + required this.isLast, + }); + + final ChildInfoModel child; + final RiwayatItemModel item; + final bool isLast; + + @override + Widget build(BuildContext context) { + final visual = nutritionStatusVisual(item.statusGabungan); + + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: visual.color.withValues(alpha: 0.12), + shape: BoxShape.circle, + border: Border.all(color: visual.color.withValues(alpha: 0.4)), + ), + child: Icon(visual.icon, color: visual.color, size: 12), + ), + if (!isLast) + Expanded( + child: Container( + width: 2, + margin: const EdgeInsets.symmetric(vertical: 4), + color: SgColors.border, + ), + ), + ], + ), + const SizedBox(width: 16), + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.calendar_today_outlined, + size: 16, + color: SgColors.textSecondary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + formatMeasurementDate(item.tanggalUkur), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.caption.copyWith( + fontWeight: FontWeight.w800, + letterSpacing: 0.4, + ), + ), + ), + ], + ), + const SizedBox(height: 10), + HealthCard( + onTap: () { + Navigator.of(context).push( + fadeRoute( + RiwayatDetailScreen(child: child, item: item), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChildAvatar( + name: child.nama, + gender: child.jenisKelamin, + photoUrl: child.photoUrl, + radius: 22, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + child.nama, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + const SizedBox(height: 4), + Text( + 'Usia ${formatAgeFromMonths(item.umurBulan)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.caption, + ), + ], + ), + ), + const SizedBox(width: 8), + Flexible( + child: StatusBadge( + text: item.statusGabungan, + color: visual.color, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _MeasurementInfo( + icon: Icons.monitor_weight_outlined, + label: 'Berat badan', + value: '${item.berat.toStringAsFixed(1)} kg', + ), + ), + const SizedBox(width: 12), + Expanded( + child: _MeasurementInfo( + icon: Icons.straighten_rounded, + label: 'Tinggi badan', + value: '${item.tinggi.toStringAsFixed(1)} cm', + ), + ), + ], + ), + const SizedBox(height: 14), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFF8FBFA), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: SgColors.border), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + visual.icon, + size: 18, + color: visual.color, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + visual.summary, + style: AppTypography.body, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + Row( + children: const [ + Icon( + Icons.info_outline_rounded, + size: 16, + color: SgColors.textSecondary, + ), + SizedBox(width: 8), + Expanded( + child: Text( + 'Ketuk untuk detail analisis', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.caption, + ), + ), + Icon( + Icons.chevron_right_rounded, + color: SgColors.textSecondary, + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _MeasurementInfo extends StatelessWidget { + const _MeasurementInfo({ + required this.icon, + required this.label, + required this.value, + }); + + final IconData icon; + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 20, + backgroundColor: const Color(0xFFEAF7F7), + child: Icon(icon, color: SgColors.primary, size: 18), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTypography.caption), + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTypography.h3, + ), + ], + ), + ), + ], + ); + } +} + +class _HistorySkeleton extends StatelessWidget { + const _HistorySkeleton(); + + @override + Widget build(BuildContext context) { + return ListView( + padding: const EdgeInsets.all(24), + children: [ + HealthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(width: 180, height: 18, color: const Color(0xFFE9EEEC)), + const SizedBox(height: 12), + Container( + width: double.infinity, + height: 12, + color: const Color(0xFFE9EEEC), + ), + ], + ), + ), + const SizedBox(height: 24), + for (var i = 0; i < 3; i++) ...[ + HealthCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 140, + height: 16, + color: const Color(0xFFE9EEEC), + ), + const SizedBox(height: 16), + Container( + width: double.infinity, + height: 12, + color: const Color(0xFFE9EEEC), + ), + const SizedBox(height: 10), + Container( + width: 180, + height: 12, + color: const Color(0xFFE9EEEC), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + ], + ); + } +} diff --git a/Android/flutter_app/lib/screens/security_screen.dart b/Android/flutter_app/lib/screens/security_screen.dart new file mode 100644 index 0000000..6e88f72 --- /dev/null +++ b/Android/flutter_app/lib/screens/security_screen.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:lucide_icons/lucide_icons.dart'; + +import '../app_design.dart'; + +class SecurityScreen extends StatefulWidget { + const SecurityScreen({super.key}); + + @override + State createState() => _SecurityScreenState(); +} + +class _SecurityScreenState extends State { + final _oldPassword = TextEditingController(); + final _newPassword = TextEditingController(); + final _confirmPassword = TextEditingController(); + final _otpController = TextEditingController(); + + bool _hideOld = true; + bool _hideNew = true; + bool _hideConfirm = true; + bool _savingPassword = false; + + @override + void dispose() { + _oldPassword.dispose(); + _newPassword.dispose(); + _confirmPassword.dispose(); + _otpController.dispose(); + super.dispose(); + } + + String get _strength { + final value = _newPassword.text; + if (value.length >= 10 && RegExp(r'[A-Z]').hasMatch(value) && RegExp(r'[0-9]').hasMatch(value)) { + return 'Kuat'; + } + if (value.length >= 7) return 'Sedang'; + return 'Lemah'; + } + + Color get _strengthColor { + if (_strength == 'Kuat') return const Color(0xFF34A853); + if (_strength == 'Sedang') return const Color(0xFFF59E0B); + return const Color(0xFFE53935); + } + + Future _savePassword() async { + if (_newPassword.text.trim().isEmpty || _newPassword.text != _confirmPassword.text) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Konfirmasi password tidak sesuai.')), + ); + return; + } + setState(() => _savingPassword = true); + await Future.delayed(const Duration(milliseconds: 900)); + if (!mounted) return; + setState(() => _savingPassword = false); + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Berhasil'), + content: const Text('Password berhasil diperbarui.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7F6), + appBar: AppBar( + title: const Text('Privasi & Keamanan'), + backgroundColor: const Color(0xFFF5F7F6), + ), + body: SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 14, 20, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Ubah Password', style: AppTypography.h2.copyWith(color: SgColors.textPrimary)), + const SizedBox(height: 10), + HealthCard( + child: Column( + children: [ + _PasswordField( + controller: _oldPassword, + label: 'Password Lama', + hidden: _hideOld, + onToggle: () => setState(() => _hideOld = !_hideOld), + ), + const SizedBox(height: 10), + _PasswordField( + controller: _newPassword, + label: 'Password Baru', + hidden: _hideNew, + onToggle: () => setState(() => _hideNew = !_hideNew), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 8), + Row( + children: [ + Text('Kekuatan password: ', style: AppTypography.caption), + Text( + _strength, + style: AppTypography.caption.copyWith(color: _strengthColor, fontWeight: FontWeight.w800), + ), + ], + ), + const SizedBox(height: 10), + _PasswordField( + controller: _confirmPassword, + label: 'Konfirmasi Password', + hidden: _hideConfirm, + onToggle: () => setState(() => _hideConfirm = !_hideConfirm), + ), + const SizedBox(height: 14), + PrimaryButton( + label: _savingPassword ? 'Menyimpan...' : 'Simpan Password', + icon: LucideIcons.arrowRight, + onPressed: _savingPassword ? null : _savePassword, + ), + ], + ), + ), + const SizedBox(height: 16), + const Text('Verifikasi Nomor Telepon', style: AppTypography.h2), + const SizedBox(height: 10), + HealthCard( + child: Column( + children: [ + TextField( + controller: _otpController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Kode OTP', + prefixIcon: const Icon(LucideIcons.shield, color: Color(0xFF0B7A86)), + filled: true, + fillColor: const Color(0xFFF7FAF9), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Color(0xFFE3EAE8)), + ), + ), + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => _snack(context, 'OTP berhasil dikirim ulang.'), + child: const Text('Resend OTP'), + ), + ), + const SizedBox(width: 10), + Expanded( + child: FilledButton( + onPressed: () => _snack(context, 'Nomor berhasil diverifikasi.'), + child: const Text('Verifikasi'), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + const Text('Kebijakan Privasi', style: AppTypography.h2), + const SizedBox(height: 10), + const HealthCard( + child: Text( + 'Privasi data pengguna S-Gizi dilindungi dan hanya digunakan untuk kebutuhan layanan monitoring dan edukasi gizi.', + style: AppTypography.body, + ), + ), + ], + ), + ), + ), + ).animate().fadeIn(duration: 220.ms).slideY(begin: 0.02, end: 0); + } +} + +class _PasswordField extends StatelessWidget { + const _PasswordField({ + required this.controller, + required this.label, + required this.hidden, + required this.onToggle, + this.onChanged, + }); + + final TextEditingController controller; + final String label; + final bool hidden; + final VoidCallback onToggle; + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + obscureText: hidden, + onChanged: onChanged, + decoration: InputDecoration( + labelText: label, + prefixIcon: const Icon(LucideIcons.shield, color: Color(0xFF0B7A86)), + suffixIcon: IconButton( + onPressed: onToggle, + icon: Icon(hidden ? LucideIcons.eye : LucideIcons.eyeOff), + ), + filled: true, + fillColor: const Color(0xFFF7FAF9), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Color(0xFFE3EAE8)), + ), + ), + ); + } +} + +void _snack(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); +} + diff --git a/Android/flutter_app/lib/screens/splash_screen.dart b/Android/flutter_app/lib/screens/splash_screen.dart new file mode 100644 index 0000000..f9c9b63 --- /dev/null +++ b/Android/flutter_app/lib/screens/splash_screen.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../app_design.dart'; +import 'onboarding_screen.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _imageScale; + late final Animation _imageOpacity; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..forward(); + _imageScale = Tween(begin: 1.22, end: 1).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + ), + ); + _imageOpacity = Tween(begin: 0.2, end: 1).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOut), + ); + + Timer(const Duration(milliseconds: 1500), () { + if (!mounted) return; + Navigator.of(context).pushReplacement(fadeRoute(const OnboardingScreen())); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final shortest = size.shortestSide; + final imageSize = (shortest * 0.58).clamp(190.0, 320.0); + + return Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFFFDFEFE), Color(0xFFE8F6F3), Color(0xFFF5F7F6)], + ), + ), + child: LayoutBuilder( + builder: (context, constraints) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth.clamp(280.0, 520.0), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: (size.width * 0.08).clamp(20.0, 36.0), + ), + child: Center( + child: FadeTransition( + opacity: _imageOpacity, + child: ScaleTransition( + scale: _imageScale, + child: Container( + width: imageSize, + height: imageSize, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40), + boxShadow: [ + BoxShadow( + color: const Color(0xFF4B8E96).withValues(alpha: 0.24), + blurRadius: 42, + spreadRadius: 4, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(40), + child: Image.asset( + 'assets/image/Logo_SplashScreen.png', + fit: BoxFit.cover, + filterQuality: FilterQuality.high, + errorBuilder: (_, __, ___) => Image.asset( + 'assets/image/logo_sgizi.png', + fit: BoxFit.contain, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/Android/flutter_app/lib/services/api_service.dart b/Android/flutter_app/lib/services/api_service.dart new file mode 100644 index 0000000..dd34085 --- /dev/null +++ b/Android/flutter_app/lib/services/api_service.dart @@ -0,0 +1,427 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; + +import 'package:http/http.dart' as http; + +import '../app_state.dart'; +import '../models/api_result_model.dart'; +import '../models/mobile_child_model.dart'; +import '../models/news_article_model.dart'; +import '../models/recommendation_response_model.dart'; +import '../models/riwayat_response_model.dart'; + +class ApiService { + ApiService({http.Client? client, String? baseUrl}) + : _client = client ?? http.Client(), + baseUrl = baseUrl ?? _resolveDefaultBaseUrl(); + + final http.Client _client; + final String baseUrl; + + static String _resolveDefaultBaseUrl() { + // Web -> localhost browser, Android emulator -> 10.0.2.2 + if (kIsWeb) { + return 'http://127.0.0.1:8000/api'; + } + return 'http://10.0.2.2:8000/api'; + //192.168.1.77// +//10.0.2.2 // ip address android emulator + } + + + Future> hitungGizi(Map data) async { + final response = await _client.post( + Uri.parse('$baseUrl/gizi/hitung'), + headers: _headers(), + body: jsonEncode(data), + ); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + if (decoded is! Map) { + throw Exception('Format response API tidak valid.'); + } + return decoded; + } + + throw Exception('Gagal API (${response.statusCode}): ${response.body}'); + } + + Future sendOtp(String phone) async { + final response = await _client.post( + Uri.parse('$baseUrl/send-otp'), + headers: const { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode({'phone': phone}), + ); + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal mengirim OTP: ${response.body}'); + } + } + + Future verifyOtp(String phone, String otp) async { + final response = await _client.post( + Uri.parse('$baseUrl/verify-otp'), + headers: const { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode({'phone': phone, 'otp': otp}), + ); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + if (decoded is! Map) { + throw Exception('Format response API tidak valid.'); + } + final token = decoded['token'] as String? ?? ''; + if (token.isEmpty) throw Exception('Token login kosong.'); + return token; + } + + throw Exception('OTP tidak valid: ${response.body}'); + } + + Future> getChildren() async { + final response = await _client.get( + Uri.parse('$baseUrl/children'), + headers: _headers(), + ); + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal mengambil data anak: ${response.body}'); + } + + final decoded = jsonDecode(response.body); + final list = decoded is Map ? decoded['data'] : null; + if (list is! List) return const []; + return list + .whereType>() + .map(MobileChildModel.fromJson) + .toList(); + } + + Future createChild(Map data) async { + final response = await _client.post( + Uri.parse('$baseUrl/children'), + headers: _headers(), + body: jsonEncode({ + 'nama_anak': data['nama_anak'] ?? data['nama'], + 'tanggal_lahir': data['tanggal_lahir'], + 'jenis_kelamin': data['jenis_kelamin'], + }), + ); + + if (response.statusCode != 201 && response.statusCode != 200) { + throw Exception('Gagal menyimpan data anak: ${response.body}'); + } + + final decoded = jsonDecode(response.body); + if (decoded is! Map || + decoded['data'] is! Map) { + throw Exception('Format data anak tidak valid.'); + } + + return MobileChildModel.fromJson(decoded['data'] as Map); + } + + Future updateChild({ + required int childId, + required Map data, + }) async { + final response = await _client.put( + Uri.parse('$baseUrl/children/$childId'), + headers: _headers(), + body: jsonEncode({ + if (data['nama'] != null) 'nama': data['nama'], + if (data['nama_anak'] != null) 'nama_anak': data['nama_anak'], + if (data['tanggal_lahir'] != null) 'tanggal_lahir': data['tanggal_lahir'], + if (data['jenis_kelamin'] != null) 'jenis_kelamin': data['jenis_kelamin'], + }), + ); + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal memperbarui data anak: ${response.body}'); + } + final decoded = jsonDecode(response.body); + if (decoded is! Map || decoded['data'] is! Map) { + throw Exception('Format response update anak tidak valid.'); + } + return MobileChildModel.fromJson(decoded['data'] as Map); + } + + Future deleteChild({required int childId}) async { + final response = await _client.delete( + Uri.parse('$baseUrl/children/$childId'), + headers: _headers(), + ); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal menghapus data anak: ${response.body}'); + } + } + + Future postHasil(Map data) async { + final json = await hitungGizi(data); + return ApiResultModel.fromJson(json); + } + + Future getRiwayat({required int childId}) async { + final response = await _client.get( + Uri.parse('$baseUrl/riwayat/$childId'), + headers: _headers(), + ); + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception( + 'Gagal mengambil riwayat. Status code: ${response.statusCode}', + ); + } + + final decoded = jsonDecode(response.body); + if (decoded is! Map) { + throw Exception('Format response API tidak valid.'); + } + + return RiwayatResponseModel.fromJson(decoded); + } + + Future getRecommendations({ + required String status, + int? childId, + int? riwayatId, + }) async { + final encodedStatus = Uri.encodeComponent(status.isEmpty ? 'latest' : status); + final uri = Uri.parse('$baseUrl/rekomendasi/$encodedStatus').replace( + queryParameters: { + if (childId != null) 'child_id': '$childId', + if (riwayatId != null) 'riwayat_id': '$riwayatId', + }, + ); + final response = await _client.get( + uri, + headers: _headers(), + ); + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception( + 'Gagal mengambil rekomendasi. Status code: ${response.statusCode}', + ); + } + + final decoded = jsonDecode(response.body); + if (decoded is! Map) { + throw Exception('Format response rekomendasi tidak valid.'); + } + return RecommendationResponseModel.fromJson(decoded); + } + + Future> getNewsArticles({ + String query = + 'kesehatan anak gizi balita stunting mpasi tumbuh kembang protein anak parenting kesehatan who nutrition imunisasi kesehatan ibu anak', + }) async { + // Online/Internet (Google News RSS) tetap lewat endpoint `/news` + // agar tidak bentrok dengan `/articles` yang sekarang khusus DB admin. + final uri = Uri.parse('$baseUrl/news').replace(queryParameters: {'q': query}); + final response = await _client.get(uri, headers: _headers()); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception( + 'Gagal mengambil artikel berita. Status code: ${response.statusCode}', + ); + } + final decoded = jsonDecode(response.body); + if (decoded is! Map) { + throw Exception('Format response artikel tidak valid.'); + } + final data = decoded['data']; + if (data is! List) return const []; + return data + .whereType>() + .map(NewsArticleModel.fromJson) + .toList(); + } + + Future> getProfile() async { + final response = await _client.get( + Uri.parse('$baseUrl/profile'), + headers: _headers(), + ); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal mengambil profil: ${response.body}'); + } + final decoded = jsonDecode(response.body); + if (decoded is! Map || decoded['data'] is! Map) { + throw Exception('Format profil tidak valid.'); + } + return decoded['data'] as Map; + } + + Future> updateProfile({ + required String name, + required String phone, + String? email, + }) async { + final response = await _client.put( + Uri.parse('$baseUrl/profile'), + headers: _headers(), + body: jsonEncode({ + 'name': name, + 'phone': phone, + 'email': (email ?? '').trim().isEmpty ? null : email?.trim(), + }), + ); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal memperbarui profil: ${response.body}'); + } + final decoded = jsonDecode(response.body); + if (decoded is! Map || decoded['data'] is! Map) { + throw Exception('Format update profil tidak valid.'); + } + return decoded['data'] as Map; + } + + /// Ambil artikel dari database `s_gizi` (tanpa query News API). + Future> getArticlesDb() async { + final uri = Uri.parse('$baseUrl/articles'); + final response = await _client.get(uri, headers: _headers()); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception( + 'Gagal mengambil artikel. Status code: ${response.statusCode}', + ); + } + final decoded = jsonDecode(response.body); + if (decoded is! Map) { + throw Exception('Format response artikel tidak valid.'); + } + final data = decoded['data']; + if (data is! List) return const []; + return data + .whereType>() + .map(NewsArticleModel.fromJson) + .toList(); + } + + Future>> getConsultationRooms({ + required int childId, + }) async { + final uri = Uri.parse('$baseUrl/consultation/rooms') + .replace(queryParameters: {'child_id': '$childId'}); + final response = await _client.get(uri, headers: _headers()); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal mengambil room konsultasi: ${response.body}'); + } + final decoded = jsonDecode(response.body); + final data = decoded is Map ? decoded['data'] : null; + if (data is! List) return const []; + return data.whereType>().toList(); + } + + Future> openConsultationRoom({ + required int childId, + required String expertId, + required String expertName, + required String specialization, + required String assetImage, + required bool online, + }) async { + final response = await _client.post( + Uri.parse('$baseUrl/consultation/rooms'), + headers: _headers(), + body: jsonEncode({ + 'child_id': childId, + 'expert_id': expertId, + 'expert_name': expertName, + 'specialization': specialization, + 'asset_image': assetImage, + 'online': online, + }), + ); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal membuka room konsultasi: ${response.body}'); + } + final decoded = jsonDecode(response.body); + if (decoded is! Map || decoded['data'] is! Map) { + throw Exception('Format room konsultasi tidak valid.'); + } + return decoded['data'] as Map; + } + + Future>> getConsultationMessages({ + required int roomId, + }) async { + final response = await _client.get( + Uri.parse('$baseUrl/consultation/rooms/$roomId/messages'), + headers: _headers(), + ); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal mengambil messages konsultasi: ${response.body}'); + } + final decoded = jsonDecode(response.body); + final data = decoded is Map ? decoded['data'] : null; + if (data is! List) return const []; + return data.whereType>().toList(); + } + + Future> sendConsultationMessage({ + required int roomId, + required String message, + }) async { + final response = await _client.post( + Uri.parse('$baseUrl/consultation/rooms/$roomId/messages'), + headers: _headers(), + body: jsonEncode({'message': message}), + ); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal kirim pesan konsultasi: ${response.body}'); + } + final decoded = jsonDecode(response.body); + if (decoded is! Map || decoded['data'] is! Map) { + throw Exception('Format response kirim pesan tidak valid.'); + } + return decoded['data'] as Map; + } + + Future> sendConsultationExpertReply({ + required int roomId, + required String message, + }) async { + final response = await _client.post( + Uri.parse('$baseUrl/consultation/rooms/$roomId/expert-reply'), + headers: _headers(), + body: jsonEncode({'message': message}), + ); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal kirim balasan ahli: ${response.body}'); + } + final decoded = jsonDecode(response.body); + if (decoded is! Map || decoded['data'] is! Map) { + throw Exception('Format response balasan ahli tidak valid.'); + } + return decoded['data'] as Map; + } + + Future updateConsultationRoomStatus({ + required int roomId, + required String status, + }) async { + final response = await _client.patch( + Uri.parse('$baseUrl/consultation/rooms/$roomId/status'), + headers: _headers(), + body: jsonEncode({'status': status}), + ); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw Exception('Gagal update status konsultasi: ${response.body}'); + } + } + + Map _headers() { + final token = SgiziAppState.instance.authToken; + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }; + } +} diff --git a/Android/flutter_app/lib/services/calculation_service.dart b/Android/flutter_app/lib/services/calculation_service.dart new file mode 100644 index 0000000..fc94018 --- /dev/null +++ b/Android/flutter_app/lib/services/calculation_service.dart @@ -0,0 +1,157 @@ +import 'dart:developer' as developer; +import 'dart:math'; + +import '../models/child_model.dart'; +import 'data_service.dart'; + +class CalculationResult { + const CalculationResult({ + required this.bbU, + required this.tbU, + required this.bbTb, + required this.adjustedHeightCm, + required this.weightForHeightIndicator, + required this.debugLog, + }); + + final double bbU; + final double tbU; + final double bbTb; + final double adjustedHeightCm; + final String weightForHeightIndicator; + final String debugLog; +} + +class CalculationService { + CalculationService({DataService? dataService}) + : _dataService = dataService ?? DataService.instance; + + final DataService _dataService; + + Future calculate(ChildModel child) async { + final gender = child.normalizedGender; + final position = child.normalizedPosition; + final adjustedHeight = child.adjustedHeightCm; + final indicator = child.weightForHeightIndicator; + + final wfaLms = await _dataService.getAgeBasedLms( + gender: gender, + indicator: child.weightForAgeIndicator, + ageInMonths: child.ageInMonths, + ); + + final hfaLms = await _dataService.getAgeBasedLms( + gender: gender, + indicator: child.heightForAgeIndicator, + ageInMonths: child.ageInMonths, + ); + + final wfhLms = await _dataService.getHeightBasedLms( + gender: gender, + indicator: indicator, + heightCm: adjustedHeight, + ); + + final bbU = _calculateZScore( + l: wfaLms.l, + m: wfaLms.m, + s: wfaLms.s, + x: child.weightKg, + ); + final tbU = _calculateZScore( + l: hfaLms.l, + m: hfaLms.m, + s: hfaLms.s, + x: child.heightCm, + ); + final bbTb = _calculateZScore( + l: wfhLms.l, + m: wfhLms.m, + s: wfhLms.s, + x: child.weightKg, + ); + + final debugLog = _buildDebugLog( + child: child, + gender: gender, + position: position, + indicator: indicator, + adjustedHeight: adjustedHeight, + wfaLms: wfaLms, + hfaLms: hfaLms, + wfhLms: wfhLms, + bbU: bbU, + tbU: tbU, + bbTb: bbTb, + ); + developer.log(debugLog, name: 'nutrition.zscore'); + + return CalculationResult( + bbU: bbU, + tbU: tbU, + bbTb: bbTb, + adjustedHeightCm: adjustedHeight, + weightForHeightIndicator: indicator, + debugLog: debugLog, + ); + } + + double _calculateZScore({ + required double l, + required double m, + required double s, + required double x, + }) { + if (x <= 0 || m <= 0 || s <= 0) { + throw Exception('Nilai LMS atau data pengukuran tidak valid.'); + } + + final zScore = l.abs() < 1e-7 + ? log(x / m) / s + : (pow(x / m, l) - 1) / (l * s); + + if (zScore.isNaN || zScore.isInfinite) { + throw Exception('Z-score tidak valid.'); + } + + return zScore.toDouble(); + } + + String _buildDebugLog({ + required ChildModel child, + required String gender, + required String position, + required String indicator, + required double adjustedHeight, + required LmsRecord wfaLms, + required LmsRecord hfaLms, + required LmsRecord wfhLms, + required double bbU, + required double tbU, + required double bbTb, + }) { + String lmsLine(String label, LmsRecord record) { + return '$label LMS: L=${record.l}, M=${record.m}, S=${record.s}'; + } + + return [ + '=== WHO Z-SCORE DEBUG ===', + 'Umur (bulan): ${child.ageInMonths}', + 'Berat (kg): ${child.weightKg}', + 'Tinggi input (cm): ${child.heightCm}', + 'Tinggi adjusted (cm): $adjustedHeight', + 'Gender input: ${child.gender}', + 'Gender normalized: $gender', + 'Posisi input: ${child.measurementPosition}', + 'Posisi normalized: $position', + 'Indikator BB/TB: ${indicator.toUpperCase()}', + lmsLine('WFA', wfaLms), + lmsLine('HFA', hfaLms), + lmsLine(indicator.toUpperCase(), wfhLms), + 'Z-score BB/U: $bbU', + 'Z-score TB/U: $tbU', + 'Z-score BB/TB: $bbTb', + '==========================', + ].join('\n'); + } +} diff --git a/Android/flutter_app/lib/services/classification_service.dart b/Android/flutter_app/lib/services/classification_service.dart new file mode 100644 index 0000000..a8f728c --- /dev/null +++ b/Android/flutter_app/lib/services/classification_service.dart @@ -0,0 +1,85 @@ +class ClassificationService { + // ========================= + // KLASIFIKASI UMUM (Z-SCORE) + // ========================= + String classify(double zScore) { + if (zScore < -3) return 'Severe'; + if (zScore < -2) return 'Moderate'; + if (zScore <= 2) return 'Normal'; + if (zScore <= 3) return 'Overweight'; + return 'Obese'; + } + + // ========================= + // BB/U (BERAT BADAN MENURUT UMUR) + // ========================= + String interpretWeightForAge(double zScore) { + if (zScore < -3) { + return 'Gizi buruk (berat badan sangat kurang)'; + } + if (zScore < -2) { + return 'Gizi kurang (underweight)'; + } + if (zScore > 2) { + return 'Berat badan lebih dari standar usia'; + } + return 'Berat badan sesuai dengan standar usia'; + } + + // ========================= + // TB/U (TINGGI BADAN MENURUT UMUR) + // ========================= + String interpretHeightForAge(double zScore) { + if (zScore < -3) { + return 'Sangat pendek (stunting berat)'; + } + if (zScore < -2) { + return 'Pendek (stunting)'; + } + if (zScore < -1) { + return 'Tinggi badan sedikit di bawah standar'; + } + return 'Tinggi badan sesuai dengan standar usia'; + } + + // ========================= + // BB/TB (BERAT BADAN MENURUT TINGGI) + // ========================= + String interpretWeightForHeight(double zScore) { + if (zScore < -3) { + return 'Gizi buruk (wasting berat)'; + } + if (zScore < -2) { + return 'Gizi kurang (wasting)'; + } + if (zScore <= 2) { + return 'Status gizi normal'; + } + if (zScore <= 3) { + return 'Berisiko gizi lebih'; + } + return 'Obesitas'; + } + + // ========================= + // KESIMPULAN AKHIR (SMART ANALYSIS) + // ========================= + String buildConclusion({ + required double bbU, + required double tbU, + required double bbTb, + }) { + final masalah = []; + + if (bbU < -2) masalah.add('berat badan kurang'); + if (tbU < -2) masalah.add('stunting'); + if (bbTb < -2) masalah.add('wasting'); + if (bbTb > 2) masalah.add('gizi lebih'); + + if (masalah.isEmpty) { + return 'Status gizi anak normal berdasarkan indikator WHO.'; + } + + return 'Anak mengalami ${masalah.join(', ')}.'; + } +} diff --git a/Android/flutter_app/lib/services/data_service.dart b/Android/flutter_app/lib/services/data_service.dart new file mode 100644 index 0000000..0e40095 --- /dev/null +++ b/Android/flutter_app/lib/services/data_service.dart @@ -0,0 +1,178 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; + +class LmsRecord { + const LmsRecord({required this.l, required this.m, required this.s}); + + final double l; + final double m; + final double s; + + factory LmsRecord.fromJson(Map json) { + return LmsRecord( + l: (json['L'] as num).toDouble(), + m: (json['M'] as num).toDouble(), + s: (json['S'] as num).toDouble(), + ); + } +} + +class DataService { + DataService._(); + static final DataService instance = DataService._(); + + Map? _cache; + Future>? _loadingFuture; + + Future loadData() async { + if (_cache != null) return; + + _loadingFuture ??= _readJson(); + _cache = await _loadingFuture; + } + + Future> _readJson() async { + final raw = await rootBundle.loadString('assets/data/lms_who_final.json'); + return jsonDecode(raw) as Map; + } + + // ========================= + // ✅ AGE BASED (FIX TOTAL) + // ========================= + Future getAgeBasedLms({ + required String gender, + required String indicator, + required double ageInMonths, + }) async { + await loadData(); + final indicatorMap = _getIndicatorMap(gender, indicator); + + // 🔥 FIX: pakai double bukan int + final monthKeys = indicatorMap.keys.map(double.parse).toList()..sort(); + + final clampedAge = ageInMonths.clamp(monthKeys.first, monthKeys.last); + + double lower = monthKeys.first; + double upper = monthKeys.last; + + for (int i = 0; i < monthKeys.length - 1; i++) { + if (clampedAge >= monthKeys[i] && clampedAge <= monthKeys[i + 1]) { + lower = monthKeys[i]; + upper = monthKeys[i + 1]; + break; + } + } + + final lowerRecord = _parseRecord( + _getSafe(indicatorMap, lower), + '$indicator age $lower', + ); + + if ((upper - lower).abs() < 1e-6) return lowerRecord; + + final upperRecord = _parseRecord( + _getSafe(indicatorMap, upper), + '$indicator age $upper', + ); + + final factor = (clampedAge - lower) / (upper - lower); + + return LmsRecord( + l: _lerp(lowerRecord.l, upperRecord.l, factor), + m: _lerp(lowerRecord.m, upperRecord.m, factor), + s: _lerp(lowerRecord.s, upperRecord.s, factor), + ); + } + + // ========================= + // HEIGHT BASED (SUDAH BENAR) + // ========================= + Future getHeightBasedLms({ + required String gender, + required String indicator, + required double heightCm, + }) async { + await loadData(); + final indicatorMap = _getIndicatorMap(gender, indicator); + + final heights = indicatorMap.keys.map(double.parse).toList()..sort(); + + final clampedHeight = heightCm.clamp(heights.first, heights.last); + + double lower = heights.first; + double upper = heights.last; + + for (int i = 0; i < heights.length - 1; i++) { + if (clampedHeight >= heights[i] && clampedHeight <= heights[i + 1]) { + lower = heights[i]; + upper = heights[i + 1]; + break; + } + } + + final lowerRecord = _parseRecord( + _getSafe(indicatorMap, lower), + '$indicator height $lower', + ); + + if ((upper - lower).abs() < 1e-6) return lowerRecord; + + final upperRecord = _parseRecord( + _getSafe(indicatorMap, upper), + '$indicator height $upper', + ); + + final factor = (clampedHeight - lower) / (upper - lower); + + return LmsRecord( + l: _lerp(lowerRecord.l, upperRecord.l, factor), + m: _lerp(lowerRecord.m, upperRecord.m, factor), + s: _lerp(lowerRecord.s, upperRecord.s, factor), + ); + } + + // ========================= + // HELPERS + // ========================= + + Map _getIndicatorMap(String gender, String indicator) { + final data = _cache; + if (data == null) { + throw Exception('Data LMS belum dimuat.'); + } + + final genderMap = data[gender]; + if (genderMap is! Map) { + throw Exception('Gender $gender tidak ditemukan.'); + } + + final indicatorMap = genderMap[indicator]; + if (indicatorMap is! Map) { + throw Exception('Indikator $indicator tidak ditemukan.'); + } + + return indicatorMap; + } + + LmsRecord _parseRecord(dynamic value, String label) { + if (value is! Map) { + throw Exception('Record LMS untuk $label tidak ditemukan.'); + } + return LmsRecord.fromJson(value); + } + + double _lerp(double start, double end, double factor) { + return start + ((end - start) * factor); + } + + // 🔥 FIX FLEXIBLE KEY + dynamic _getSafe(Map map, double key) { + final k1 = key.toStringAsFixed(1); // 31.0 + final k2 = key.toStringAsFixed(0); // 31 + + if (map.containsKey(k1)) return map[k1]; + if (map.containsKey(k2)) return map[k2]; + + throw Exception('Key tidak ditemukan: $key'); + } +} diff --git a/Android/flutter_app/lib/services/local_notification_service.dart b/Android/flutter_app/lib/services/local_notification_service.dart new file mode 100644 index 0000000..eac8c0f --- /dev/null +++ b/Android/flutter_app/lib/services/local_notification_service.dart @@ -0,0 +1,38 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class LocalNotificationService { + LocalNotificationService._(); + static final LocalNotificationService instance = LocalNotificationService._(); + + final FlutterLocalNotificationsPlugin _plugin = + FlutterLocalNotificationsPlugin(); + bool _inited = false; + + Future init() async { + if (_inited) return; + const settings = InitializationSettings( + android: AndroidInitializationSettings('@mipmap/ic_launcher'), + ); + await _plugin.initialize(settings); + _inited = true; + } + + Future show({ + required int id, + required String title, + required String body, + }) async { + await init(); + const details = NotificationDetails( + android: AndroidNotificationDetails( + 'consultation_channel', + 'Konsultasi Ahli Gizi', + channelDescription: 'Notifikasi pesan konsultasi', + importance: Importance.high, + priority: Priority.high, + ), + ); + await _plugin.show(id, title, body, details); + } +} + diff --git a/Android/flutter_app/lib/utils/nutrition_display_utils.dart b/Android/flutter_app/lib/utils/nutrition_display_utils.dart new file mode 100644 index 0000000..8d1bf55 --- /dev/null +++ b/Android/flutter_app/lib/utils/nutrition_display_utils.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; + +import '../app_design.dart'; + +class NutritionStatusVisual { + const NutritionStatusVisual({ + required this.color, + required this.icon, + required this.badgeLabel, + required this.summary, + }); + + final Color color; + final IconData icon; + final String badgeLabel; + final String summary; +} + +class NormalizedStatus { + const NormalizedStatus({ + required this.originalStatus, + required this.primaryCategory, + required this.categories, + }); + + final String originalStatus; + final String primaryCategory; + final List categories; + + bool get hasStunting => categories.contains('Stunting'); + bool get hasUnderweight => categories.contains('Underweight'); + bool get hasWasting => categories.contains('Wasting'); + bool get hasObesitas => categories.contains('Obesitas'); + bool get isNormal => categories.length == 1 && categories.first == 'Normal'; + + String get focusSummary { + if (isNormal) { + return 'Fokus menu gizi seimbang untuk mempertahankan pertumbuhan yang baik.'; + } + + final focuses = []; + if (hasStunting) { + focuses.add('protein tinggi, zat besi, dan zinc'); + } + if (hasUnderweight || hasWasting) { + focuses.add('energi padat dan protein tinggi'); + } + if (hasObesitas) { + focuses.add('rendah gula, rendah lemak, dan tinggi serat'); + } + + if (focuses.isEmpty) { + return 'Fokus menu disesuaikan dengan status gizi terakhir anak.'; + } + + return 'Fokus menu: ${focuses.join(', ')}.'; + } +} + +NormalizedStatus normalizeStatus(String status) { + final originalStatus = status.trim().isEmpty ? 'Normal' : status.trim(); + final value = originalStatus.toLowerCase(); + final categories = []; + + if (value.contains('stunting') || value.contains('stunted')) { + categories.add('Stunting'); + } + if (value.contains('wasting')) { + categories.add('Wasting'); + } + if (value.contains('underweight') || value.contains('gizi kurang')) { + categories.add('Underweight'); + } + if (value.contains('obesitas') || + value.contains('overweight') || + value.contains('gizi lebih')) { + categories.add('Obesitas'); + } + + if (categories.isEmpty) { + categories.add('Normal'); + } + + return NormalizedStatus( + originalStatus: originalStatus, + primaryCategory: categories.first, + categories: categories.toSet().toList(), + ); +} + +NutritionStatusVisual nutritionStatusVisual(String status) { + final normalized = normalizeStatus(status); + final value = normalized.originalStatus.toLowerCase(); + + if (value.contains('stunting')) { + return const NutritionStatusVisual( + color: SgColors.warning, + icon: Icons.height_rounded, + badgeLabel: 'Perlu Perhatian', + summary: 'Pertumbuhan tinggi badan perlu perhatian dan pemantauan rutin.', + ); + } + if (value.contains('buruk')) { + return const NutritionStatusVisual( + color: SgColors.danger, + icon: Icons.warning_amber_rounded, + badgeLabel: 'Perlu Tindak Lanjut', + summary: 'Status gizi perlu tindak lanjut cepat bersama tenaga kesehatan.', + ); + } + if (value.contains('kurang') || value.contains('underweight')) { + return const NutritionStatusVisual( + color: SgColors.warning, + icon: Icons.monitor_weight_outlined, + badgeLabel: 'Perlu Perhatian', + summary: 'Asupan energi dan protein perlu diperkuat secara konsisten.', + ); + } + if (value.contains('obesitas') || value.contains('lebih')) { + return const NutritionStatusVisual( + color: Color(0xFFF97316), + icon: Icons.balance_rounded, + badgeLabel: 'Perlu Pemantauan', + summary: 'Porsi dan kualitas makan perlu dijaga agar pertumbuhan tetap seimbang.', + ); + } + + return const NutritionStatusVisual( + color: SgColors.success, + icon: Icons.favorite_rounded, + badgeLabel: 'Pertumbuhan Baik', + summary: 'Pertumbuhan sesuai jalur dan tetap perlu dipantau secara berkala.', + ); +} + +String formatMeasurementDate(String rawDate) { + final date = DateTime.tryParse(rawDate); + if (date == null) return rawDate; + + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des', + ]; + + return '${date.day.toString().padLeft(2, '0')} ${months[date.month - 1]} ${date.year}'; +} + +String formatAgeFromBirthDate(String rawBirthDate, {String? onDate}) { + final birthDate = DateTime.tryParse(rawBirthDate); + final referenceDate = DateTime.tryParse(onDate ?? '') ?? DateTime.now(); + if (birthDate == null) return '-'; + + int months = + (referenceDate.year - birthDate.year) * 12 + + (referenceDate.month - birthDate.month); + + if (referenceDate.day < birthDate.day) { + months -= 1; + } + + return formatAgeFromMonths(months.toDouble()); +} + +String formatAgeFromMonths(double months) { + if (months.isNaN || months.isInfinite || months < 0) return '-'; + + final totalMonths = months.round(); + final years = totalMonths ~/ 12; + final remainingMonths = totalMonths % 12; + + if (years <= 0) { + return '$remainingMonths Bulan'; + } + if (remainingMonths == 0) { + return '$years Tahun'; + } + return '$years Tahun $remainingMonths Bulan'; +} + +String genderLabel(String gender) { + if (gender.toLowerCase().startsWith('p')) return 'Perempuan'; + return 'Laki-laki'; +} + +String measurementMethodLabel(String? rawMethod, double ageMonths) { + final value = (rawMethod ?? '').toLowerCase(); + if (value == 'lying') return 'Terlentang'; + if (value == 'standing') return 'Berdiri'; + if (ageMonths < 24) return 'Terlentang'; + return 'Berdiri'; +} + +String formatScore(double? value) { + if (value == null || value.isNaN || value.isInfinite) return '-'; + return value.toStringAsFixed(2); +} + +String bbuCategoryFromScore(double? score, {String? fallback}) { + if (fallback != null && fallback.trim().isNotEmpty) return fallback; + if (score == null || score.isNaN || score.isInfinite) return '-'; + if (score < -3) return 'Sangat Kurang'; + if (score < -2) return 'Kurang'; + if (score <= 1) return 'Normal'; + return 'Risiko Lebih'; +} + +String tbuCategoryFromScore(double? score, {String? fallback}) { + if (fallback != null && fallback.trim().isNotEmpty) return fallback; + if (score == null || score.isNaN || score.isInfinite) return '-'; + if (score < -3) return 'Sangat Pendek'; + if (score < -2) return 'Pendek'; + if (score <= 3) return 'Normal'; + return 'Tinggi'; +} + +String bbtbCategoryFromScore(double? score, {String? fallback}) { + if (fallback != null && fallback.trim().isNotEmpty) return fallback; + if (score == null || score.isNaN || score.isInfinite) return '-'; + if (score < -3) return 'Gizi Buruk'; + if (score < -2) return 'Kurang'; + if (score <= 1) return 'Normal'; + if (score <= 2) return 'Risiko Gizi Lebih'; + if (score <= 3) return 'Gizi Lebih'; + return 'Obesitas'; +} + +String buildInterpretation({ + required String status, + required String bbuCategory, + required String tbuCategory, + required String bbtbCategory, +}) { + final value = status.toLowerCase(); + + if (value.contains('stunting')) { + return 'Anak mengalami stunting ($tbuCategory) dibandingkan usia. ' + 'Status BB/TB saat ini $bbtbCategory dan BB/U $bbuCategory.'; + } + if (value.contains('buruk') || value.contains('kurang')) { + return 'Berat badan perlu perhatian. BB/U tercatat $bbuCategory dan proporsi BB/TB $bbtbCategory.'; + } + if (value.contains('obesitas') || value.contains('lebih')) { + return 'Proporsi berat terhadap tinggi menunjukkan $bbtbCategory. ' + 'Pengaturan porsi dan kualitas asupan perlu dipantau.'; + } + + return 'Pertumbuhan saat ini berada pada jalur yang baik. ' + 'BB/U $bbuCategory, TB/U $tbuCategory, dan BB/TB $bbtbCategory.'; +} + +String recommendationStatusExplanation(String status) { + final normalized = normalizeStatus(status); + + if (normalized.isNormal) { + return 'Status gizi terbaru berada pada kategori normal. Menu diarahkan untuk menjaga pola makan yang seimbang.'; + } + + final parts = []; + if (normalized.hasStunting) { + parts.add('pertumbuhan tinggi badan perlu dukungan nutrisi'); + } + if (normalized.hasUnderweight || normalized.hasWasting) { + parts.add('asupan energi dan protein perlu diperkuat'); + } + if (normalized.hasObesitas) { + parts.add('berat badan perlu dijaga dengan menu lebih terkontrol'); + } + + return '${normalized.originalStatus}: ${parts.join(', ')}.'; +} diff --git a/Android/flutter_app/linux/.gitignore b/Android/flutter_app/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/Android/flutter_app/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/Android/flutter_app/linux/CMakeLists.txt b/Android/flutter_app/linux/CMakeLists.txt new file mode 100644 index 0000000..b6455af --- /dev/null +++ b/Android/flutter_app/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "s_gizi") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.s_gizi") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/Android/flutter_app/linux/flutter/CMakeLists.txt b/Android/flutter_app/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/Android/flutter_app/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/Android/flutter_app/linux/flutter/generated_plugin_registrant.cc b/Android/flutter_app/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..f6f23bf --- /dev/null +++ b/Android/flutter_app/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/Android/flutter_app/linux/flutter/generated_plugin_registrant.h b/Android/flutter_app/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/Android/flutter_app/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/Android/flutter_app/linux/flutter/generated_plugins.cmake b/Android/flutter_app/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..df8d2f7 --- /dev/null +++ b/Android/flutter_app/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/Android/flutter_app/linux/runner/CMakeLists.txt b/Android/flutter_app/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/Android/flutter_app/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/Android/flutter_app/linux/runner/main.cc b/Android/flutter_app/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/Android/flutter_app/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/Android/flutter_app/linux/runner/my_application.cc b/Android/flutter_app/linux/runner/my_application.cc new file mode 100644 index 0000000..334ffdb --- /dev/null +++ b/Android/flutter_app/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "s_gizi"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "s_gizi"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/Android/flutter_app/linux/runner/my_application.h b/Android/flutter_app/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/Android/flutter_app/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/Android/flutter_app/macos/.gitignore b/Android/flutter_app/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/Android/flutter_app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/Android/flutter_app/macos/Flutter/Flutter-Debug.xcconfig b/Android/flutter_app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/Android/flutter_app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/Android/flutter_app/macos/Flutter/Flutter-Release.xcconfig b/Android/flutter_app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/Android/flutter_app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/Android/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift b/Android/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..9889731 --- /dev/null +++ b/Android/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import flutter_local_notifications +import sqflite_darwin +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/Android/flutter_app/macos/Runner.xcodeproj/project.pbxproj b/Android/flutter_app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..aee834c --- /dev/null +++ b/Android/flutter_app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* s_gizi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "s_gizi.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* s_gizi.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* s_gizi.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/s_gizi.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/s_gizi"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/s_gizi.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/s_gizi"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/s_gizi.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/s_gizi"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/Android/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Android/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Android/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Android/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/Android/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..4cea577 --- /dev/null +++ b/Android/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata b/Android/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/Android/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Android/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Android/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Android/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Android/flutter_app/macos/Runner/AppDelegate.swift b/Android/flutter_app/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/Android/flutter_app/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/Android/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/Android/flutter_app/macos/Runner/Base.lproj/MainMenu.xib b/Android/flutter_app/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/Android/flutter_app/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/flutter_app/macos/Runner/Configs/AppInfo.xcconfig b/Android/flutter_app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..7a8aaad --- /dev/null +++ b/Android/flutter_app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = s_gizi + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.sGizi + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/Android/flutter_app/macos/Runner/Configs/Debug.xcconfig b/Android/flutter_app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/Android/flutter_app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/Android/flutter_app/macos/Runner/Configs/Release.xcconfig b/Android/flutter_app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/Android/flutter_app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/Android/flutter_app/macos/Runner/Configs/Warnings.xcconfig b/Android/flutter_app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/Android/flutter_app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/Android/flutter_app/macos/Runner/DebugProfile.entitlements b/Android/flutter_app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/Android/flutter_app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/Android/flutter_app/macos/Runner/Info.plist b/Android/flutter_app/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/Android/flutter_app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/Android/flutter_app/macos/Runner/MainFlutterWindow.swift b/Android/flutter_app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/Android/flutter_app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/Android/flutter_app/macos/Runner/Release.entitlements b/Android/flutter_app/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/Android/flutter_app/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/Android/flutter_app/macos/RunnerTests/RunnerTests.swift b/Android/flutter_app/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/Android/flutter_app/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/Android/flutter_app/pubspec.lock b/Android/flutter_app/pubspec.lock new file mode 100644 index 0000000..2fa7a2a --- /dev/null +++ b/Android/flutter_app/pubspec.lock @@ -0,0 +1,786 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + animated_text_kit: + dependency: "direct main" + description: + name: animated_text_kit + sha256: "1d41486860885321972dd332179357a535d50d4780fc2ba5e1805bde085c81f2" + url: "https://pub.dev" + source: hosted + version: "4.3.0" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + bootstrap_icons: + dependency: "direct main" + description: + name: bootstrap_icons + sha256: "74ecde048cc51f35bd972a391cbf61741424d1a5524bcb5e5aa3fb7b08fcf71d" + url: "https://pub.dev" + source: hosted + version: "1.13.1+1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: febf4b0163e0242adc13d7a863b04965351f59e7dfea56675c7c2caa7bcd7476 + url: "https://pub.dev" + source: hosted + version: "5.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + url: "https://pub.dev" + source: hosted + version: "17.2.4" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: "09dcde8ab90ffae1a7d65ff2ef96fc62a17ad9d0ce7c127b317ded676b0d5935" + url: "https://pub.dev" + source: hosted + version: "11.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: "8b6359a7422167014aa73ce763fa133fb832065dcc0ac4d1dec1f603a5cef7d0" + url: "https://pub.dev" + source: hosted + version: "3.3.3" + lucide_icons: + dependency: "direct main" + description: + name: lucide_icons + sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 + url: "https://pub.dev" + source: hosted + version: "0.257.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + phosphor_flutter: + dependency: "direct main" + description: + name: phosphor_flutter + sha256: "8a14f238f28a0b54842c5a4dc20676598dd4811fcba284ed828bd5a262c11fde" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a" + url: "https://pub.dev" + source: hosted + version: "2.4.2+1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" + url: "https://pub.dev" + source: hosted + version: "2.4.2+3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: f8a08a13fb8f0f8c590df89d745000bed44a673ed94bac846739e1a016875c21 + url: "https://pub.dev" + source: hosted + version: "2.5.7" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" + url: "https://pub.dev" + source: hosted + version: "3.4.0+1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + url: "https://pub.dev" + source: hosted + version: "6.3.29" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.38.4" diff --git a/Android/flutter_app/pubspec.yaml b/Android/flutter_app/pubspec.yaml new file mode 100644 index 0000000..22c1281 --- /dev/null +++ b/Android/flutter_app/pubspec.yaml @@ -0,0 +1,102 @@ +name: s_gizi +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.10.3 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + http: ^1.2.2 + flutter_animate: ^4.5.2 + lottie: ^3.3.3 + animated_text_kit: ^4.3.0 + bootstrap_icons: ^1.13.1+1 + phosphor_flutter: ^2.1.0 + lucide_icons: ^0.257.0 + font_awesome_flutter: ^11.0.0 + shimmer: ^3.0.0 + google_fonts: ^8.1.0 + cached_network_image: ^3.4.1 + url_launcher: ^6.3.2 + carousel_slider: ^5.0.0 + flutter_local_notifications: ^17.2.4 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + assets: + - assets/data/lms_who_final.json + - assets/image/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/Android/flutter_app/test/calculation_service_test.dart b/Android/flutter_app/test/calculation_service_test.dart new file mode 100644 index 0000000..660cfa1 --- /dev/null +++ b/Android/flutter_app/test/calculation_service_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:s_gizi/models/child_model.dart'; +import 'package:s_gizi/services/calculation_service.dart'; +import 'package:s_gizi/services/data_service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('ChildModel', () { + test('normalizes gender, position, and adjusted height correctly', () { + const child = ChildModel( + ageInMonths: 18, + weightKg: 10.2, + heightCm: 78, + gender: 'L', + measurementPosition: 'berdiri', + ); + + expect(child.normalizedGender, 'male'); + expect(child.normalizedPosition, 'standing'); + expect(child.weightForHeightIndicator, 'wfl'); + expect(child.adjustedHeightCm, closeTo(78.7, 0.0001)); + }); + }); + + group('CalculationService', () { + final service = CalculationService(dataService: DataService.instance); + + test('matches WHO sample calculation for male child', () async { + const child = ChildModel( + ageInMonths: 18, + weightKg: 10.2, + heightCm: 78, + gender: 'male', + measurementPosition: 'standing', + ); + + final result = await service.calculate(child); + + expect(result.weightForHeightIndicator, 'wfl'); + expect(result.adjustedHeightCm, closeTo(78.7, 0.0001)); + expect(result.bbU, closeTo(-0.6282, 0.0001)); + expect(result.tbU, closeTo(-1.5789, 0.0001)); + expect(result.bbTb, closeTo(-0.0123, 0.0001)); + }); + + test('matches WHO sample calculation for female child', () async { + const child = ChildModel( + ageInMonths: 30, + weightKg: 12.4, + heightCm: 91.3, + gender: 'perempuan', + measurementPosition: 'recumbent', + ); + + final result = await service.calculate(child); + + expect(result.weightForHeightIndicator, 'wfh'); + expect(result.adjustedHeightCm, closeTo(91.3, 0.0001)); + expect(result.bbU, closeTo(-0.1941, 0.0001)); + expect(result.tbU, closeTo(0.1757, 0.0001)); + expect(result.bbTb, closeTo(-0.5064, 0.0001)); + }); + }); +} diff --git a/Android/flutter_app/test/widget_test.dart b/Android/flutter_app/test/widget_test.dart new file mode 100644 index 0000000..e69de29 diff --git a/Android/flutter_app/web/favicon.png b/Android/flutter_app/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/Android/flutter_app/web/favicon.png differ diff --git a/Android/flutter_app/web/icons/Icon-192.png b/Android/flutter_app/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/Android/flutter_app/web/icons/Icon-192.png differ diff --git a/Android/flutter_app/web/icons/Icon-512.png b/Android/flutter_app/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/Android/flutter_app/web/icons/Icon-512.png differ diff --git a/Android/flutter_app/web/icons/Icon-maskable-192.png b/Android/flutter_app/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/Android/flutter_app/web/icons/Icon-maskable-192.png differ diff --git a/Android/flutter_app/web/icons/Icon-maskable-512.png b/Android/flutter_app/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/Android/flutter_app/web/icons/Icon-maskable-512.png differ diff --git a/Android/flutter_app/web/index.html b/Android/flutter_app/web/index.html new file mode 100644 index 0000000..3abe6af --- /dev/null +++ b/Android/flutter_app/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + s_gizi + + + + + + diff --git a/Android/flutter_app/web/manifest.json b/Android/flutter_app/web/manifest.json new file mode 100644 index 0000000..20aaca1 --- /dev/null +++ b/Android/flutter_app/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "s_gizi", + "short_name": "s_gizi", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/Android/flutter_app/windows/.gitignore b/Android/flutter_app/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/Android/flutter_app/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/Android/flutter_app/windows/CMakeLists.txt b/Android/flutter_app/windows/CMakeLists.txt new file mode 100644 index 0000000..6e52072 --- /dev/null +++ b/Android/flutter_app/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(s_gizi LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "s_gizi") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/Android/flutter_app/windows/flutter/CMakeLists.txt b/Android/flutter_app/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/Android/flutter_app/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/Android/flutter_app/windows/flutter/generated_plugin_registrant.cc b/Android/flutter_app/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..4f78848 --- /dev/null +++ b/Android/flutter_app/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/Android/flutter_app/windows/flutter/generated_plugin_registrant.h b/Android/flutter_app/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/Android/flutter_app/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/Android/flutter_app/windows/flutter/generated_plugins.cmake b/Android/flutter_app/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..a962892 --- /dev/null +++ b/Android/flutter_app/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/Android/flutter_app/windows/runner/CMakeLists.txt b/Android/flutter_app/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/Android/flutter_app/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/Android/flutter_app/windows/runner/Runner.rc b/Android/flutter_app/windows/runner/Runner.rc new file mode 100644 index 0000000..f8985a4 --- /dev/null +++ b/Android/flutter_app/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "s_gizi" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "s_gizi" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "s_gizi.exe" "\0" + VALUE "ProductName", "s_gizi" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/Android/flutter_app/windows/runner/flutter_window.cpp b/Android/flutter_app/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/Android/flutter_app/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/Android/flutter_app/windows/runner/flutter_window.h b/Android/flutter_app/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/Android/flutter_app/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/Android/flutter_app/windows/runner/main.cpp b/Android/flutter_app/windows/runner/main.cpp new file mode 100644 index 0000000..5f4c664 --- /dev/null +++ b/Android/flutter_app/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"s_gizi", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/Android/flutter_app/windows/runner/resource.h b/Android/flutter_app/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/Android/flutter_app/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/Android/flutter_app/windows/runner/resources/app_icon.ico b/Android/flutter_app/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/Android/flutter_app/windows/runner/resources/app_icon.ico differ diff --git a/Android/flutter_app/windows/runner/runner.exe.manifest b/Android/flutter_app/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/Android/flutter_app/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/Android/flutter_app/windows/runner/utils.cpp b/Android/flutter_app/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/Android/flutter_app/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/Android/flutter_app/windows/runner/utils.h b/Android/flutter_app/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/Android/flutter_app/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/Android/flutter_app/windows/runner/win32_window.cpp b/Android/flutter_app/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/Android/flutter_app/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/Android/flutter_app/windows/runner/win32_window.h b/Android/flutter_app/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/Android/flutter_app/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/Data LMS/lms_who_final.json b/Data LMS/lms_who_final.json new file mode 100644 index 0000000..54e77da --- /dev/null +++ b/Data LMS/lms_who_final.json @@ -0,0 +1,3662 @@ +{ + "male": { + "wfa": { + "0": { + "L": 0.3487, + "M": 3.3464, + "S": 0.14602 + }, + "1": { + "L": 0.2297, + "M": 4.4709, + "S": 0.13395 + }, + "2": { + "L": 0.197, + "M": 5.5675, + "S": 0.12385 + }, + "3": { + "L": 0.1738, + "M": 6.3762, + "S": 0.11727 + }, + "4": { + "L": 0.1553, + "M": 7.0023, + "S": 0.11316 + }, + "5": { + "L": 0.1395, + "M": 7.5105, + "S": 0.1108 + }, + "6": { + "L": 0.1257, + "M": 7.934, + "S": 0.10958 + }, + "7": { + "L": 0.1134, + "M": 8.297, + "S": 0.10902 + }, + "8": { + "L": 0.1021, + "M": 8.6151, + "S": 0.10882 + }, + "9": { + "L": 0.0917, + "M": 8.9014, + "S": 0.10881 + }, + "10": { + "L": 0.082, + "M": 9.1649, + "S": 0.10891 + }, + "11": { + "L": 0.073, + "M": 9.4122, + "S": 0.10906 + }, + "12": { + "L": 0.0644, + "M": 9.6479, + "S": 0.10925 + }, + "13": { + "L": 0.0563, + "M": 9.8749, + "S": 0.10949 + }, + "14": { + "L": 0.0487, + "M": 10.0953, + "S": 0.10976 + }, + "15": { + "L": 0.0413, + "M": 10.3108, + "S": 0.11007 + }, + "16": { + "L": 0.0343, + "M": 10.5228, + "S": 0.11041 + }, + "17": { + "L": 0.0275, + "M": 10.7319, + "S": 0.11079 + }, + "18": { + "L": 0.0211, + "M": 10.9385, + "S": 0.11119 + }, + "19": { + "L": 0.0148, + "M": 11.143, + "S": 0.11164 + }, + "20": { + "L": 0.0087, + "M": 11.3462, + "S": 0.11211 + }, + "21": { + "L": 0.0029, + "M": 11.5486, + "S": 0.11261 + }, + "22": { + "L": -0.0028, + "M": 11.7504, + "S": 0.11314 + }, + "23": { + "L": -0.0083, + "M": 11.9514, + "S": 0.11369 + }, + "24": { + "L": -0.0137, + "M": 12.1515, + "S": 0.11426 + }, + "25": { + "L": -0.0189, + "M": 12.3502, + "S": 0.11485 + }, + "26": { + "L": -0.024, + "M": 12.5466, + "S": 0.11544 + }, + "27": { + "L": -0.0289, + "M": 12.7401, + "S": 0.11604 + }, + "28": { + "L": -0.0337, + "M": 12.9303, + "S": 0.11664 + }, + "29": { + "L": -0.0385, + "M": 13.1169, + "S": 0.11723 + }, + "30": { + "L": -0.0431, + "M": 13.3, + "S": 0.11781 + }, + "31": { + "L": -0.0476, + "M": 13.4798, + "S": 0.11839 + }, + "32": { + "L": -0.052, + "M": 13.6567, + "S": 0.11896 + }, + "33": { + "L": -0.0564, + "M": 13.8309, + "S": 0.11953 + }, + "34": { + "L": -0.0606, + "M": 14.0031, + "S": 0.12008 + }, + "35": { + "L": -0.0648, + "M": 14.1736, + "S": 0.12062 + }, + "36": { + "L": -0.0689, + "M": 14.3429, + "S": 0.12116 + }, + "37": { + "L": -0.0729, + "M": 14.5113, + "S": 0.12168 + }, + "38": { + "L": -0.0769, + "M": 14.6791, + "S": 0.1222 + }, + "39": { + "L": -0.0808, + "M": 14.8466, + "S": 0.12271 + }, + "40": { + "L": -0.0846, + "M": 15.014, + "S": 0.12322 + }, + "41": { + "L": -0.0883, + "M": 15.1813, + "S": 0.12373 + }, + "42": { + "L": -0.092, + "M": 15.3486, + "S": 0.12425 + }, + "43": { + "L": -0.0957, + "M": 15.5158, + "S": 0.12478 + }, + "44": { + "L": -0.0993, + "M": 15.6828, + "S": 0.12531 + }, + "45": { + "L": -0.1028, + "M": 15.8497, + "S": 0.12586 + }, + "46": { + "L": -0.1063, + "M": 16.0163, + "S": 0.12643 + }, + "47": { + "L": -0.1097, + "M": 16.1827, + "S": 0.127 + }, + "48": { + "L": -0.1131, + "M": 16.3489, + "S": 0.12759 + }, + "49": { + "L": -0.1165, + "M": 16.515, + "S": 0.12819 + }, + "50": { + "L": -0.1198, + "M": 16.6811, + "S": 0.1288 + }, + "51": { + "L": -0.123, + "M": 16.8471, + "S": 0.12943 + }, + "52": { + "L": -0.1262, + "M": 17.0132, + "S": 0.13005 + }, + "53": { + "L": -0.1294, + "M": 17.1792, + "S": 0.13069 + }, + "54": { + "L": -0.1325, + "M": 17.3452, + "S": 0.13133 + }, + "55": { + "L": -0.1356, + "M": 17.5111, + "S": 0.13197 + }, + "56": { + "L": -0.1387, + "M": 17.6768, + "S": 0.13261 + }, + "57": { + "L": -0.1417, + "M": 17.8422, + "S": 0.13325 + }, + "58": { + "L": -0.1447, + "M": 18.0073, + "S": 0.13389 + }, + "59": { + "L": -0.1477, + "M": 18.1722, + "S": 0.13453 + }, + "60": { + "L": -0.1506, + "M": 18.3366, + "S": 0.13517 + } + }, + "hfa": { + "0": { + "L": 1.0, + "M": 49.8842, + "S": 0.03795 + }, + "1": { + "L": 1.0, + "M": 54.7244, + "S": 0.03557 + }, + "2": { + "L": 1.0, + "M": 58.4249, + "S": 0.03424 + }, + "3": { + "L": 1.0, + "M": 61.4292, + "S": 0.03328 + }, + "4": { + "L": 1.0, + "M": 63.886, + "S": 0.03257 + }, + "5": { + "L": 1.0, + "M": 65.9026, + "S": 0.03204 + }, + "6": { + "L": 1.0, + "M": 67.6236, + "S": 0.03165 + }, + "7": { + "L": 1.0, + "M": 69.1645, + "S": 0.03139 + }, + "8": { + "L": 1.0, + "M": 70.5994, + "S": 0.03124 + }, + "9": { + "L": 1.0, + "M": 71.9687, + "S": 0.03117 + }, + "10": { + "L": 1.0, + "M": 73.2812, + "S": 0.03118 + }, + "11": { + "L": 1.0, + "M": 74.5388, + "S": 0.03125 + }, + "12": { + "L": 1.0, + "M": 75.7488, + "S": 0.03137 + }, + "13": { + "L": 1.0, + "M": 76.9186, + "S": 0.03154 + }, + "14": { + "L": 1.0, + "M": 78.0497, + "S": 0.03174 + }, + "15": { + "L": 1.0, + "M": 79.1458, + "S": 0.03197 + }, + "16": { + "L": 1.0, + "M": 80.2113, + "S": 0.03222 + }, + "17": { + "L": 1.0, + "M": 81.2487, + "S": 0.0325 + }, + "18": { + "L": 1.0, + "M": 82.2587, + "S": 0.03279 + }, + "19": { + "L": 1.0, + "M": 83.2418, + "S": 0.0331 + }, + "20": { + "L": 1.0, + "M": 84.1996, + "S": 0.03342 + }, + "21": { + "L": 1.0, + "M": 85.1348, + "S": 0.03376 + }, + "22": { + "L": 1.0, + "M": 86.0477, + "S": 0.0341 + }, + "23": { + "L": 1.0, + "M": 86.941, + "S": 0.03445 + }, + "24": { + "L": 1.0, + "M": 87.1161, + "S": 0.03507 + }, + "25": { + "L": 1.0, + "M": 87.972, + "S": 0.03542 + }, + "26": { + "L": 1.0, + "M": 88.8065, + "S": 0.03576 + }, + "27": { + "L": 1.0, + "M": 89.6197, + "S": 0.0361 + }, + "28": { + "L": 1.0, + "M": 90.412, + "S": 0.03642 + }, + "29": { + "L": 1.0, + "M": 91.1828, + "S": 0.03674 + }, + "30": { + "L": 1.0, + "M": 91.9327, + "S": 0.03704 + }, + "31": { + "L": 1.0, + "M": 92.6631, + "S": 0.03733 + }, + "32": { + "L": 1.0, + "M": 93.3753, + "S": 0.03761 + }, + "33": { + "L": 1.0, + "M": 94.0711, + "S": 0.03787 + }, + "34": { + "L": 1.0, + "M": 94.7532, + "S": 0.03812 + }, + "35": { + "L": 1.0, + "M": 95.4236, + "S": 0.03836 + }, + "36": { + "L": 1.0, + "M": 96.0835, + "S": 0.03858 + }, + "37": { + "L": 1.0, + "M": 96.7337, + "S": 0.03879 + }, + "38": { + "L": 1.0, + "M": 97.3749, + "S": 0.039 + }, + "39": { + "L": 1.0, + "M": 98.0073, + "S": 0.03919 + }, + "40": { + "L": 1.0, + "M": 98.631, + "S": 0.03937 + }, + "41": { + "L": 1.0, + "M": 99.2459, + "S": 0.03954 + }, + "42": { + "L": 1.0, + "M": 99.8515, + "S": 0.03971 + }, + "43": { + "L": 1.0, + "M": 100.4485, + "S": 0.03986 + }, + "44": { + "L": 1.0, + "M": 101.0374, + "S": 0.04002 + }, + "45": { + "L": 1.0, + "M": 101.6186, + "S": 0.04016 + }, + "46": { + "L": 1.0, + "M": 102.1933, + "S": 0.04031 + }, + "47": { + "L": 1.0, + "M": 102.7625, + "S": 0.04045 + }, + "48": { + "L": 1.0, + "M": 103.3273, + "S": 0.04059 + }, + "49": { + "L": 1.0, + "M": 103.8886, + "S": 0.04073 + }, + "50": { + "L": 1.0, + "M": 104.4473, + "S": 0.04086 + }, + "51": { + "L": 1.0, + "M": 105.0041, + "S": 0.041 + }, + "52": { + "L": 1.0, + "M": 105.5596, + "S": 0.04113 + }, + "53": { + "L": 1.0, + "M": 106.1138, + "S": 0.04126 + }, + "54": { + "L": 1.0, + "M": 106.6668, + "S": 0.04139 + }, + "55": { + "L": 1.0, + "M": 107.2188, + "S": 0.04152 + }, + "56": { + "L": 1.0, + "M": 107.7697, + "S": 0.04165 + }, + "57": { + "L": 1.0, + "M": 108.3198, + "S": 0.04177 + }, + "58": { + "L": 1.0, + "M": 108.8689, + "S": 0.0419 + }, + "59": { + "L": 1.0, + "M": 109.417, + "S": 0.04202 + }, + "60": { + "L": 1.0, + "M": 109.9638, + "S": 0.04214 + } + }, + "wfl": { + "45.0": { + "L": -0.3521, + "M": 2.441, + "S": 0.09182 + }, + "45.5": { + "L": -0.3521, + "M": 2.5244, + "S": 0.09153 + }, + "46.0": { + "L": -0.3521, + "M": 2.6077, + "S": 0.09124 + }, + "46.5": { + "L": -0.3521, + "M": 2.6913, + "S": 0.09094 + }, + "47.0": { + "L": -0.3521, + "M": 2.7755, + "S": 0.09065 + }, + "47.5": { + "L": -0.3521, + "M": 2.8609, + "S": 0.09036 + }, + "48.0": { + "L": -0.3521, + "M": 2.948, + "S": 0.09007 + }, + "48.5": { + "L": -0.3521, + "M": 3.0377, + "S": 0.08977 + }, + "49.0": { + "L": -0.3521, + "M": 3.1308, + "S": 0.08948 + }, + "49.5": { + "L": -0.3521, + "M": 3.2276, + "S": 0.08919 + }, + "50.0": { + "L": -0.3521, + "M": 3.3278, + "S": 0.0889 + }, + "50.5": { + "L": -0.3521, + "M": 3.4311, + "S": 0.08861 + }, + "51.0": { + "L": -0.3521, + "M": 3.5376, + "S": 0.08831 + }, + "51.5": { + "L": -0.3521, + "M": 3.6477, + "S": 0.08801 + }, + "52.0": { + "L": -0.3521, + "M": 3.762, + "S": 0.08771 + }, + "52.5": { + "L": -0.3521, + "M": 3.8814, + "S": 0.08741 + }, + "53.0": { + "L": -0.3521, + "M": 4.006, + "S": 0.08711 + }, + "53.5": { + "L": -0.3521, + "M": 4.1354, + "S": 0.08681 + }, + "54.0": { + "L": -0.3521, + "M": 4.2693, + "S": 0.08651 + }, + "54.5": { + "L": -0.3521, + "M": 4.4066, + "S": 0.08621 + }, + "55.0": { + "L": -0.3521, + "M": 4.5467, + "S": 0.08592 + }, + "55.5": { + "L": -0.3521, + "M": 4.6892, + "S": 0.08563 + }, + "56.0": { + "L": -0.3521, + "M": 4.8338, + "S": 0.08535 + }, + "56.5": { + "L": -0.3521, + "M": 4.9796, + "S": 0.08507 + }, + "57.0": { + "L": -0.3521, + "M": 5.1259, + "S": 0.08481 + }, + "57.5": { + "L": -0.3521, + "M": 5.2721, + "S": 0.08455 + }, + "58.0": { + "L": -0.3521, + "M": 5.418, + "S": 0.0843 + }, + "58.5": { + "L": -0.3521, + "M": 5.5632, + "S": 0.08406 + }, + "59.0": { + "L": -0.3521, + "M": 5.7074, + "S": 0.08383 + }, + "59.5": { + "L": -0.3521, + "M": 5.8501, + "S": 0.08362 + }, + "60.0": { + "L": -0.3521, + "M": 5.9907, + "S": 0.08342 + }, + "60.5": { + "L": -0.3521, + "M": 6.1284, + "S": 0.08324 + }, + "61.0": { + "L": -0.3521, + "M": 6.2632, + "S": 0.08308 + }, + "61.5": { + "L": -0.3521, + "M": 6.3954, + "S": 0.08292 + }, + "62.0": { + "L": -0.3521, + "M": 6.5251, + "S": 0.08279 + }, + "62.5": { + "L": -0.3521, + "M": 6.6527, + "S": 0.08266 + }, + "63.0": { + "L": -0.3521, + "M": 6.7786, + "S": 0.08255 + }, + "63.5": { + "L": -0.3521, + "M": 6.9028, + "S": 0.08245 + }, + "64.0": { + "L": -0.3521, + "M": 7.0255, + "S": 0.08236 + }, + "64.5": { + "L": -0.3521, + "M": 7.1467, + "S": 0.08229 + }, + "65.0": { + "L": -0.3521, + "M": 7.2666, + "S": 0.08223 + }, + "65.5": { + "L": -0.3521, + "M": 7.3854, + "S": 0.08218 + }, + "66.0": { + "L": -0.3521, + "M": 7.5034, + "S": 0.08215 + }, + "66.5": { + "L": -0.3521, + "M": 7.6206, + "S": 0.08213 + }, + "67.0": { + "L": -0.3521, + "M": 7.737, + "S": 0.08212 + }, + "67.5": { + "L": -0.3521, + "M": 7.8526, + "S": 0.08212 + }, + "68.0": { + "L": -0.3521, + "M": 7.9674, + "S": 0.08214 + }, + "68.5": { + "L": -0.3521, + "M": 8.0816, + "S": 0.08216 + }, + "69.0": { + "L": -0.3521, + "M": 8.1955, + "S": 0.08219 + }, + "69.5": { + "L": -0.3521, + "M": 8.3092, + "S": 0.08224 + }, + "70.0": { + "L": -0.3521, + "M": 8.4227, + "S": 0.08229 + }, + "70.5": { + "L": -0.3521, + "M": 8.5358, + "S": 0.08235 + }, + "71.0": { + "L": -0.3521, + "M": 8.648, + "S": 0.08241 + }, + "71.5": { + "L": -0.3521, + "M": 8.7594, + "S": 0.08248 + }, + "72.0": { + "L": -0.3521, + "M": 8.8697, + "S": 0.08254 + }, + "72.5": { + "L": -0.3521, + "M": 8.9788, + "S": 0.08262 + }, + "73.0": { + "L": -0.3521, + "M": 9.0865, + "S": 0.08269 + }, + "73.5": { + "L": -0.3521, + "M": 9.1927, + "S": 0.08276 + }, + "74.0": { + "L": -0.3521, + "M": 9.2974, + "S": 0.08283 + }, + "74.5": { + "L": -0.3521, + "M": 9.401, + "S": 0.08289 + }, + "75.0": { + "L": -0.3521, + "M": 9.5032, + "S": 0.08295 + }, + "75.5": { + "L": -0.3521, + "M": 9.6041, + "S": 0.08301 + }, + "76.0": { + "L": -0.3521, + "M": 9.7033, + "S": 0.08307 + }, + "76.5": { + "L": -0.3521, + "M": 9.8007, + "S": 0.08311 + }, + "77.0": { + "L": -0.3521, + "M": 9.8963, + "S": 0.08314 + }, + "77.5": { + "L": -0.3521, + "M": 9.9902, + "S": 0.08317 + }, + "78.0": { + "L": -0.3521, + "M": 10.0827, + "S": 0.08318 + }, + "78.5": { + "L": -0.3521, + "M": 10.1741, + "S": 0.08318 + }, + "79.0": { + "L": -0.3521, + "M": 10.2649, + "S": 0.08316 + }, + "79.5": { + "L": -0.3521, + "M": 10.3558, + "S": 0.08313 + }, + "80.0": { + "L": -0.3521, + "M": 10.4475, + "S": 0.08308 + }, + "80.5": { + "L": -0.3521, + "M": 10.5405, + "S": 0.08301 + }, + "81.0": { + "L": -0.3521, + "M": 10.6352, + "S": 0.08293 + }, + "81.5": { + "L": -0.3521, + "M": 10.7322, + "S": 0.08284 + }, + "82.0": { + "L": -0.3521, + "M": 10.8321, + "S": 0.08273 + }, + "82.5": { + "L": -0.3521, + "M": 10.935, + "S": 0.0826 + }, + "83.0": { + "L": -0.3521, + "M": 11.0415, + "S": 0.08246 + }, + "83.5": { + "L": -0.3521, + "M": 11.1516, + "S": 0.08231 + }, + "84.0": { + "L": -0.3521, + "M": 11.2651, + "S": 0.08215 + }, + "84.5": { + "L": -0.3521, + "M": 11.3817, + "S": 0.08198 + }, + "85.0": { + "L": -0.3521, + "M": 11.5007, + "S": 0.08181 + }, + "85.5": { + "L": -0.3521, + "M": 11.6218, + "S": 0.08163 + }, + "86.0": { + "L": -0.3521, + "M": 11.7444, + "S": 0.08145 + }, + "86.5": { + "L": -0.3521, + "M": 11.8678, + "S": 0.08128 + }, + "87.0": { + "L": -0.3521, + "M": 11.9916, + "S": 0.08111 + }, + "87.5": { + "L": -0.3521, + "M": 12.1152, + "S": 0.08096 + }, + "88.0": { + "L": -0.3521, + "M": 12.2382, + "S": 0.08082 + }, + "88.5": { + "L": -0.3521, + "M": 12.3603, + "S": 0.08069 + }, + "89.0": { + "L": -0.3521, + "M": 12.4815, + "S": 0.08058 + }, + "89.5": { + "L": -0.3521, + "M": 12.6017, + "S": 0.08048 + }, + "90.0": { + "L": -0.3521, + "M": 12.7209, + "S": 0.08041 + }, + "90.5": { + "L": -0.3521, + "M": 12.8392, + "S": 0.08034 + }, + "91.0": { + "L": -0.3521, + "M": 12.9569, + "S": 0.0803 + }, + "91.5": { + "L": -0.3521, + "M": 13.0742, + "S": 0.08026 + }, + "92.0": { + "L": -0.3521, + "M": 13.191, + "S": 0.08025 + }, + "92.5": { + "L": -0.3521, + "M": 13.3075, + "S": 0.08025 + }, + "93.0": { + "L": -0.3521, + "M": 13.4239, + "S": 0.08026 + }, + "93.5": { + "L": -0.3521, + "M": 13.5404, + "S": 0.08029 + }, + "94.0": { + "L": -0.3521, + "M": 13.6572, + "S": 0.08034 + }, + "94.5": { + "L": -0.3521, + "M": 13.7746, + "S": 0.0804 + }, + "95.0": { + "L": -0.3521, + "M": 13.8928, + "S": 0.08047 + }, + "95.5": { + "L": -0.3521, + "M": 14.012, + "S": 0.08056 + }, + "96.0": { + "L": -0.3521, + "M": 14.1325, + "S": 0.08067 + }, + "96.5": { + "L": -0.3521, + "M": 14.2544, + "S": 0.08078 + }, + "97.0": { + "L": -0.3521, + "M": 14.3782, + "S": 0.08092 + }, + "97.5": { + "L": -0.3521, + "M": 14.5038, + "S": 0.08106 + }, + "98.0": { + "L": -0.3521, + "M": 14.6316, + "S": 0.08122 + }, + "98.5": { + "L": -0.3521, + "M": 14.7614, + "S": 0.08139 + }, + "99.0": { + "L": -0.3521, + "M": 14.8934, + "S": 0.08157 + }, + "99.5": { + "L": -0.3521, + "M": 15.0275, + "S": 0.08177 + }, + "100.0": { + "L": -0.3521, + "M": 15.1637, + "S": 0.08198 + }, + "100.5": { + "L": -0.3521, + "M": 15.3018, + "S": 0.0822 + }, + "101.0": { + "L": -0.3521, + "M": 15.4419, + "S": 0.08243 + }, + "101.5": { + "L": -0.3521, + "M": 15.5838, + "S": 0.08267 + }, + "102.0": { + "L": -0.3521, + "M": 15.7276, + "S": 0.08292 + }, + "102.5": { + "L": -0.3521, + "M": 15.8732, + "S": 0.08317 + }, + "103.0": { + "L": -0.3521, + "M": 16.0206, + "S": 0.08343 + }, + "103.5": { + "L": -0.3521, + "M": 16.1697, + "S": 0.0837 + }, + "104.0": { + "L": -0.3521, + "M": 16.3204, + "S": 0.08397 + }, + "104.5": { + "L": -0.3521, + "M": 16.4728, + "S": 0.08425 + }, + "105.0": { + "L": -0.3521, + "M": 16.6268, + "S": 0.08453 + }, + "105.5": { + "L": -0.3521, + "M": 16.7826, + "S": 0.08481 + }, + "106.0": { + "L": -0.3521, + "M": 16.9401, + "S": 0.0851 + }, + "106.5": { + "L": -0.3521, + "M": 17.0995, + "S": 0.08539 + }, + "107.0": { + "L": -0.3521, + "M": 17.2607, + "S": 0.08568 + }, + "107.5": { + "L": -0.3521, + "M": 17.4237, + "S": 0.08599 + }, + "108.0": { + "L": -0.3521, + "M": 17.5885, + "S": 0.08629 + }, + "108.5": { + "L": -0.3521, + "M": 17.7553, + "S": 0.0866 + }, + "109.0": { + "L": -0.3521, + "M": 17.9242, + "S": 0.08691 + }, + "109.5": { + "L": -0.3521, + "M": 18.0954, + "S": 0.08723 + }, + "110.0": { + "L": -0.3521, + "M": 18.2689, + "S": 0.08755 + } + }, + "wfh": { + "65.0": { + "L": -0.3521, + "M": 7.4327, + "S": 0.08217 + }, + "65.5": { + "L": -0.3521, + "M": 7.5504, + "S": 0.08214 + }, + "66.0": { + "L": -0.3521, + "M": 7.6673, + "S": 0.08212 + }, + "66.5": { + "L": -0.3521, + "M": 7.7834, + "S": 0.08212 + }, + "67.0": { + "L": -0.3521, + "M": 7.8986, + "S": 0.08213 + }, + "67.5": { + "L": -0.3521, + "M": 8.0132, + "S": 0.08214 + }, + "68.0": { + "L": -0.3521, + "M": 8.1272, + "S": 0.08217 + }, + "68.5": { + "L": -0.3521, + "M": 8.241, + "S": 0.08221 + }, + "69.0": { + "L": -0.3521, + "M": 8.3547, + "S": 0.08226 + }, + "69.5": { + "L": -0.3521, + "M": 8.468, + "S": 0.08231 + }, + "70.0": { + "L": -0.3521, + "M": 8.5808, + "S": 0.08237 + }, + "70.5": { + "L": -0.3521, + "M": 8.6927, + "S": 0.08243 + }, + "71.0": { + "L": -0.3521, + "M": 8.8036, + "S": 0.0825 + }, + "71.5": { + "L": -0.3521, + "M": 8.9135, + "S": 0.08257 + }, + "72.0": { + "L": -0.3521, + "M": 9.0221, + "S": 0.08264 + }, + "72.5": { + "L": -0.3521, + "M": 9.1292, + "S": 0.08272 + }, + "73.0": { + "L": -0.3521, + "M": 9.2347, + "S": 0.08278 + }, + "73.5": { + "L": -0.3521, + "M": 9.339, + "S": 0.08285 + }, + "74.0": { + "L": -0.3521, + "M": 9.442, + "S": 0.08292 + }, + "74.5": { + "L": -0.3521, + "M": 9.5438, + "S": 0.08298 + }, + "75.0": { + "L": -0.3521, + "M": 9.644, + "S": 0.08303 + }, + "75.5": { + "L": -0.3521, + "M": 9.7425, + "S": 0.08308 + }, + "76.0": { + "L": -0.3521, + "M": 9.8392, + "S": 0.08312 + }, + "76.5": { + "L": -0.3521, + "M": 9.9341, + "S": 0.08315 + }, + "77.0": { + "L": -0.3521, + "M": 10.0274, + "S": 0.08317 + }, + "77.5": { + "L": -0.3521, + "M": 10.1194, + "S": 0.08318 + }, + "78.0": { + "L": -0.3521, + "M": 10.2105, + "S": 0.08317 + }, + "78.5": { + "L": -0.3521, + "M": 10.3012, + "S": 0.08315 + }, + "79.0": { + "L": -0.3521, + "M": 10.3923, + "S": 0.08311 + }, + "79.5": { + "L": -0.3521, + "M": 10.4845, + "S": 0.08305 + }, + "80.0": { + "L": -0.3521, + "M": 10.5781, + "S": 0.08298 + }, + "80.5": { + "L": -0.3521, + "M": 10.6737, + "S": 0.0829 + }, + "81.0": { + "L": -0.3521, + "M": 10.7718, + "S": 0.08279 + }, + "81.5": { + "L": -0.3521, + "M": 10.8728, + "S": 0.08268 + }, + "82.0": { + "L": -0.3521, + "M": 10.9772, + "S": 0.08255 + }, + "82.5": { + "L": -0.3521, + "M": 11.0851, + "S": 0.08241 + }, + "83.0": { + "L": -0.3521, + "M": 11.1966, + "S": 0.08225 + }, + "83.5": { + "L": -0.3521, + "M": 11.3114, + "S": 0.08209 + }, + "84.0": { + "L": -0.3521, + "M": 11.429, + "S": 0.08191 + }, + "84.5": { + "L": -0.3521, + "M": 11.549, + "S": 0.08174 + }, + "85.0": { + "L": -0.3521, + "M": 11.6707, + "S": 0.08156 + }, + "85.5": { + "L": -0.3521, + "M": 11.7937, + "S": 0.08138 + }, + "86.0": { + "L": -0.3521, + "M": 11.9173, + "S": 0.08121 + }, + "86.5": { + "L": -0.3521, + "M": 12.0411, + "S": 0.08105 + }, + "87.0": { + "L": -0.3521, + "M": 12.1645, + "S": 0.0809 + }, + "87.5": { + "L": -0.3521, + "M": 12.2871, + "S": 0.08076 + }, + "88.0": { + "L": -0.3521, + "M": 12.4089, + "S": 0.08064 + }, + "88.5": { + "L": -0.3521, + "M": 12.5298, + "S": 0.08054 + }, + "89.0": { + "L": -0.3521, + "M": 12.6495, + "S": 0.08045 + }, + "89.5": { + "L": -0.3521, + "M": 12.7683, + "S": 0.08038 + }, + "90.0": { + "L": -0.3521, + "M": 12.8864, + "S": 0.08032 + }, + "90.5": { + "L": -0.3521, + "M": 13.0038, + "S": 0.08028 + }, + "91.0": { + "L": -0.3521, + "M": 13.1209, + "S": 0.08025 + }, + "91.5": { + "L": -0.3521, + "M": 13.2376, + "S": 0.08024 + }, + "92.0": { + "L": -0.3521, + "M": 13.3541, + "S": 0.08025 + }, + "92.5": { + "L": -0.3521, + "M": 13.4705, + "S": 0.08027 + }, + "93.0": { + "L": -0.3521, + "M": 13.587, + "S": 0.08031 + }, + "93.5": { + "L": -0.3521, + "M": 13.7041, + "S": 0.08036 + }, + "94.0": { + "L": -0.3521, + "M": 13.8217, + "S": 0.08043 + }, + "94.5": { + "L": -0.3521, + "M": 13.9403, + "S": 0.08051 + }, + "95.0": { + "L": -0.3521, + "M": 14.06, + "S": 0.0806 + }, + "95.5": { + "L": -0.3521, + "M": 14.1811, + "S": 0.08071 + }, + "96.0": { + "L": -0.3521, + "M": 14.3037, + "S": 0.08083 + }, + "96.5": { + "L": -0.3521, + "M": 14.4282, + "S": 0.08097 + }, + "97.0": { + "L": -0.3521, + "M": 14.5547, + "S": 0.08112 + }, + "97.5": { + "L": -0.3521, + "M": 14.6832, + "S": 0.08129 + }, + "98.0": { + "L": -0.3521, + "M": 14.814, + "S": 0.08146 + }, + "98.5": { + "L": -0.3521, + "M": 14.9468, + "S": 0.08165 + }, + "99.0": { + "L": -0.3521, + "M": 15.0818, + "S": 0.08185 + }, + "99.5": { + "L": -0.3521, + "M": 15.2187, + "S": 0.08206 + }, + "100.0": { + "L": -0.3521, + "M": 15.3576, + "S": 0.08229 + }, + "100.5": { + "L": -0.3521, + "M": 15.4985, + "S": 0.08252 + }, + "101.0": { + "L": -0.3521, + "M": 15.6412, + "S": 0.08277 + }, + "101.5": { + "L": -0.3521, + "M": 15.7857, + "S": 0.08302 + }, + "102.0": { + "L": -0.3521, + "M": 15.932, + "S": 0.08328 + }, + "102.5": { + "L": -0.3521, + "M": 16.0801, + "S": 0.08354 + }, + "103.0": { + "L": -0.3521, + "M": 16.2298, + "S": 0.08381 + }, + "103.5": { + "L": -0.3521, + "M": 16.3812, + "S": 0.08408 + }, + "104.0": { + "L": -0.3521, + "M": 16.5342, + "S": 0.08436 + }, + "104.5": { + "L": -0.3521, + "M": 16.6889, + "S": 0.08464 + }, + "105.0": { + "L": -0.3521, + "M": 16.8454, + "S": 0.08493 + }, + "105.5": { + "L": -0.3521, + "M": 17.0036, + "S": 0.08521 + }, + "106.0": { + "L": -0.3521, + "M": 17.1637, + "S": 0.08551 + }, + "106.5": { + "L": -0.3521, + "M": 17.3256, + "S": 0.0858 + }, + "107.0": { + "L": -0.3521, + "M": 17.4894, + "S": 0.08611 + }, + "107.5": { + "L": -0.3521, + "M": 17.655, + "S": 0.08641 + }, + "108.0": { + "L": -0.3521, + "M": 17.8226, + "S": 0.08673 + }, + "108.5": { + "L": -0.3521, + "M": 17.9924, + "S": 0.08704 + }, + "109.0": { + "L": -0.3521, + "M": 18.1645, + "S": 0.08736 + }, + "109.5": { + "L": -0.3521, + "M": 18.339, + "S": 0.08768 + }, + "110.0": { + "L": -0.3521, + "M": 18.5158, + "S": 0.088 + }, + "110.5": { + "L": -0.3521, + "M": 18.6948, + "S": 0.08832 + }, + "111.0": { + "L": -0.3521, + "M": 18.8759, + "S": 0.08864 + }, + "111.5": { + "L": -0.3521, + "M": 19.059, + "S": 0.08896 + }, + "112.0": { + "L": -0.3521, + "M": 19.2439, + "S": 0.08928 + }, + "112.5": { + "L": -0.3521, + "M": 19.4304, + "S": 0.0896 + }, + "113.0": { + "L": -0.3521, + "M": 19.6185, + "S": 0.08991 + }, + "113.5": { + "L": -0.3521, + "M": 19.8081, + "S": 0.09022 + }, + "114.0": { + "L": -0.3521, + "M": 19.999, + "S": 0.09054 + }, + "114.5": { + "L": -0.3521, + "M": 20.1912, + "S": 0.09085 + }, + "115.0": { + "L": -0.3521, + "M": 20.3846, + "S": 0.09116 + }, + "115.5": { + "L": -0.3521, + "M": 20.5789, + "S": 0.09147 + }, + "116.0": { + "L": -0.3521, + "M": 20.7741, + "S": 0.09177 + }, + "116.5": { + "L": -0.3521, + "M": 20.97, + "S": 0.09208 + }, + "117.0": { + "L": -0.3521, + "M": 21.1666, + "S": 0.09239 + }, + "117.5": { + "L": -0.3521, + "M": 21.3636, + "S": 0.0927 + }, + "118.0": { + "L": -0.3521, + "M": 21.5611, + "S": 0.093 + }, + "118.5": { + "L": -0.3521, + "M": 21.7588, + "S": 0.09331 + }, + "119.0": { + "L": -0.3521, + "M": 21.9568, + "S": 0.09362 + }, + "119.5": { + "L": -0.3521, + "M": 22.1549, + "S": 0.09393 + }, + "120.0": { + "L": -0.3521, + "M": 22.353, + "S": 0.09424 + } + } + }, + "female": { + "wfa": { + "0": { + "L": 0.3809, + "M": 3.2322, + "S": 0.14171 + }, + "1": { + "L": 0.1714, + "M": 4.1873, + "S": 0.13724 + }, + "2": { + "L": 0.0962, + "M": 5.1282, + "S": 0.13 + }, + "3": { + "L": 0.0402, + "M": 5.8458, + "S": 0.12619 + }, + "4": { + "L": -0.005, + "M": 6.4237, + "S": 0.12402 + }, + "5": { + "L": -0.043, + "M": 6.8985, + "S": 0.12274 + }, + "6": { + "L": -0.0756, + "M": 7.297, + "S": 0.12204 + }, + "7": { + "L": -0.1039, + "M": 7.6422, + "S": 0.12178 + }, + "8": { + "L": -0.1288, + "M": 7.9487, + "S": 0.12181 + }, + "9": { + "L": -0.1507, + "M": 8.2254, + "S": 0.12199 + }, + "10": { + "L": -0.17, + "M": 8.48, + "S": 0.12223 + }, + "11": { + "L": -0.1872, + "M": 8.7192, + "S": 0.12247 + }, + "12": { + "L": -0.2024, + "M": 8.9481, + "S": 0.12268 + }, + "13": { + "L": -0.2158, + "M": 9.1699, + "S": 0.12283 + }, + "14": { + "L": -0.2278, + "M": 9.387, + "S": 0.12294 + }, + "15": { + "L": -0.2384, + "M": 9.6008, + "S": 0.12299 + }, + "16": { + "L": -0.2478, + "M": 9.8124, + "S": 0.12303 + }, + "17": { + "L": -0.2562, + "M": 10.0226, + "S": 0.12306 + }, + "18": { + "L": -0.2637, + "M": 10.2315, + "S": 0.12309 + }, + "19": { + "L": -0.2703, + "M": 10.4393, + "S": 0.12315 + }, + "20": { + "L": -0.2762, + "M": 10.6464, + "S": 0.12323 + }, + "21": { + "L": -0.2815, + "M": 10.8534, + "S": 0.12335 + }, + "22": { + "L": -0.2862, + "M": 11.0608, + "S": 0.1235 + }, + "23": { + "L": -0.2903, + "M": 11.2688, + "S": 0.12369 + }, + "24": { + "L": -0.2941, + "M": 11.4775, + "S": 0.1239 + }, + "25": { + "L": -0.2975, + "M": 11.6864, + "S": 0.12414 + }, + "26": { + "L": -0.3005, + "M": 11.8947, + "S": 0.12441 + }, + "27": { + "L": -0.3032, + "M": 12.1015, + "S": 0.12472 + }, + "28": { + "L": -0.3057, + "M": 12.3059, + "S": 0.12506 + }, + "29": { + "L": -0.308, + "M": 12.5073, + "S": 0.12545 + }, + "30": { + "L": -0.3101, + "M": 12.7055, + "S": 0.12587 + }, + "31": { + "L": -0.312, + "M": 12.9006, + "S": 0.12633 + }, + "32": { + "L": -0.3138, + "M": 13.093, + "S": 0.12683 + }, + "33": { + "L": -0.3155, + "M": 13.2837, + "S": 0.12737 + }, + "34": { + "L": -0.3171, + "M": 13.4731, + "S": 0.12794 + }, + "35": { + "L": -0.3186, + "M": 13.6618, + "S": 0.12855 + }, + "36": { + "L": -0.3201, + "M": 13.8503, + "S": 0.12919 + }, + "37": { + "L": -0.3216, + "M": 14.0385, + "S": 0.12988 + }, + "38": { + "L": -0.323, + "M": 14.2265, + "S": 0.13059 + }, + "39": { + "L": -0.3243, + "M": 14.414, + "S": 0.13135 + }, + "40": { + "L": -0.3257, + "M": 14.601, + "S": 0.13213 + }, + "41": { + "L": -0.327, + "M": 14.7873, + "S": 0.13293 + }, + "42": { + "L": -0.3283, + "M": 14.9727, + "S": 0.13376 + }, + "43": { + "L": -0.3296, + "M": 15.1573, + "S": 0.1346 + }, + "44": { + "L": -0.3309, + "M": 15.341, + "S": 0.13545 + }, + "45": { + "L": -0.3322, + "M": 15.524, + "S": 0.1363 + }, + "46": { + "L": -0.3335, + "M": 15.7064, + "S": 0.13716 + }, + "47": { + "L": -0.3348, + "M": 15.8882, + "S": 0.138 + }, + "48": { + "L": -0.3361, + "M": 16.0697, + "S": 0.13884 + }, + "49": { + "L": -0.3374, + "M": 16.2511, + "S": 0.13968 + }, + "50": { + "L": -0.3387, + "M": 16.4322, + "S": 0.14051 + }, + "51": { + "L": -0.34, + "M": 16.6133, + "S": 0.14132 + }, + "52": { + "L": -0.3414, + "M": 16.7942, + "S": 0.14213 + }, + "53": { + "L": -0.3427, + "M": 16.9748, + "S": 0.14293 + }, + "54": { + "L": -0.344, + "M": 17.1551, + "S": 0.14371 + }, + "55": { + "L": -0.3453, + "M": 17.3347, + "S": 0.14448 + }, + "56": { + "L": -0.3466, + "M": 17.5136, + "S": 0.14525 + }, + "57": { + "L": -0.3479, + "M": 17.6916, + "S": 0.146 + }, + "58": { + "L": -0.3492, + "M": 17.8686, + "S": 0.14675 + }, + "59": { + "L": -0.3505, + "M": 18.0445, + "S": 0.14748 + }, + "60": { + "L": -0.3518, + "M": 18.2193, + "S": 0.14821 + } + }, + "hfa": { + "0": { + "L": 1.0, + "M": 49.1477, + "S": 0.0379 + }, + "1": { + "L": 1.0, + "M": 53.6872, + "S": 0.0364 + }, + "2": { + "L": 1.0, + "M": 57.0673, + "S": 0.03568 + }, + "3": { + "L": 1.0, + "M": 59.8029, + "S": 0.0352 + }, + "4": { + "L": 1.0, + "M": 62.0899, + "S": 0.03486 + }, + "5": { + "L": 1.0, + "M": 64.0301, + "S": 0.03463 + }, + "6": { + "L": 1.0, + "M": 65.7311, + "S": 0.03448 + }, + "7": { + "L": 1.0, + "M": 67.2873, + "S": 0.03441 + }, + "8": { + "L": 1.0, + "M": 68.7498, + "S": 0.0344 + }, + "9": { + "L": 1.0, + "M": 70.1435, + "S": 0.03444 + }, + "10": { + "L": 1.0, + "M": 71.4818, + "S": 0.03452 + }, + "11": { + "L": 1.0, + "M": 72.771, + "S": 0.03464 + }, + "12": { + "L": 1.0, + "M": 74.015, + "S": 0.03479 + }, + "13": { + "L": 1.0, + "M": 75.2176, + "S": 0.03496 + }, + "14": { + "L": 1.0, + "M": 76.3817, + "S": 0.03514 + }, + "15": { + "L": 1.0, + "M": 77.5099, + "S": 0.03534 + }, + "16": { + "L": 1.0, + "M": 78.6055, + "S": 0.03555 + }, + "17": { + "L": 1.0, + "M": 79.671, + "S": 0.03576 + }, + "18": { + "L": 1.0, + "M": 80.7079, + "S": 0.03598 + }, + "19": { + "L": 1.0, + "M": 81.7182, + "S": 0.0362 + }, + "20": { + "L": 1.0, + "M": 82.7036, + "S": 0.03643 + }, + "21": { + "L": 1.0, + "M": 83.6654, + "S": 0.03666 + }, + "22": { + "L": 1.0, + "M": 84.604, + "S": 0.03688 + }, + "23": { + "L": 1.0, + "M": 85.5202, + "S": 0.03711 + }, + "24": { + "L": 1.0, + "M": 85.7153, + "S": 0.03764 + }, + "25": { + "L": 1.0, + "M": 86.5904, + "S": 0.03786 + }, + "26": { + "L": 1.0, + "M": 87.4462, + "S": 0.03808 + }, + "27": { + "L": 1.0, + "M": 88.283, + "S": 0.0383 + }, + "28": { + "L": 1.0, + "M": 89.1004, + "S": 0.03851 + }, + "29": { + "L": 1.0, + "M": 89.8991, + "S": 0.03872 + }, + "30": { + "L": 1.0, + "M": 90.6797, + "S": 0.03893 + }, + "31": { + "L": 1.0, + "M": 91.443, + "S": 0.03913 + }, + "32": { + "L": 1.0, + "M": 92.1906, + "S": 0.03933 + }, + "33": { + "L": 1.0, + "M": 92.9239, + "S": 0.03952 + }, + "34": { + "L": 1.0, + "M": 93.6444, + "S": 0.03971 + }, + "35": { + "L": 1.0, + "M": 94.3533, + "S": 0.03989 + }, + "36": { + "L": 1.0, + "M": 95.0515, + "S": 0.04006 + }, + "37": { + "L": 1.0, + "M": 95.7399, + "S": 0.04024 + }, + "38": { + "L": 1.0, + "M": 96.4187, + "S": 0.04041 + }, + "39": { + "L": 1.0, + "M": 97.0885, + "S": 0.04057 + }, + "40": { + "L": 1.0, + "M": 97.7493, + "S": 0.04073 + }, + "41": { + "L": 1.0, + "M": 98.4015, + "S": 0.04089 + }, + "42": { + "L": 1.0, + "M": 99.0448, + "S": 0.04105 + }, + "43": { + "L": 1.0, + "M": 99.6795, + "S": 0.0412 + }, + "44": { + "L": 1.0, + "M": 100.3058, + "S": 0.04135 + }, + "45": { + "L": 1.0, + "M": 100.9238, + "S": 0.0415 + }, + "46": { + "L": 1.0, + "M": 101.5337, + "S": 0.04164 + }, + "47": { + "L": 1.0, + "M": 102.136, + "S": 0.04179 + }, + "48": { + "L": 1.0, + "M": 102.7312, + "S": 0.04193 + }, + "49": { + "L": 1.0, + "M": 103.3197, + "S": 0.04206 + }, + "50": { + "L": 1.0, + "M": 103.9021, + "S": 0.0422 + }, + "51": { + "L": 1.0, + "M": 104.4786, + "S": 0.04233 + }, + "52": { + "L": 1.0, + "M": 105.0494, + "S": 0.04246 + }, + "53": { + "L": 1.0, + "M": 105.6148, + "S": 0.04259 + }, + "54": { + "L": 1.0, + "M": 106.1748, + "S": 0.04272 + }, + "55": { + "L": 1.0, + "M": 106.7295, + "S": 0.04285 + }, + "56": { + "L": 1.0, + "M": 107.2788, + "S": 0.04298 + }, + "57": { + "L": 1.0, + "M": 107.8227, + "S": 0.0431 + }, + "58": { + "L": 1.0, + "M": 108.3613, + "S": 0.04322 + }, + "59": { + "L": 1.0, + "M": 108.8948, + "S": 0.04334 + }, + "60": { + "L": 1.0, + "M": 109.4233, + "S": 0.04347 + } + }, + "wfl": { + "45.0": { + "L": -0.3833, + "M": 2.4607, + "S": 0.09029 + }, + "45.5": { + "L": -0.3833, + "M": 2.5457, + "S": 0.09033 + }, + "46.0": { + "L": -0.3833, + "M": 2.6306, + "S": 0.09037 + }, + "46.5": { + "L": -0.3833, + "M": 2.7155, + "S": 0.0904 + }, + "47.0": { + "L": -0.3833, + "M": 2.8007, + "S": 0.09044 + }, + "47.5": { + "L": -0.3833, + "M": 2.8867, + "S": 0.09048 + }, + "48.0": { + "L": -0.3833, + "M": 2.9741, + "S": 0.09052 + }, + "48.5": { + "L": -0.3833, + "M": 3.0636, + "S": 0.09056 + }, + "49.0": { + "L": -0.3833, + "M": 3.156, + "S": 0.0906 + }, + "49.5": { + "L": -0.3833, + "M": 3.252, + "S": 0.09064 + }, + "50.0": { + "L": -0.3833, + "M": 3.3518, + "S": 0.09068 + }, + "50.5": { + "L": -0.3833, + "M": 3.4557, + "S": 0.09072 + }, + "51.0": { + "L": -0.3833, + "M": 3.5636, + "S": 0.09076 + }, + "51.5": { + "L": -0.3833, + "M": 3.6754, + "S": 0.0908 + }, + "52.0": { + "L": -0.3833, + "M": 3.7911, + "S": 0.09085 + }, + "52.5": { + "L": -0.3833, + "M": 3.9105, + "S": 0.09089 + }, + "53.0": { + "L": -0.3833, + "M": 4.0332, + "S": 0.09093 + }, + "53.5": { + "L": -0.3833, + "M": 4.1591, + "S": 0.09098 + }, + "54.0": { + "L": -0.3833, + "M": 4.2875, + "S": 0.09102 + }, + "54.5": { + "L": -0.3833, + "M": 4.4179, + "S": 0.09106 + }, + "55.0": { + "L": -0.3833, + "M": 4.5498, + "S": 0.0911 + }, + "55.5": { + "L": -0.3833, + "M": 4.6827, + "S": 0.09114 + }, + "56.0": { + "L": -0.3833, + "M": 4.8162, + "S": 0.09118 + }, + "56.5": { + "L": -0.3833, + "M": 4.95, + "S": 0.09121 + }, + "57.0": { + "L": -0.3833, + "M": 5.0837, + "S": 0.09125 + }, + "57.5": { + "L": -0.3833, + "M": 5.2173, + "S": 0.09128 + }, + "58.0": { + "L": -0.3833, + "M": 5.3507, + "S": 0.0913 + }, + "58.5": { + "L": -0.3833, + "M": 5.4834, + "S": 0.09132 + }, + "59.0": { + "L": -0.3833, + "M": 5.6151, + "S": 0.09134 + }, + "59.5": { + "L": -0.3833, + "M": 5.7454, + "S": 0.09135 + }, + "60.0": { + "L": -0.3833, + "M": 5.8742, + "S": 0.09136 + }, + "60.5": { + "L": -0.3833, + "M": 6.0014, + "S": 0.09137 + }, + "61.0": { + "L": -0.3833, + "M": 6.127, + "S": 0.09137 + }, + "61.5": { + "L": -0.3833, + "M": 6.2511, + "S": 0.09136 + }, + "62.0": { + "L": -0.3833, + "M": 6.3738, + "S": 0.09135 + }, + "62.5": { + "L": -0.3833, + "M": 6.4948, + "S": 0.09133 + }, + "63.0": { + "L": -0.3833, + "M": 6.6144, + "S": 0.09131 + }, + "63.5": { + "L": -0.3833, + "M": 6.7328, + "S": 0.09129 + }, + "64.0": { + "L": -0.3833, + "M": 6.8501, + "S": 0.09126 + }, + "64.5": { + "L": -0.3833, + "M": 6.9662, + "S": 0.09123 + }, + "65.0": { + "L": -0.3833, + "M": 7.0812, + "S": 0.09119 + }, + "65.5": { + "L": -0.3833, + "M": 7.195, + "S": 0.09115 + }, + "66.0": { + "L": -0.3833, + "M": 7.3076, + "S": 0.0911 + }, + "66.5": { + "L": -0.3833, + "M": 7.4189, + "S": 0.09106 + }, + "67.0": { + "L": -0.3833, + "M": 7.5288, + "S": 0.09101 + }, + "67.5": { + "L": -0.3833, + "M": 7.6375, + "S": 0.09096 + }, + "68.0": { + "L": -0.3833, + "M": 7.7448, + "S": 0.0909 + }, + "68.5": { + "L": -0.3833, + "M": 7.8509, + "S": 0.09085 + }, + "69.0": { + "L": -0.3833, + "M": 7.9559, + "S": 0.09079 + }, + "69.5": { + "L": -0.3833, + "M": 8.0599, + "S": 0.09074 + }, + "70.0": { + "L": -0.3833, + "M": 8.163, + "S": 0.09068 + }, + "70.5": { + "L": -0.3833, + "M": 8.2651, + "S": 0.09062 + }, + "71.0": { + "L": -0.3833, + "M": 8.3666, + "S": 0.09056 + }, + "71.5": { + "L": -0.3833, + "M": 8.4676, + "S": 0.0905 + }, + "72.0": { + "L": -0.3833, + "M": 8.5679, + "S": 0.09043 + }, + "72.5": { + "L": -0.3833, + "M": 8.6674, + "S": 0.09037 + }, + "73.0": { + "L": -0.3833, + "M": 8.7661, + "S": 0.09031 + }, + "73.5": { + "L": -0.3833, + "M": 8.8638, + "S": 0.09025 + }, + "74.0": { + "L": -0.3833, + "M": 8.9601, + "S": 0.09018 + }, + "74.5": { + "L": -0.3833, + "M": 9.0552, + "S": 0.09012 + }, + "75.0": { + "L": -0.3833, + "M": 9.149, + "S": 0.09005 + }, + "75.5": { + "L": -0.3833, + "M": 9.2418, + "S": 0.08999 + }, + "76.0": { + "L": -0.3833, + "M": 9.3337, + "S": 0.08992 + }, + "76.5": { + "L": -0.3833, + "M": 9.4252, + "S": 0.08985 + }, + "77.0": { + "L": -0.3833, + "M": 9.5166, + "S": 0.08979 + }, + "77.5": { + "L": -0.3833, + "M": 9.6086, + "S": 0.08972 + }, + "78.0": { + "L": -0.3833, + "M": 9.7015, + "S": 0.08965 + }, + "78.5": { + "L": -0.3833, + "M": 9.7957, + "S": 0.08959 + }, + "79.0": { + "L": -0.3833, + "M": 9.8915, + "S": 0.08952 + }, + "79.5": { + "L": -0.3833, + "M": 9.9892, + "S": 0.08946 + }, + "80.0": { + "L": -0.3833, + "M": 10.0891, + "S": 0.0894 + }, + "80.5": { + "L": -0.3833, + "M": 10.1916, + "S": 0.08934 + }, + "81.0": { + "L": -0.3833, + "M": 10.2965, + "S": 0.08928 + }, + "81.5": { + "L": -0.3833, + "M": 10.4041, + "S": 0.08923 + }, + "82.0": { + "L": -0.3833, + "M": 10.514, + "S": 0.08918 + }, + "82.5": { + "L": -0.3833, + "M": 10.6263, + "S": 0.08914 + }, + "83.0": { + "L": -0.3833, + "M": 10.741, + "S": 0.0891 + }, + "83.5": { + "L": -0.3833, + "M": 10.8578, + "S": 0.08906 + }, + "84.0": { + "L": -0.3833, + "M": 10.9767, + "S": 0.08903 + }, + "84.5": { + "L": -0.3833, + "M": 11.0974, + "S": 0.089 + }, + "85.0": { + "L": -0.3833, + "M": 11.2198, + "S": 0.08898 + }, + "85.5": { + "L": -0.3833, + "M": 11.3435, + "S": 0.08897 + }, + "86.0": { + "L": -0.3833, + "M": 11.4684, + "S": 0.08895 + }, + "86.5": { + "L": -0.3833, + "M": 11.594, + "S": 0.08895 + }, + "87.0": { + "L": -0.3833, + "M": 11.7201, + "S": 0.08895 + }, + "87.5": { + "L": -0.3833, + "M": 11.8461, + "S": 0.08895 + }, + "88.0": { + "L": -0.3833, + "M": 11.972, + "S": 0.08896 + }, + "88.5": { + "L": -0.3833, + "M": 12.0976, + "S": 0.08898 + }, + "89.0": { + "L": -0.3833, + "M": 12.2229, + "S": 0.089 + }, + "89.5": { + "L": -0.3833, + "M": 12.3477, + "S": 0.08903 + }, + "90.0": { + "L": -0.3833, + "M": 12.4723, + "S": 0.08906 + }, + "90.5": { + "L": -0.3833, + "M": 12.5965, + "S": 0.08909 + }, + "91.0": { + "L": -0.3833, + "M": 12.7205, + "S": 0.08913 + }, + "91.5": { + "L": -0.3833, + "M": 12.8443, + "S": 0.08918 + }, + "92.0": { + "L": -0.3833, + "M": 12.9681, + "S": 0.08923 + }, + "92.5": { + "L": -0.3833, + "M": 13.092, + "S": 0.08928 + }, + "93.0": { + "L": -0.3833, + "M": 13.2158, + "S": 0.08934 + }, + "93.5": { + "L": -0.3833, + "M": 13.3399, + "S": 0.08941 + }, + "94.0": { + "L": -0.3833, + "M": 13.4643, + "S": 0.08948 + }, + "94.5": { + "L": -0.3833, + "M": 13.5892, + "S": 0.08955 + }, + "95.0": { + "L": -0.3833, + "M": 13.7146, + "S": 0.08963 + }, + "95.5": { + "L": -0.3833, + "M": 13.8408, + "S": 0.08972 + }, + "96.0": { + "L": -0.3833, + "M": 13.9676, + "S": 0.08981 + }, + "96.5": { + "L": -0.3833, + "M": 14.0953, + "S": 0.0899 + }, + "97.0": { + "L": -0.3833, + "M": 14.2239, + "S": 0.09 + }, + "97.5": { + "L": -0.3833, + "M": 14.3537, + "S": 0.0901 + }, + "98.0": { + "L": -0.3833, + "M": 14.4848, + "S": 0.09021 + }, + "98.5": { + "L": -0.3833, + "M": 14.6174, + "S": 0.09033 + }, + "99.0": { + "L": -0.3833, + "M": 14.7519, + "S": 0.09044 + }, + "99.5": { + "L": -0.3833, + "M": 14.8882, + "S": 0.09057 + }, + "100.0": { + "L": -0.3833, + "M": 15.0267, + "S": 0.09069 + }, + "100.5": { + "L": -0.3833, + "M": 15.1676, + "S": 0.09083 + }, + "101.0": { + "L": -0.3833, + "M": 15.3108, + "S": 0.09096 + }, + "101.5": { + "L": -0.3833, + "M": 15.4564, + "S": 0.0911 + }, + "102.0": { + "L": -0.3833, + "M": 15.6046, + "S": 0.09125 + }, + "102.5": { + "L": -0.3833, + "M": 15.7553, + "S": 0.09139 + }, + "103.0": { + "L": -0.3833, + "M": 15.9087, + "S": 0.09155 + }, + "103.5": { + "L": -0.3833, + "M": 16.0645, + "S": 0.0917 + }, + "104.0": { + "L": -0.3833, + "M": 16.2229, + "S": 0.09186 + }, + "104.5": { + "L": -0.3833, + "M": 16.3837, + "S": 0.09203 + }, + "105.0": { + "L": -0.3833, + "M": 16.547, + "S": 0.09219 + }, + "105.5": { + "L": -0.3833, + "M": 16.7129, + "S": 0.09236 + }, + "106.0": { + "L": -0.3833, + "M": 16.8814, + "S": 0.09254 + }, + "106.5": { + "L": -0.3833, + "M": 17.0527, + "S": 0.09271 + }, + "107.0": { + "L": -0.3833, + "M": 17.2269, + "S": 0.09289 + }, + "107.5": { + "L": -0.3833, + "M": 17.4039, + "S": 0.09307 + }, + "108.0": { + "L": -0.3833, + "M": 17.5839, + "S": 0.09326 + }, + "108.5": { + "L": -0.3833, + "M": 17.7668, + "S": 0.09344 + }, + "109.0": { + "L": -0.3833, + "M": 17.9526, + "S": 0.09363 + }, + "109.5": { + "L": -0.3833, + "M": 18.1412, + "S": 0.09382 + }, + "110.0": { + "L": -0.3833, + "M": 18.3324, + "S": 0.09401 + } + }, + "wfh": { + "65.0": { + "L": -0.3833, + "M": 7.2402, + "S": 0.09113 + }, + "65.5": { + "L": -0.3833, + "M": 7.3523, + "S": 0.09109 + }, + "66.0": { + "L": -0.3833, + "M": 7.463, + "S": 0.09104 + }, + "66.5": { + "L": -0.3833, + "M": 7.5724, + "S": 0.09099 + }, + "67.0": { + "L": -0.3833, + "M": 7.6806, + "S": 0.09094 + }, + "67.5": { + "L": -0.3833, + "M": 7.7874, + "S": 0.09088 + }, + "68.0": { + "L": -0.3833, + "M": 7.893, + "S": 0.09083 + }, + "68.5": { + "L": -0.3833, + "M": 7.9976, + "S": 0.09077 + }, + "69.0": { + "L": -0.3833, + "M": 8.1012, + "S": 0.09071 + }, + "69.5": { + "L": -0.3833, + "M": 8.2039, + "S": 0.09065 + }, + "70.0": { + "L": -0.3833, + "M": 8.3058, + "S": 0.09059 + }, + "70.5": { + "L": -0.3833, + "M": 8.4071, + "S": 0.09053 + }, + "71.0": { + "L": -0.3833, + "M": 8.5078, + "S": 0.09047 + }, + "71.5": { + "L": -0.3833, + "M": 8.6078, + "S": 0.09041 + }, + "72.0": { + "L": -0.3833, + "M": 8.707, + "S": 0.09035 + }, + "72.5": { + "L": -0.3833, + "M": 8.8053, + "S": 0.09028 + }, + "73.0": { + "L": -0.3833, + "M": 8.9025, + "S": 0.09022 + }, + "73.5": { + "L": -0.3833, + "M": 8.9983, + "S": 0.09016 + }, + "74.0": { + "L": -0.3833, + "M": 9.0928, + "S": 0.09009 + }, + "74.5": { + "L": -0.3833, + "M": 9.1862, + "S": 0.09003 + }, + "75.0": { + "L": -0.3833, + "M": 9.2786, + "S": 0.08996 + }, + "75.5": { + "L": -0.3833, + "M": 9.3703, + "S": 0.08989 + }, + "76.0": { + "L": -0.3833, + "M": 9.4617, + "S": 0.08983 + }, + "76.5": { + "L": -0.3833, + "M": 9.5533, + "S": 0.08976 + }, + "77.0": { + "L": -0.3833, + "M": 9.6456, + "S": 0.08969 + }, + "77.5": { + "L": -0.3833, + "M": 9.739, + "S": 0.08963 + }, + "78.0": { + "L": -0.3833, + "M": 9.8338, + "S": 0.08956 + }, + "78.5": { + "L": -0.3833, + "M": 9.9303, + "S": 0.0895 + }, + "79.0": { + "L": -0.3833, + "M": 10.0289, + "S": 0.08943 + }, + "79.5": { + "L": -0.3833, + "M": 10.1298, + "S": 0.08937 + }, + "80.0": { + "L": -0.3833, + "M": 10.2332, + "S": 0.08932 + }, + "80.5": { + "L": -0.3833, + "M": 10.3393, + "S": 0.08926 + }, + "81.0": { + "L": -0.3833, + "M": 10.4477, + "S": 0.08921 + }, + "81.5": { + "L": -0.3833, + "M": 10.5586, + "S": 0.08916 + }, + "82.0": { + "L": -0.3833, + "M": 10.6719, + "S": 0.08912 + }, + "82.5": { + "L": -0.3833, + "M": 10.7874, + "S": 0.08908 + }, + "83.0": { + "L": -0.3833, + "M": 10.9051, + "S": 0.08905 + }, + "83.5": { + "L": -0.3833, + "M": 11.0248, + "S": 0.08902 + }, + "84.0": { + "L": -0.3833, + "M": 11.1462, + "S": 0.08899 + }, + "84.5": { + "L": -0.3833, + "M": 11.2691, + "S": 0.08897 + }, + "85.0": { + "L": -0.3833, + "M": 11.3934, + "S": 0.08896 + }, + "85.5": { + "L": -0.3833, + "M": 11.5186, + "S": 0.08895 + }, + "86.0": { + "L": -0.3833, + "M": 11.6444, + "S": 0.08895 + }, + "86.5": { + "L": -0.3833, + "M": 11.7705, + "S": 0.08895 + }, + "87.0": { + "L": -0.3833, + "M": 11.8965, + "S": 0.08896 + }, + "87.5": { + "L": -0.3833, + "M": 12.0223, + "S": 0.08897 + }, + "88.0": { + "L": -0.3833, + "M": 12.1478, + "S": 0.08899 + }, + "88.5": { + "L": -0.3833, + "M": 12.2729, + "S": 0.08901 + }, + "89.0": { + "L": -0.3833, + "M": 12.3976, + "S": 0.08904 + }, + "89.5": { + "L": -0.3833, + "M": 12.522, + "S": 0.08907 + }, + "90.0": { + "L": -0.3833, + "M": 12.6461, + "S": 0.08911 + }, + "90.5": { + "L": -0.3833, + "M": 12.77, + "S": 0.08915 + }, + "91.0": { + "L": -0.3833, + "M": 12.8939, + "S": 0.0892 + }, + "91.5": { + "L": -0.3833, + "M": 13.0177, + "S": 0.08925 + }, + "92.0": { + "L": -0.3833, + "M": 13.1415, + "S": 0.08931 + }, + "92.5": { + "L": -0.3833, + "M": 13.2654, + "S": 0.08937 + }, + "93.0": { + "L": -0.3833, + "M": 13.3896, + "S": 0.08944 + }, + "93.5": { + "L": -0.3833, + "M": 13.5142, + "S": 0.08951 + }, + "94.0": { + "L": -0.3833, + "M": 13.6393, + "S": 0.08959 + }, + "94.5": { + "L": -0.3833, + "M": 13.765, + "S": 0.08967 + }, + "95.0": { + "L": -0.3833, + "M": 13.8914, + "S": 0.08975 + }, + "95.5": { + "L": -0.3833, + "M": 14.0186, + "S": 0.08984 + }, + "96.0": { + "L": -0.3833, + "M": 14.1466, + "S": 0.08994 + }, + "96.5": { + "L": -0.3833, + "M": 14.2757, + "S": 0.09004 + }, + "97.0": { + "L": -0.3833, + "M": 14.4059, + "S": 0.09015 + }, + "97.5": { + "L": -0.3833, + "M": 14.5376, + "S": 0.09026 + }, + "98.0": { + "L": -0.3833, + "M": 14.671, + "S": 0.09037 + }, + "98.5": { + "L": -0.3833, + "M": 14.8062, + "S": 0.09049 + }, + "99.0": { + "L": -0.3833, + "M": 14.9434, + "S": 0.09062 + }, + "99.5": { + "L": -0.3833, + "M": 15.0828, + "S": 0.09075 + }, + "100.0": { + "L": -0.3833, + "M": 15.2246, + "S": 0.09088 + }, + "100.5": { + "L": -0.3833, + "M": 15.3687, + "S": 0.09102 + }, + "101.0": { + "L": -0.3833, + "M": 15.5154, + "S": 0.09116 + }, + "101.5": { + "L": -0.3833, + "M": 15.6646, + "S": 0.09131 + }, + "102.0": { + "L": -0.3833, + "M": 15.8164, + "S": 0.09146 + }, + "102.5": { + "L": -0.3833, + "M": 15.9707, + "S": 0.09161 + }, + "103.0": { + "L": -0.3833, + "M": 16.1276, + "S": 0.09177 + }, + "103.5": { + "L": -0.3833, + "M": 16.287, + "S": 0.09193 + }, + "104.0": { + "L": -0.3833, + "M": 16.4488, + "S": 0.09209 + }, + "104.5": { + "L": -0.3833, + "M": 16.6131, + "S": 0.09226 + }, + "105.0": { + "L": -0.3833, + "M": 16.78, + "S": 0.09243 + }, + "105.5": { + "L": -0.3833, + "M": 16.9496, + "S": 0.09261 + }, + "106.0": { + "L": -0.3833, + "M": 17.122, + "S": 0.09278 + }, + "106.5": { + "L": -0.3833, + "M": 17.2973, + "S": 0.09296 + }, + "107.0": { + "L": -0.3833, + "M": 17.4755, + "S": 0.09315 + }, + "107.5": { + "L": -0.3833, + "M": 17.6567, + "S": 0.09333 + }, + "108.0": { + "L": -0.3833, + "M": 17.8407, + "S": 0.09352 + }, + "108.5": { + "L": -0.3833, + "M": 18.0277, + "S": 0.09371 + }, + "109.0": { + "L": -0.3833, + "M": 18.2174, + "S": 0.0939 + }, + "109.5": { + "L": -0.3833, + "M": 18.4096, + "S": 0.09409 + }, + "110.0": { + "L": -0.3833, + "M": 18.6043, + "S": 0.09428 + }, + "110.5": { + "L": -0.3833, + "M": 18.8015, + "S": 0.09448 + }, + "111.0": { + "L": -0.3833, + "M": 19.0009, + "S": 0.09467 + }, + "111.5": { + "L": -0.3833, + "M": 19.2024, + "S": 0.09487 + }, + "112.0": { + "L": -0.3833, + "M": 19.406, + "S": 0.09507 + }, + "112.5": { + "L": -0.3833, + "M": 19.6116, + "S": 0.09527 + }, + "113.0": { + "L": -0.3833, + "M": 19.819, + "S": 0.09546 + }, + "113.5": { + "L": -0.3833, + "M": 20.028, + "S": 0.09566 + }, + "114.0": { + "L": -0.3833, + "M": 20.2385, + "S": 0.09586 + }, + "114.5": { + "L": -0.3833, + "M": 20.4502, + "S": 0.09606 + }, + "115.0": { + "L": -0.3833, + "M": 20.6629, + "S": 0.09626 + }, + "115.5": { + "L": -0.3833, + "M": 20.8766, + "S": 0.09646 + }, + "116.0": { + "L": -0.3833, + "M": 21.0909, + "S": 0.09666 + }, + "116.5": { + "L": -0.3833, + "M": 21.3059, + "S": 0.09686 + }, + "117.0": { + "L": -0.3833, + "M": 21.5213, + "S": 0.09707 + }, + "117.5": { + "L": -0.3833, + "M": 21.737, + "S": 0.09727 + }, + "118.0": { + "L": -0.3833, + "M": 21.9529, + "S": 0.09747 + }, + "118.5": { + "L": -0.3833, + "M": 22.169, + "S": 0.09767 + }, + "119.0": { + "L": -0.3833, + "M": 22.3851, + "S": 0.09788 + }, + "119.5": { + "L": -0.3833, + "M": 22.6012, + "S": 0.09808 + }, + "120.0": { + "L": -0.3833, + "M": 22.8173, + "S": 0.09828 + } + } + } +} \ No newline at end of file diff --git a/Website/s_gizi/.editorconfig b/Website/s_gizi/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/Website/s_gizi/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/Website/s_gizi/.env.example b/Website/s_gizi/.env.example new file mode 100644 index 0000000..c0660ea --- /dev/null +++ b/Website/s_gizi/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/Website/s_gizi/.gitattributes b/Website/s_gizi/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/Website/s_gizi/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/Website/s_gizi/.gitignore b/Website/s_gizi/.gitignore new file mode 100644 index 0000000..b71b1ea --- /dev/null +++ b/Website/s_gizi/.gitignore @@ -0,0 +1,24 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.fleet +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/Website/s_gizi/README.md b/Website/s_gizi/README.md new file mode 100644 index 0000000..0165a77 --- /dev/null +++ b/Website/s_gizi/README.md @@ -0,0 +1,59 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com)** +- **[Tighten Co.](https://tighten.co)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Redberry](https://redberry.international/laravel-development)** +- **[Active Logic](https://activelogic.com)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/Website/s_gizi/README_API_SGIZI.md b/Website/s_gizi/README_API_SGIZI.md new file mode 100644 index 0000000..9bc91c8 --- /dev/null +++ b/Website/s_gizi/README_API_SGIZI.md @@ -0,0 +1,58 @@ +# API S-Gizi (Laravel) + +## Endpoint + +### POST `/api/hasil` + +Input JSON: + +```json +{ + "jenis_kelamin": "L", + "tanggal_lahir": "2023-01-01", + "tanggal_ukur": "2026-04-21", + "berat_badan": 12.5, + "tinggi_badan": 90.0, + "cara_ukur": "standing" +} +``` + +Output (contoh struktur): + +```json +{ + "zscore": { "bbu": -1.23, "tbu": -2.8, "bbtb": 0.45 }, + "kategori": { "bbu": "Normal", "tbu": "Pendek", "bbtb": "Normal" }, + "status_gabungan": "Stunting", + "rekomendasi": [ + { + "menu": "Nasi + Ayam + Sayur", + "protein": 15, + "kalori": 250, + "lemak": 8, + "karbohidrat": 30, + "alasan": "Disarankan makanan tinggi protein dan mikronutrien untuk mendukung pertumbuhan tinggi badan." + } + ] +} +``` + +## Import data LMS WHO dari Flutter + +Data LMS bersumber dari file: + +- `Android/flutter_app/assets/data/lms_who_final.json` + +Jalankan: + +```bash +php artisan migrate +php artisan db:seed --class=LMSSeeder +``` + +Opsional contoh seed makanan: + +```bash +php artisan db:seed --class=MakananSeeder +``` + diff --git a/Website/s_gizi/README_WHO_IMPORT.md b/Website/s_gizi/README_WHO_IMPORT.md new file mode 100644 index 0000000..25e83c2 --- /dev/null +++ b/Website/s_gizi/README_WHO_IMPORT.md @@ -0,0 +1,38 @@ +# Import Data WHO LMS (Wajib) + +API ini menghitung Z-score **berdasarkan tabel LMS WHO**, jadi Anda harus mengisi tabel: + +- `who_lms_age` untuk indikator: + - `bbu` (BB/U) + - `tbu` (TB/U) +- `who_lms_wfh` untuk indikator: + - BB/TB (berat terhadap tinggi) + +## Format tabel + +### 1) `who_lms_age` + +Kolom utama: + +- `indikator`: `bbu` atau `tbu` +- `jenis_kelamin`: `L` atau `P` +- `umur_bulan`: bulan (biasanya 0..60) +- `l`, `m`, `s` + +### 2) `who_lms_wfh` + +Kolom utama: + +- `jenis_kelamin`: `L` atau `P` +- `tinggi`: cm (biasanya step 0.1) +- `l`, `m`, `s` + +## Sumber data + +Gunakan tabel LMS resmi **WHO Child Growth Standards** (format L, M, S). + +## Catatan + +- Sistem akan melakukan **interpolasi linear** jika input umur/tinggi berada di antara titik LMS yang tersedia. +- Pastikan data LMS terisi sebelum mencoba endpoint `POST /api/hasil`, kalau tidak API akan gagal karena LMS tidak ditemukan. + diff --git a/Website/s_gizi/S_Gizi b/Website/s_gizi/S_Gizi new file mode 100644 index 0000000..11452ee Binary files /dev/null and b/Website/s_gizi/S_Gizi differ diff --git a/Website/s_gizi/app/Console/Commands/ImportWhoLmsFromFlutterJson.php b/Website/s_gizi/app/Console/Commands/ImportWhoLmsFromFlutterJson.php new file mode 100644 index 0000000..c5478cd --- /dev/null +++ b/Website/s_gizi/app/Console/Commands/ImportWhoLmsFromFlutterJson.php @@ -0,0 +1,178 @@ +option('path') ?: $defaultPath); + + if (!is_file($path)) { + $this->error("File tidak ditemukan: {$path}"); + return self::FAILURE; + } + + $json = file_get_contents($path); + if ($json === false) { + $this->error('Gagal membaca file JSON.'); + return self::FAILURE; + } + + $decoded = json_decode($json, true); + if (!is_array($decoded)) { + $this->error('JSON tidak valid.'); + return self::FAILURE; + } + + $mapSex = [ + 'male' => 'L', + 'female' => 'P', + ]; + + $ageRows = []; + $wflRows = []; + $wfhRows = []; + + foreach ($mapSex as $sexKey => $jk) { + if (!isset($decoded[$sexKey]) || !is_array($decoded[$sexKey])) { + continue; + } + + $sexData = $decoded[$sexKey]; + + // wfa -> bbu + $this->collectAgeRows($ageRows, $sexData['wfa'] ?? null, $jk, 'bbu'); + // hfa -> tbu + $this->collectAgeRows($ageRows, $sexData['hfa'] ?? null, $jk, 'tbu'); + + // wfl & wfh + $this->collectLengthRows($wflRows, $sexData['wfl'] ?? null, $jk); + $this->collectHeightRows($wfhRows, $sexData['wfh'] ?? null, $jk); + } + + // TRUNCATE melakukan implicit commit, jadi jangan dibungkus transaction. + if ((bool) $this->option('truncate')) { + DB::table('who_lms_age')->truncate(); + DB::table('who_lms_wfl')->truncate(); + DB::table('who_lms_wfh')->truncate(); + } + + DB::transaction(function () use ($ageRows, $wflRows, $wfhRows): void { + $now = now(); + $chunkInsert = function (string $table, array $rows) use ($now): void { + $rows = array_map(function (array $r) use ($now) { + return $r + ['created_at' => $now, 'updated_at' => $now]; + }, $rows); + + foreach (array_chunk($rows, 1000) as $chunk) { + DB::table($table)->upsert( + $chunk, + $this->uniqueKeys($table), + ['l', 'm', 's', 'updated_at'] + ); + } + }; + + $chunkInsert('who_lms_age', $ageRows); + $chunkInsert('who_lms_wfl', $wflRows); + $chunkInsert('who_lms_wfh', $wfhRows); + }); + + $this->info('Import selesai.'); + $this->line('Ringkasan:'); + $this->line('- who_lms_age: '.count($ageRows)); + $this->line('- who_lms_wfl: '.count($wflRows)); + $this->line('- who_lms_wfh: '.count($wfhRows)); + + return self::SUCCESS; + } + + private function collectAgeRows(array &$out, mixed $data, string $jk, string $indikator): void + { + if (!is_array($data)) { + return; + } + + foreach ($data as $umurStr => $lms) { + if (!is_array($lms)) { + continue; + } + + $out[] = [ + 'indikator' => $indikator, + 'jenis_kelamin' => $jk, + 'umur_bulan' => (float) $umurStr, + 'l' => (float) ($lms['L'] ?? 0), + 'm' => (float) ($lms['M'] ?? 0), + 's' => (float) ($lms['S'] ?? 0), + ]; + } + } + + private function collectLengthRows(array &$out, mixed $data, string $jk): void + { + if (!is_array($data)) { + return; + } + + foreach ($data as $panjangStr => $lms) { + if (!is_array($lms)) { + continue; + } + + $out[] = [ + 'jenis_kelamin' => $jk, + 'panjang' => (float) $panjangStr, + 'l' => (float) ($lms['L'] ?? 0), + 'm' => (float) ($lms['M'] ?? 0), + 's' => (float) ($lms['S'] ?? 0), + ]; + } + } + + private function collectHeightRows(array &$out, mixed $data, string $jk): void + { + if (!is_array($data)) { + return; + } + + foreach ($data as $tinggiStr => $lms) { + if (!is_array($lms)) { + continue; + } + + $out[] = [ + 'jenis_kelamin' => $jk, + 'tinggi' => (float) $tinggiStr, + 'l' => (float) ($lms['L'] ?? 0), + 'm' => (float) ($lms['M'] ?? 0), + 's' => (float) ($lms['S'] ?? 0), + ]; + } + } + + /** + * @return array + */ + private function uniqueKeys(string $table): array + { + return match ($table) { + 'who_lms_age' => ['indikator', 'jenis_kelamin', 'umur_bulan'], + 'who_lms_wfl' => ['jenis_kelamin', 'panjang'], + 'who_lms_wfh' => ['jenis_kelamin', 'tinggi'], + default => [], + }; + } +} + diff --git a/Website/s_gizi/app/Http/Controllers/Admin/ArticleController.php b/Website/s_gizi/app/Http/Controllers/Admin/ArticleController.php new file mode 100644 index 0000000..aa5424c --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Admin/ArticleController.php @@ -0,0 +1,52 @@ + Article::query()->latest()->paginate(20), + ]); + } + + public function store(Request $request): RedirectResponse + { + Article::query()->create($request->validate([ + 'title' => ['required', 'string', 'max:160'], + 'category' => ['required', 'string', 'max:80'], + 'excerpt' => ['nullable', 'string', 'max:240'], + 'content' => ['required', 'string'], + 'published' => ['nullable', 'boolean'], + ]) + ['published' => $request->boolean('published')]); + + return back()->with('success', 'Artikel berhasil ditambahkan.'); + } + + public function update(Request $request, Article $article): RedirectResponse + { + $article->update($request->validate([ + 'title' => ['required', 'string', 'max:160'], + 'category' => ['required', 'string', 'max:80'], + 'excerpt' => ['nullable', 'string', 'max:240'], + 'content' => ['required', 'string'], + 'published' => ['nullable', 'boolean'], + ]) + ['published' => $request->boolean('published')]); + + return back()->with('success', 'Artikel berhasil diperbarui.'); + } + + public function destroy(Article $article): RedirectResponse + { + $article->delete(); + + return back()->with('success', 'Artikel berhasil dihapus.'); + } +} diff --git a/Website/s_gizi/app/Http/Controllers/Admin/ChildController.php b/Website/s_gizi/app/Http/Controllers/Admin/ChildController.php new file mode 100644 index 0000000..d27d2bf --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Admin/ChildController.php @@ -0,0 +1,69 @@ +query('q', ''); + + $children = Child::query() + ->when($q !== '', fn ($qb) => $qb->where('nama', 'like', "%{$q}%")) + ->orderBy('nama') + ->paginate(15) + ->withQueryString(); + + return view('admin.children.index', compact('children', 'q')); + } + + public function create(): View + { + return view('admin.children.create'); + } + + public function store(Request $request): RedirectResponse + { + $data = $request->validate([ + 'nama' => ['required', 'string', 'max:255'], + 'tanggal_lahir' => ['required', 'date'], + 'jenis_kelamin' => ['required', 'in:L,P'], + ]); + + Child::query()->create($data); + + return redirect()->route('admin.children.index')->with('success', 'Data anak berhasil ditambahkan.'); + } + + public function edit(Child $child): View + { + return view('admin.children.edit', compact('child')); + } + + public function update(Request $request, Child $child): RedirectResponse + { + $data = $request->validate([ + 'nama' => ['required', 'string', 'max:255'], + 'tanggal_lahir' => ['required', 'date'], + 'jenis_kelamin' => ['required', 'in:L,P'], + ]); + + $child->update($data); + + return redirect()->route('admin.children.index')->with('success', 'Data anak berhasil diperbarui.'); + } + + public function destroy(Child $child): RedirectResponse + { + $child->delete(); + + return redirect()->route('admin.children.index')->with('success', 'Data anak berhasil dihapus.'); + } +} + diff --git a/Website/s_gizi/app/Http/Controllers/Admin/DashboardController.php b/Website/s_gizi/app/Http/Controllers/Admin/DashboardController.php new file mode 100644 index 0000000..703adb9 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Admin/DashboardController.php @@ -0,0 +1,29 @@ + Child::query()->count(), + 'countUsers' => User::query()->count(), + 'countMeasurements' => Measurement::query()->count(), + 'countFoods' => Makanan::query()->count(), + 'statusDistribution' => Measurement::query() + ->selectRaw('status_gabungan, count(*) as total') + ->groupBy('status_gabungan') + ->orderByDesc('total') + ->get(), + ]); + } +} + diff --git a/Website/s_gizi/app/Http/Controllers/Admin/FoodConditionController.php b/Website/s_gizi/app/Http/Controllers/Admin/FoodConditionController.php new file mode 100644 index 0000000..8d44c65 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Admin/FoodConditionController.php @@ -0,0 +1,49 @@ +query('status', ''); + + $mappings = FoodCondition::query() + ->with('food') + ->when($status !== '', fn ($qb) => $qb->where('status_gizi', $status)) + ->orderBy('status_gizi') + ->paginate(20) + ->withQueryString(); + + $foods = Makanan::query()->orderBy('nama')->get(['id', 'nama', 'kategori']); + + return view('admin.food_conditions.index', compact('mappings', 'foods', 'status')); + } + + public function store(Request $request): RedirectResponse + { + $data = $request->validate([ + 'food_id' => ['required', 'integer', 'exists:foods,id'], + 'status_gizi' => ['required', 'string', 'max:255'], + ]); + + FoodCondition::query()->firstOrCreate($data); + + return redirect()->route('admin.food_conditions.index')->with('success', 'Mapping berhasil disimpan.'); + } + + public function destroy(FoodCondition $foodCondition): RedirectResponse + { + $foodCondition->delete(); + + return redirect()->route('admin.food_conditions.index')->with('success', 'Mapping berhasil dihapus.'); + } +} + diff --git a/Website/s_gizi/app/Http/Controllers/Admin/FoodController.php b/Website/s_gizi/app/Http/Controllers/Admin/FoodController.php new file mode 100644 index 0000000..493db68 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Admin/FoodController.php @@ -0,0 +1,72 @@ +query('q', ''); + + $foods = Makanan::query() + ->when($q !== '', function ($qb) use ($q) { + $qb->where(function ($w) use ($q) { + $w->where('nama', 'like', "%{$q}%") + ->orWhere('kategori', 'like', "%{$q}%"); + }); + }) + ->orderBy('nama') + ->paginate(15) + ->withQueryString(); + + return view('admin.foods.index', compact('foods', 'q')); + } + + public function create(): View + { + return view('admin.foods.create'); + } + + public function store(Request $request): RedirectResponse + { + $data = $request->validate([ + 'nama' => ['required', 'string', 'max:255'], + 'kategori' => ['required', 'string', 'max:100'], + ]); + + Makanan::query()->create($data); + + return redirect()->route('admin.foods.index')->with('success', 'Makanan berhasil ditambahkan.'); + } + + public function edit(Food $food): View + { + return view('admin.foods.edit', compact('food')); + } + + public function update(Request $request, Food $food): RedirectResponse + { + $data = $request->validate([ + 'nama' => ['required', 'string', 'max:255'], + 'kategori' => ['required', 'string', 'max:100'], + ]); + + $food->update($data); + + return redirect()->route('admin.foods.index')->with('success', 'Makanan berhasil diperbarui.'); + } + + public function destroy(Food $food): RedirectResponse + { + $food->delete(); + + return redirect()->route('admin.foods.index')->with('success', 'Makanan berhasil dihapus.'); + } +} + diff --git a/Website/s_gizi/app/Http/Controllers/Admin/LmsDataController.php b/Website/s_gizi/app/Http/Controllers/Admin/LmsDataController.php new file mode 100644 index 0000000..58f3183 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Admin/LmsDataController.php @@ -0,0 +1,82 @@ +query('type', 'age'); + $model = $this->model($type); + $rows = $model::query()->orderBy($type === 'age' ? 'umur' : ($type === 'wfl' ? 'panjang' : 'tinggi'))->paginate(25)->withQueryString(); + + return view('admin.lms.index', compact('rows', 'type')); + } + + public function store(Request $request): RedirectResponse + { + $type = $request->input('type', 'age'); + $model = $this->model($type); + $model::query()->create($this->validated($request, $type)); + Cache::flush(); + + return back()->with('success', 'Data LMS berhasil ditambahkan.'); + } + + public function update(Request $request, string $type, int $id): RedirectResponse + { + $model = $this->model($type); + $model::query()->findOrFail($id)->update($this->validated($request, $type)); + Cache::flush(); + + return back()->with('success', 'Data LMS berhasil diperbarui.'); + } + + public function destroy(string $type, int $id): RedirectResponse + { + $model = $this->model($type); + $model::query()->findOrFail($id)->delete(); + Cache::flush(); + + return back()->with('success', 'Data LMS berhasil dihapus.'); + } + + private function model(string $type): string + { + return match ($type) { + 'wfh' => WhoLmsWfh::class, + 'wfl' => WhoLmsWfl::class, + default => WhoLmsAge::class, + }; + } + + private function validated(Request $request, string $type): array + { + $base = [ + 'jk' => ['required', 'in:L,P'], + 'L' => ['required', 'numeric'], + 'M' => ['required', 'numeric', 'gt:0'], + 'S' => ['required', 'numeric', 'gt:0'], + ]; + + if ($type === 'age') { + return $request->validate($base + [ + 'indikator' => ['required', 'in:bbu,tbu'], + 'umur' => ['required', 'integer', 'min:0', 'max:60'], + ]); + } + + return $request->validate($base + [ + $type === 'wfl' ? 'panjang' : 'tinggi' => ['required', 'numeric', 'min:30', 'max:130'], + ]); + } +} diff --git a/Website/s_gizi/app/Http/Controllers/Admin/MeasurementController.php b/Website/s_gizi/app/Http/Controllers/Admin/MeasurementController.php new file mode 100644 index 0000000..3458e29 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Admin/MeasurementController.php @@ -0,0 +1,56 @@ +query('q', ''); + + $measurements = Measurement::query() + ->with('child') + ->when($q !== '', function ($qb) use ($q) { + $qb->whereHas('child', fn ($cqb) => $cqb->where('nama', 'like', "%{$q}%")); + }) + ->orderByDesc('tanggal_ukur') + ->orderByDesc('id') + ->paginate(20) + ->withQueryString(); + + return view('admin.measurements.index', compact('measurements', 'q')); + } + + public function export(Request $request): StreamedResponse + { + $filename = 'riwayat-gizi-'.now()->format('Ymd-His').'.csv'; + + return response()->streamDownload(function () { + $out = fopen('php://output', 'w'); + fputcsv($out, ['Anak', 'Tanggal Ukur', 'Berat', 'Tinggi', 'Umur Bulan', 'BB/U', 'TB/U', 'BB/TB', 'Status']); + Measurement::query()->with('child')->orderByDesc('tanggal_ukur')->chunk(200, function ($rows) use ($out) { + foreach ($rows as $row) { + fputcsv($out, [ + $row->child?->nama, + $row->tanggal_ukur?->toDateString(), + $row->berat, + $row->tinggi, + $row->umur_bulan, + $row->z_bbu, + $row->z_tbu, + $row->z_bbtb, + $row->status_gabungan, + ]); + } + }); + fclose($out); + }, $filename, ['Content-Type' => 'text/csv']); + } +} + diff --git a/Website/s_gizi/app/Http/Controllers/Api/ArticleController.php b/Website/s_gizi/app/Http/Controllers/Api/ArticleController.php new file mode 100644 index 0000000..defedf7 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/ArticleController.php @@ -0,0 +1,61 @@ +query('limit', 50); + $limit = $limit > 0 ? min($limit, 100) : 50; + + $category = trim((string) $request->query('category', '')); + + $query = Article::query() + ->where('published', true) + ->latest(); + + if ($category !== '' && strtolower($category) !== 'semua') { + $query->where('category', $category); + } + + $articles = $query + ->limit($limit) + ->get() + ->map(function (Article $article) { + $excerpt = trim((string) ($article->excerpt ?? '')); + $content = trim((string) ($article->content ?? '')); + + if ($excerpt === '' && $content !== '') { + $excerpt = mb_strlen($content) > 160 + ? mb_substr($content, 0, 160) . '...' + : $content; + } + + return [ + 'id' => (int) $article->id, + 'title' => (string) $article->title, + 'description' => $excerpt !== '' ? $excerpt : '-', + 'content' => $content, + 'category' => (string) ($article->category ?? 'Nutrisi Anak'), + 'created_at' => optional($article->created_at)->toISOString(), + // field opsional agar model Flutter tetap aman + 'image' => null, + 'url' => null, + 'source_name' => 'S-Gizi', + ]; + }) + ->values(); + + return response()->json([ + 'source' => 'db_articles', + 'data' => $articles, + ]); + } +} + diff --git a/Website/s_gizi/app/Http/Controllers/Api/AuthController.php b/Website/s_gizi/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 0000000..75f5c42 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,83 @@ +validate([ + 'phone' => ['required', 'regex:/^(\+62|62|0)8[0-9]{7,13}$/'], + ]); + + $phone = $this->normalizePhone($validated['phone']); + $otp = app()->isProduction() ? (string) random_int(100000, 999999) : '123456'; + + User::query()->updateOrCreate( + ['phone' => $phone], + [ + 'role' => 'orang_tua', + 'otp_code' => $otp, + 'otp_expires_at' => now()->addMinutes(5), + ] + ); + + return response()->json([ + 'message' => 'OTP berhasil dikirim.', + 'phone' => $phone, + 'debug_otp' => app()->isProduction() ? null : $otp, + ]); + } + + public function verifyOtp(Request $request): JsonResponse + { + $validated = $request->validate([ + 'phone' => ['required', 'string'], + 'otp' => ['required', 'digits:6'], + ]); + + $phone = $this->normalizePhone($validated['phone']); + $user = User::query()->where('phone', $phone)->first(); + + if (!$user || $user->otp_code !== $validated['otp'] || $user->otp_expires_at?->isPast()) { + return response()->json([ + 'message' => 'OTP salah atau sudah kedaluwarsa.', + ], 422); + } + + $user->forceFill([ + 'api_token' => Str::random(80), + 'otp_code' => null, + 'otp_expires_at' => null, + ])->save(); + + return response()->json([ + 'token' => $user->api_token, + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'phone' => $user->phone, + 'role' => $user->role, + ], + ]); + } + + private function normalizePhone(string $phone): string + { + $digits = preg_replace('/\D+/', '', $phone) ?? ''; + if (str_starts_with($digits, '0')) { + $digits = '62'.substr($digits, 1); + } + if (!str_starts_with($digits, '62')) { + $digits = '62'.$digits; + } + + return '+'.$digits; + } +} diff --git a/Website/s_gizi/app/Http/Controllers/Api/ChildController.php b/Website/s_gizi/app/Http/Controllers/Api/ChildController.php new file mode 100644 index 0000000..38ede3f --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/ChildController.php @@ -0,0 +1,100 @@ +userFromToken($request); + if (!$user) return $this->unauthenticated(); + + $children = $user->children() + ->with([ + 'measurements' => fn ($q) => $q + ->orderByDesc('tanggal_ukur') + ->orderByDesc('id') + ->limit(1), + ]) + ->orderBy('nama') + ->get() + ->map(fn (Child $child) => $this->resource($child)); + + return response()->json(['data' => $children]); + } + + public function store(Request $request): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user) return $this->unauthenticated(); + + $validated = $request->validate([ + 'nama_anak' => ['required_without:nama', 'string', 'max:120'], + 'nama' => ['required_without:nama_anak', 'string', 'max:120'], + 'tanggal_lahir' => ['required', 'date', 'before_or_equal:today'], + 'jenis_kelamin' => ['required', 'in:L,P'], + ]); + + $validated['nama_anak'] = $validated['nama_anak'] ?? $validated['nama']; + $validated['nama'] = $validated['nama'] ?? $validated['nama_anak']; + $child = $user->children()->create($validated); + + return response()->json(['data' => $this->resource($child)], 201); + } + + public function update(Request $request, Child $child): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user || $child->user_id !== $user->id) return $this->unauthenticated(); + + $validated = $request->validate([ + 'nama_anak' => ['sometimes', 'required', 'string', 'max:120'], + 'nama' => ['sometimes', 'required', 'string', 'max:120'], + 'tanggal_lahir' => ['sometimes', 'required', 'date', 'before_or_equal:today'], + 'jenis_kelamin' => ['sometimes', 'required', 'in:L,P'], + ]); + + if (isset($validated['nama_anak']) && !isset($validated['nama'])) { + $validated['nama'] = $validated['nama_anak']; + } + if (isset($validated['nama']) && !isset($validated['nama_anak'])) { + $validated['nama_anak'] = $validated['nama']; + } + $child->update($validated); + + return response()->json(['data' => $this->resource($child)]); + } + + public function destroy(Request $request, Child $child): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user || $child->user_id !== $user->id) return $this->unauthenticated(); + + $child->delete(); + + return response()->json(['message' => 'Data anak berhasil dihapus.']); + } + + private function resource(Child $child): array + { + $latest = $child->measurements->first(); + + return [ + 'id' => $child->id, + 'nama' => $child->nama_anak ?? $child->nama, + 'nama_anak' => $child->nama_anak ?? $child->nama, + 'tanggal_lahir' => optional($child->tanggal_lahir)->toDateString(), + 'jenis_kelamin' => $child->jenis_kelamin, + 'latest_status' => $latest?->status_gabungan, + 'latest_measurement_at' => $latest?->tanggal_ukur?->toDateString(), + ]; + } +} diff --git a/Website/s_gizi/app/Http/Controllers/Api/Concerns/ResolvesApiUser.php b/Website/s_gizi/app/Http/Controllers/Api/Concerns/ResolvesApiUser.php new file mode 100644 index 0000000..03ef876 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/Concerns/ResolvesApiUser.php @@ -0,0 +1,27 @@ +bearerToken(); + if (!$token) { + return null; + } + + return User::query()->where('api_token', $token)->first(); + } + + private function unauthenticated(): JsonResponse + { + return response()->json([ + 'message' => 'Token tidak valid atau belum login.', + ], 401); + } +} diff --git a/Website/s_gizi/app/Http/Controllers/Api/ConsultationController.php b/Website/s_gizi/app/Http/Controllers/Api/ConsultationController.php new file mode 100644 index 0000000..75846ce --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/ConsultationController.php @@ -0,0 +1,248 @@ +userFromToken($request); + if (!$user) { + return $this->unauthenticated(); + } + + $childId = (int) $request->query('child_id', 0); + $query = ConsultationRoom::query()->where('user_id', $user->id); + if ($childId > 0) { + $query->where('child_id', $childId); + } + + $rooms = $query->latest('last_message_at')->latest('updated_at')->get() + ->map(function (ConsultationRoom $room): array { + return [ + 'id' => $room->id, + 'child_id' => $room->child_id, + 'child_name' => (string) optional($room->child)->nama, + 'expert_id' => (string) $room->expert_id, + 'expert_name' => (string) $room->expert_name, + 'specialization' => (string) ($room->specialization ?? ''), + 'asset_image' => (string) ($room->asset_image ?? ''), + 'online' => (bool) $room->online, + 'status' => (string) $room->status, + 'last_message' => (string) ($room->last_message ?? ''), + 'last_message_at' => optional($room->last_message_at)->toISOString(), + 'unread_count' => (int) $room->unread_count, + 'updated_at' => optional($room->updated_at)->toISOString(), + ]; + }) + ->values(); + + return response()->json(['data' => $rooms]); + } + + public function openRoom(Request $request): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user) { + return $this->unauthenticated(); + } + + $validated = $request->validate([ + 'child_id' => ['required', 'integer'], + 'expert_id' => ['required', 'string', 'max:80'], + 'expert_name' => ['required', 'string', 'max:150'], + 'specialization' => ['nullable', 'string', 'max:150'], + 'asset_image' => ['nullable', 'string', 'max:255'], + 'online' => ['nullable', 'boolean'], + ]); + + $child = Child::query()->where('id', $validated['child_id'])->where('user_id', $user->id)->first(); + if (!$child) { + return response()->json(['message' => 'Anak tidak ditemukan.'], 404); + } + + $room = ConsultationRoom::query()->firstOrCreate( + [ + 'user_id' => $user->id, + 'child_id' => $child->id, + 'expert_id' => $validated['expert_id'], + ], + [ + 'expert_name' => $validated['expert_name'], + 'specialization' => $validated['specialization'] ?? '', + 'asset_image' => $validated['asset_image'] ?? '', + 'online' => (bool) ($validated['online'] ?? false), + 'status' => 'aktif', + 'unread_count' => 0, + ] + ); + + return response()->json([ + 'data' => [ + 'id' => $room->id, + 'child_id' => $room->child_id, + 'child_name' => (string) $child->nama, + 'expert_id' => (string) $room->expert_id, + 'expert_name' => (string) $room->expert_name, + 'specialization' => (string) ($room->specialization ?? ''), + 'asset_image' => (string) ($room->asset_image ?? ''), + 'online' => (bool) $room->online, + 'status' => (string) $room->status, + 'last_message' => (string) ($room->last_message ?? ''), + 'last_message_at' => optional($room->last_message_at)->toISOString(), + 'updated_at' => optional($room->updated_at)->toISOString(), + ], + ]); + } + + public function messages(Request $request, int $roomId): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user) { + return $this->unauthenticated(); + } + + $room = ConsultationRoom::query()->where('id', $roomId)->where('user_id', $user->id)->first(); + if (!$room) { + return response()->json(['message' => 'Room tidak ditemukan.'], 404); + } + + $messages = ConsultationMessage::query() + ->where('room_id', $room->id) + ->orderBy('created_at') + ->get() + ->map(fn (ConsultationMessage $m): array => [ + 'id' => $m->id, + 'sender_type' => (string) $m->sender_type, + 'message' => (string) $m->message, + 'is_read' => (bool) $m->is_read, + 'created_at' => optional($m->created_at)->toISOString(), + ]) + ->values(); + + // Saat parent membuka room, tandai pesan dari ahli sebagai sudah dibaca. + ConsultationMessage::query() + ->where('room_id', $room->id) + ->where('sender_type', 'expert') + ->where('is_read', false) + ->update(['is_read' => true]); + if ($room->unread_count > 0) { + $room->update(['unread_count' => 0]); + } + + return response()->json(['data' => $messages]); + } + + public function sendMessage(Request $request, int $roomId): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user) { + return $this->unauthenticated(); + } + + $room = ConsultationRoom::query()->where('id', $roomId)->where('user_id', $user->id)->first(); + if (!$room) { + return response()->json(['message' => 'Room tidak ditemukan.'], 404); + } + + $validated = $request->validate([ + 'message' => ['required', 'string', 'max:3000'], + ]); + + $message = ConsultationMessage::query()->create([ + 'room_id' => $room->id, + 'sender_type' => 'parent', + 'message' => $validated['message'], + 'is_read' => true, + ]); + + $room->update([ + 'status' => 'menunggu', + 'last_message' => $validated['message'], + 'last_message_at' => now(), + ]); + + return response()->json([ + 'data' => [ + 'id' => $message->id, + 'sender_type' => (string) $message->sender_type, + 'message' => (string) $message->message, + 'is_read' => (bool) $message->is_read, + 'created_at' => optional($message->created_at)->toISOString(), + ], + ]); + } + + public function updateStatus(Request $request, int $roomId): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user) { + return $this->unauthenticated(); + } + + $room = ConsultationRoom::query()->where('id', $roomId)->where('user_id', $user->id)->first(); + if (!$room) { + return response()->json(['message' => 'Room tidak ditemukan.'], 404); + } + + $validated = $request->validate([ + 'status' => ['required', 'in:aktif,menunggu,selesai'], + ]); + + $room->update(['status' => $validated['status']]); + + return response()->json(['message' => 'Status berhasil diperbarui.']); + } + + public function sendExpertReply(Request $request, int $roomId): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user) { + return $this->unauthenticated(); + } + + $room = ConsultationRoom::query()->where('id', $roomId)->where('user_id', $user->id)->first(); + if (!$room) { + return response()->json(['message' => 'Room tidak ditemukan.'], 404); + } + + $validated = $request->validate([ + 'message' => ['required', 'string', 'max:3000'], + ]); + + $message = ConsultationMessage::query()->create([ + 'room_id' => $room->id, + 'sender_type' => 'expert', + 'message' => $validated['message'], + 'is_read' => false, + ]); + + $room->update([ + 'status' => 'aktif', + 'last_message' => $validated['message'], + 'last_message_at' => now(), + 'unread_count' => ((int) $room->unread_count) + 1, + ]); + + return response()->json([ + 'data' => [ + 'id' => $message->id, + 'sender_type' => (string) $message->sender_type, + 'message' => (string) $message->message, + 'is_read' => (bool) $message->is_read, + 'created_at' => optional($message->created_at)->toISOString(), + ], + ]); + } +} + diff --git a/Website/s_gizi/app/Http/Controllers/Api/GiziController.php b/Website/s_gizi/app/Http/Controllers/Api/GiziController.php new file mode 100644 index 0000000..57d990e --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/GiziController.php @@ -0,0 +1,139 @@ +userFromToken($request); + if (!$user) return $this->unauthenticated(); + + $validated = $request->validate([ + 'child_id' => ['required', 'integer', 'exists:children,id'], + 'tanggal_ukur' => ['required', 'date'], + 'berat_badan' => ['required', 'numeric', 'gt:0', 'max:80'], + 'tinggi_badan' => ['required', 'numeric', 'gt:0', 'min:30', 'max:130'], + 'cara_ukur' => ['required', 'in:standing,lying'], + ]); + + $child = Child::query() + ->where('user_id', $user->id) + ->findOrFail($validated['child_id']); + + $result = $this->giziService->hitung([ + 'jenis_kelamin' => $child->jenis_kelamin, + 'tanggal_lahir' => $child->tanggal_lahir->toDateString(), + 'tanggal_ukur' => $validated['tanggal_ukur'], + 'berat_badan' => $validated['berat_badan'], + 'tinggi_badan' => $validated['tinggi_badan'], + 'cara_ukur' => $validated['cara_ukur'], + ]); + + if (isset($result['error'])) { + return response()->json($result, 422); + } + + $measurement = Measurement::query()->create([ + 'child_id' => $child->id, + 'berat' => $validated['berat_badan'], + 'tinggi' => $validated['tinggi_badan'], + 'tanggal_ukur' => $validated['tanggal_ukur'], + 'cara_ukur' => $validated['cara_ukur'], + 'umur_bulan' => $result['identitas']['umur_bulan'], + 'z_bbu' => $result['zscore']['bbu'], + 'z_tbu' => $result['zscore']['tbu'], + 'z_bbtb' => $result['zscore']['bbtb'], + 'kategori' => $result['kategori'], + 'status_gabungan' => $result['status_gabungan'], + 'created_at' => now(), + ]); + + GrowthRecord::query()->create([ + 'child_id' => $child->id, + 'tinggi_badan' => $validated['tinggi_badan'], + 'berat_badan' => $validated['berat_badan'], + 'umur_dalam_bulan' => $result['identitas']['umur_bulan'], + 'z_score' => $this->primaryZScore($result['status_gabungan'], $result['zscore']), + 'status_gizi' => $result['status_gabungan'], + 'created_at' => now(), + ]); + + return response()->json([ + ...$result, + 'measurement' => [ + 'id' => $measurement->id, + 'child_id' => $child->id, + 'child_name' => $child->nama_anak ?? $child->nama, + 'tanggal_ukur' => $measurement->tanggal_ukur->toDateString(), + 'cara_ukur' => $measurement->cara_ukur, + ], + ]); + } + + public function riwayat(Request $request, int $childId): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user) return $this->unauthenticated(); + + $child = Child::query()->where('user_id', $user->id)->findOrFail($childId); + + return response()->json([ + 'child' => [ + 'id' => $child->id, + 'nama' => $child->nama_anak ?? $child->nama, + 'nama_anak' => $child->nama_anak ?? $child->nama, + 'tanggal_lahir' => $child->tanggal_lahir->toDateString(), + 'jenis_kelamin' => $child->jenis_kelamin, + ], + 'riwayat' => $child->measurements() + ->orderByDesc('tanggal_ukur') + ->orderByDesc('id') + ->get() + ->map(fn (Measurement $item) => [ + 'id' => $item->id, + 'berat' => $item->berat, + 'tinggi' => $item->tinggi, + 'tanggal_ukur' => $item->tanggal_ukur->toDateString(), + 'cara_ukur' => $item->cara_ukur, + 'umur_bulan' => $item->umur_bulan, + 'status_gabungan' => $item->status_gabungan, + 'kategori' => $item->kategori, + 'z_score' => [ + 'bbu' => $item->z_bbu, + 'tbu' => $item->z_tbu, + 'bbtb' => $item->z_bbtb, + ], + ]), + ]); + } + + private function primaryZScore(string $status, array $scores): float + { + $status = strtolower($status); + + if (str_contains($status, 'stunting') || str_contains($status, 'stunted')) { + return (float) $scores['tbu']; + } + if (str_contains($status, 'wasting') || str_contains($status, 'obesitas') || str_contains($status, 'overweight')) { + return (float) $scores['bbtb']; + } + + return (float) $scores['bbu']; + } +} diff --git a/Website/s_gizi/app/Http/Controllers/Api/HasilController.php b/Website/s_gizi/app/Http/Controllers/Api/HasilController.php new file mode 100644 index 0000000..c58ec63 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/HasilController.php @@ -0,0 +1,36 @@ +giziService->hitung($request->validated()); + return response()->json($result, isset($result['error']) ? 422 : 200); + } catch (\InvalidArgumentException $e) { + return response()->json(['error' => $e->getMessage()], 422); + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + return response()->json(['error' => $e->getMessage()], 404); + } catch (\Throwable $e) { + Log::error('API hitung gizi gagal.', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json(['error' => 'Terjadi kesalahan saat menghitung status gizi.'], 422); + } + } +} + diff --git a/Website/s_gizi/app/Http/Controllers/Api/NewsController.php b/Website/s_gizi/app/Http/Controllers/Api/NewsController.php new file mode 100644 index 0000000..f1323b5 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/NewsController.php @@ -0,0 +1,125 @@ +query('q', 'gizi balita stunting makanan sehat balita')); + $url = 'https://news.google.com/rss/search'; + $response = Http::timeout(12)->get($url, [ + 'q' => $query, + 'hl' => 'id', + 'gl' => 'ID', + 'ceid' => 'ID:id', + ]); + + if (!$response->ok()) return $this->empty($query); + + $xml = @simplexml_load_string($response->body()); + if (!$xml || !isset($xml->channel->item)) return $this->empty($query); + + $allowedCategories = [ + 'Stunting', + 'MPASI', + 'Nutrisi Anak', + 'Protein', + 'Vitamin', + 'Parenting Anak', + 'Kesehatan Balita', + ]; + + $articles = collect($xml->channel->item) + ->values() + ->map(function ($item, $index) { + $rawTitle = trim((string) $item->title); + $description = trim(strip_tags((string) $item->description)); + $content = mb_strlen($description) > 320 + ? mb_substr($description, 0, 320) . '...' + : $description; + $excerpt = mb_strlen($description) > 140 + ? mb_substr($description, 0, 160) . '...' + : $description; + [$title, $sourceName] = $this->splitTitleSource($rawTitle); + $category = $this->detectCategory($title . ' ' . $description); + + return [ + 'id' => $index + 1, + 'title' => $title !== '' ? $title : 'Artikel Gizi Balita', + 'image' => $this->extractImageUrl((string) $item->description), + 'category' => $category, + 'description' => $excerpt !== '' ? $excerpt : 'Informasi terbaru seputar gizi balita dan pencegahan stunting.', + 'content' => $content !== '' ? $content : 'Baca artikel lengkap pada sumber berita.', + 'created_at' => (string) $item->pubDate, + 'source_name' => $sourceName, + 'source' => 'google_news', + 'country' => 'id', + 'url' => (string) $item->link, + ]; + }) + ->filter(function ($article) use ($allowedCategories) { + return in_array($article['category'], $allowedCategories, true); + }) + ->take(12) + ->values(); + + return response()->json([ + 'source' => 'news_api_filtered', + 'country' => 'id', + 'query' => $query, + 'data' => $articles, + ]); + } + + private function empty(string $query): JsonResponse + { + return response()->json([ + 'source' => 'news_api_filtered', + 'country' => 'id', + 'query' => $query, + 'data' => [], + ]); + } + + private function extractImageUrl(string $descriptionHtml): ?string + { + if (preg_match('/]+src="([^">]+)"/i', $descriptionHtml, $match)) { + return trim((string) ($match[1] ?? '')) ?: null; + } + return null; + } + + private function detectCategory(string $text): string + { + $value = mb_strtolower($text); + + if (str_contains($value, 'stunting')) return 'Stunting'; + if (str_contains($value, 'mpasi')) return 'MPASI'; + if (str_contains($value, 'protein') || str_contains($value, 'ikan') || str_contains($value, 'telur')) return 'Protein'; + if (str_contains($value, 'vitamin') || str_contains($value, 'mineral') || str_contains($value, 'mikronutrien')) return 'Vitamin'; + if (str_contains($value, 'parenting') || str_contains($value, 'pola asuh') || str_contains($value, 'orang tua')) return 'Parenting Anak'; + if (str_contains($value, 'balita') || str_contains($value, 'tumbuh kembang') || str_contains($value, 'imunisasi')) return 'Kesehatan Balita'; + if (str_contains($value, 'gizi') || str_contains($value, 'nutrisi') || str_contains($value, 'who nutrition')) return 'Nutrisi Anak'; + return 'Nutrisi Anak'; + } + + private function splitTitleSource(string $rawTitle): array + { + $title = trim($rawTitle); + $source = null; + if (str_contains($rawTitle, ' - ')) { + $parts = explode(' - ', $rawTitle); + if (count($parts) >= 2) { + $source = trim((string) array_pop($parts)); + $title = trim(implode(' - ', $parts)); + } + } + return [$title, $source]; + } +} diff --git a/Website/s_gizi/app/Http/Controllers/Api/ProfileController.php b/Website/s_gizi/app/Http/Controllers/Api/ProfileController.php new file mode 100644 index 0000000..0afc3f7 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/ProfileController.php @@ -0,0 +1,61 @@ +userFromToken($request); + if (!$user) { + return $this->unauthenticated(); + } + + return response()->json([ + 'data' => [ + 'id' => $user->id, + 'phone' => $user->phone, + 'name' => $user->name, + 'email' => $user->email, + 'role' => $user->role ?? 'orang_tua', + 'children_count' => $user->children()->count(), + 'joined_at' => optional($user->created_at)->toDateString(), + ], + ]); + } + + public function update(Request $request): JsonResponse + { + $user = $this->userFromToken($request); + if (!$user) { + return $this->unauthenticated(); + } + + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:120'], + 'phone' => ['required', 'string', 'max:30'], + 'email' => ['nullable', 'email', 'max:160'], + ]); + + $user->update($validated); + + return response()->json([ + 'data' => [ + 'id' => $user->id, + 'phone' => $user->phone, + 'name' => $user->name, + 'email' => $user->email, + 'role' => $user->role ?? 'orang_tua', + 'children_count' => $user->children()->count(), + 'joined_at' => optional($user->created_at)->toDateString(), + ], + ]); + } +} diff --git a/Website/s_gizi/app/Http/Controllers/Api/RecommendationController.php b/Website/s_gizi/app/Http/Controllers/Api/RecommendationController.php new file mode 100644 index 0000000..ac203f6 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Api/RecommendationController.php @@ -0,0 +1,147 @@ +userFromToken($request); + $measurement = $this->resolveMeasurement( + $request, + $user?->id, + ); + $resolvedStatus = $measurement?->status_gabungan ?: $status; + $normalized = $this->normalizeStatus($resolvedStatus); + $conditions = $normalized['matched_categories']; + $ageMonths = $measurement?->umur_bulan; + + $menus = Makanan::query() + ->when( + $ageMonths !== null, + fn ($query) => $query + ->where('usia_min', '<=', (int) floor((float) $ageMonths)) + ->where('usia_max', '>=', (int) ceil((float) $ageMonths)) + ) + ->whereIn('kategori_status', $conditions) + ->orderBy('protein', 'desc') + ->get() + ->groupBy('kategori_status'); + + $items = collect($conditions) + ->flatMap( + fn (string $condition) => $menus + ->get($condition, collect()) + ->take($condition === 'Normal' ? 4 : 6) + ) + ->unique('id') + ->take(12) + ->map(fn (Makanan $makanan) => [ + 'menu' => $makanan->nama, + 'kalori' => $makanan->kalori, + 'protein' => $makanan->protein, + 'lemak' => $makanan->lemak, + 'karbohidrat' => $makanan->karbohidrat, + 'alasan' => $makanan->alasan, + ]); + + return response()->json([ + 'requested_status' => $status, + 'resolved_status' => $resolvedStatus, + 'normalized_status' => $normalized, + 'measurement' => $measurement ? [ + 'id' => $measurement->id, + 'child_id' => $measurement->child_id, + 'child_name' => $measurement->child?->nama_anak ?? $measurement->child?->nama, + 'tanggal_ukur' => $measurement->tanggal_ukur?->toDateString(), + 'umur_bulan' => $measurement->umur_bulan, + ] : null, + 'data' => $items->values(), + ]); + } + + private function resolveMeasurement(Request $request, ?int $userId): ?Measurement + { + $riwayatId = $request->integer('riwayat_id'); + $childId = $request->integer('child_id'); + + if ($riwayatId > 0) { + return Measurement::query() + ->with('child') + ->when( + $userId !== null, + fn ($query) => $query->whereHas( + 'child', + fn ($childQuery) => $childQuery->where('user_id', $userId) + ) + ) + ->when( + $childId > 0, + fn ($query) => $query->where('child_id', $childId) + ) + ->find($riwayatId); + } + + if ($childId > 0 && $userId !== null) { + $child = Child::query() + ->where('user_id', $userId) + ->find($childId); + if (!$child) { + return null; + } + + return $child->measurements() + ->with('child') + ->orderByDesc('tanggal_ukur') + ->orderByDesc('id') + ->first(); + } + + return null; + } + + private function normalizeStatus(string $status): array + { + $value = strtolower($status); + $matched = []; + + if (str_contains($value, 'stunting') || str_contains($value, 'stunted')) { + $matched[] = 'Stunting'; + } + if (str_contains($value, 'wasting')) { + $matched[] = 'Wasting'; + } + if (str_contains($value, 'underweight') || str_contains($value, 'gizi kurang')) { + $matched[] = 'Underweight'; + } + if ( + str_contains($value, 'obesitas') || + str_contains($value, 'overweight') || + str_contains($value, 'gizi lebih') + ) { + $matched[] = 'Obesitas'; + } + + $matched = array_values(array_unique($matched)); + + if ($matched === []) { + $matched = ['Normal']; + } + + return [ + 'original_status' => $status, + 'primary_category' => $matched[0], + 'matched_categories' => $matched, + ]; + } +} diff --git a/Website/s_gizi/app/Http/Controllers/Controller.php b/Website/s_gizi/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/Website/s_gizi/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ + ['required', 'in:L,P'], + 'tanggal_lahir' => ['required', 'date'], + 'tanggal_ukur' => ['required', 'date', 'after_or_equal:tanggal_lahir'], + 'berat_badan' => ['required', 'numeric', 'gt:0', 'max:80'], + 'tinggi_badan' => ['required', 'numeric', 'gt:0', 'min:30', 'max:130'], + 'cara_ukur' => ['required', 'in:standing,lying'], + ]; + } + + protected function failedValidation(Validator $validator): void + { + throw new HttpResponseException(response()->json([ + 'error' => 'Validasi input gagal.', + 'details' => $validator->errors(), + ], 422)); + } +} + diff --git a/Website/s_gizi/app/Http/Requests/StoreHasilRequest.php b/Website/s_gizi/app/Http/Requests/StoreHasilRequest.php new file mode 100644 index 0000000..067c001 --- /dev/null +++ b/Website/s_gizi/app/Http/Requests/StoreHasilRequest.php @@ -0,0 +1,24 @@ + ['required', 'integer', 'exists:children,id'], + 'berat' => ['required', 'numeric', 'min:0.1', 'max:200'], + 'tinggi' => ['required', 'numeric', 'min:30', 'max:200'], + 'tanggal_ukur' => ['required', 'date'], + ]; + } +} + diff --git a/Website/s_gizi/app/Models/Article.php b/Website/s_gizi/app/Models/Article.php new file mode 100644 index 0000000..ff724b2 --- /dev/null +++ b/Website/s_gizi/app/Models/Article.php @@ -0,0 +1,23 @@ + 'boolean', + ]; +} diff --git a/Website/s_gizi/app/Models/Child.php b/Website/s_gizi/app/Models/Child.php new file mode 100644 index 0000000..94acaed --- /dev/null +++ b/Website/s_gizi/app/Models/Child.php @@ -0,0 +1,43 @@ + 'date', + ]; + + public function measurements(): HasMany + { + return $this->hasMany(Measurement::class, 'child_id'); + } + + public function growthRecords(): HasMany + { + return $this->hasMany(GrowthRecord::class, 'child_id'); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} + diff --git a/Website/s_gizi/app/Models/ConsultationMessage.php b/Website/s_gizi/app/Models/ConsultationMessage.php new file mode 100644 index 0000000..4705e86 --- /dev/null +++ b/Website/s_gizi/app/Models/ConsultationMessage.php @@ -0,0 +1,29 @@ + 'boolean', + ]; + + public function room(): BelongsTo + { + return $this->belongsTo(ConsultationRoom::class, 'room_id'); + } +} + diff --git a/Website/s_gizi/app/Models/ConsultationRoom.php b/Website/s_gizi/app/Models/ConsultationRoom.php new file mode 100644 index 0000000..3f331dd --- /dev/null +++ b/Website/s_gizi/app/Models/ConsultationRoom.php @@ -0,0 +1,48 @@ + 'boolean', + 'last_message_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function child(): BelongsTo + { + return $this->belongsTo(Child::class); + } + + public function messages(): HasMany + { + return $this->hasMany(ConsultationMessage::class, 'room_id'); + } +} + diff --git a/Website/s_gizi/app/Models/Food.php b/Website/s_gizi/app/Models/Food.php new file mode 100644 index 0000000..96286f9 --- /dev/null +++ b/Website/s_gizi/app/Models/Food.php @@ -0,0 +1,23 @@ +hasMany(FoodCondition::class, 'food_id'); + } +} + diff --git a/Website/s_gizi/app/Models/FoodCondition.php b/Website/s_gizi/app/Models/FoodCondition.php new file mode 100644 index 0000000..d759d5c --- /dev/null +++ b/Website/s_gizi/app/Models/FoodCondition.php @@ -0,0 +1,23 @@ +belongsTo(Food::class, 'food_id'); + } +} + diff --git a/Website/s_gizi/app/Models/GrowthRecord.php b/Website/s_gizi/app/Models/GrowthRecord.php new file mode 100644 index 0000000..804214c --- /dev/null +++ b/Website/s_gizi/app/Models/GrowthRecord.php @@ -0,0 +1,30 @@ + 'datetime', + ]; + + public function child(): BelongsTo + { + return $this->belongsTo(Child::class); + } +} diff --git a/Website/s_gizi/app/Models/HasilGizi.php b/Website/s_gizi/app/Models/HasilGizi.php new file mode 100644 index 0000000..40be8ca --- /dev/null +++ b/Website/s_gizi/app/Models/HasilGizi.php @@ -0,0 +1,22 @@ + 'date', + 'kategori' => 'array', + 'created_at' => 'datetime', + ]; + + public function child(): BelongsTo + { + return $this->belongsTo(Child::class, 'child_id'); + } +} + diff --git a/Website/s_gizi/app/Models/User.php b/Website/s_gizi/app/Models/User.php new file mode 100644 index 0000000..a81192a --- /dev/null +++ b/Website/s_gizi/app/Models/User.php @@ -0,0 +1,63 @@ + */ + use HasFactory, Notifiable; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'phone', + 'role', + 'password', + 'otp_code', + 'otp_expires_at', + 'api_token', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + 'otp_code', + 'api_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'otp_expires_at' => 'datetime', + 'password' => 'hashed', + ]; + } + + public function children(): HasMany + { + return $this->hasMany(Child::class); + } +} diff --git a/Website/s_gizi/app/Models/WhoLmsAge.php b/Website/s_gizi/app/Models/WhoLmsAge.php new file mode 100644 index 0000000..64954fd --- /dev/null +++ b/Website/s_gizi/app/Models/WhoLmsAge.php @@ -0,0 +1,21 @@ +diffInDays($tanggalUkur); + + // WHO umumnya memakai bulan sebagai 30.4375 hari (365.25/12) untuk pendekatan. + return $days / 30.4375; + } +} + diff --git a/Website/s_gizi/app/Services/GiziService.php b/Website/s_gizi/app/Services/GiziService.php new file mode 100644 index 0000000..eaef229 --- /dev/null +++ b/Website/s_gizi/app/Services/GiziService.php @@ -0,0 +1,557 @@ +validatePayload($payload); + if (isset($validated['error'])) { + return $validated; + } + + $jk = $validated['jenis_kelamin']; + $tanggalLahir = $validated['tanggal_lahir']; + $tanggalUkur = $validated['tanggal_ukur']; + $bb = $validated['berat_badan']; + $tb = $validated['tinggi_badan']; + $caraUkur = $validated['cara_ukur']; + + $umurBulan = $this->ageService->ageInMonthsDecimal($tanggalLahir, $tanggalUkur); + if (!$this->isValidNumber($umurBulan) || $umurBulan < 0.0 || $umurBulan > 60.0) { + return $this->errorResponse('Umur hasil perhitungan harus pada rentang 0-60 bulan.', compact('jk', 'bb', 'tb', 'umurBulan')); + } + + $lmsBbu = $this->lmsAgeInterpolated($jk, 'bbu', $umurBulan); + if ($lmsBbu === null) { + return $this->errorResponse('Data LMS BB/U tidak ditemukan atau tidak valid.', compact('jk', 'bb', 'tb', 'umurBulan')); + } + + $lmsTbu = $this->lmsAgeInterpolated($jk, 'tbu', $umurBulan); + if ($lmsTbu === null) { + return $this->errorResponse('Data LMS TB/U tidak ditemukan atau tidak valid.', compact('jk', 'bb', 'tb', 'umurBulan')); + } + + // Tepat 24 bulan tetap memakai WFL bila cara ukur lying atau umur <= 24 bulan. + $useWfl = $umurBulan <= 24.0 || $caraUkur === 'lying'; + $lmsBbtb = $useWfl + ? $this->lmsLengthInterpolated($jk, $tb) + : $this->lmsHeightInterpolated($jk, $tb); + + if ($lmsBbtb === null) { + $jenis = $useWfl ? 'panjang badan (WFL)' : 'tinggi badan (WFH)'; + return $this->errorResponse("Data LMS BB/TB untuk {$jenis} tidak ditemukan atau tidak valid.", compact('jk', 'bb', 'tb', 'umurBulan')); + } + + $zBbu = $this->zScoreService->zScore($bb, $lmsBbu['L'], $lmsBbu['M'], $lmsBbu['S']); + $zTbu = $this->zScoreService->zScore($tb, $lmsTbu['L'], $lmsTbu['M'], $lmsTbu['S']); + $zBbtb = $this->zScoreService->zScore($bb, $lmsBbtb['L'], $lmsBbtb['M'], $lmsBbtb['S']); + + if (!$this->validZScores($zBbu, $zTbu, $zBbtb)) { + return $this->errorResponse('Perhitungan gagal: z-score tidak valid. Periksa BB/TB, umur, jenis kelamin, dan kelengkapan data LMS.', [ + 'jk' => $jk, + 'bb' => $bb, + 'tb' => $tb, + 'umur_bulan' => $umurBulan, + 'z_bbu' => $zBbu, + 'z_tbu' => $zTbu, + 'z_bbtb' => $zBbtb, + ]); + } + + if (max(abs($zBbu), abs($zTbu), abs($zBbtb)) > self::MAX_REASONABLE_Z) { + return $this->errorResponse('Data terlalu ekstrem untuk dihitung aman dengan standar WHO LMS.', [ + 'jk' => $jk, + 'bb' => $bb, + 'tb' => $tb, + 'umur_bulan' => $umurBulan, + 'z_bbu' => $zBbu, + 'z_tbu' => $zTbu, + 'z_bbtb' => $zBbtb, + ]); + } + + $kategoriBbu = $this->kategoriBbu($zBbu); + $kategoriTbu = $this->kategoriTbu($zTbu); + $kategoriBbtb = $this->kategoriBbtb($zBbtb); + $statusGabungan = $this->statusGabungan($zTbu, $zBbtb, $zBbu); + $kondisi = $this->kondisiKhusus($zTbu, $zBbtb, $zBbu); + $rekomendasi = $this->rekomendasiMakananAdvanced($umurBulan, $kondisi); + + $this->saveHasilGizi($bb, $tb, $umurBulan, $zBbu, $zTbu, $zBbtb, $statusGabungan); + + return [ + 'identitas' => [ + 'umur_bulan' => round($umurBulan, 2), + 'jenis_kelamin' => $jk, + 'cara_ukur' => $caraUkur, + 'standar_bbtb' => $useWfl ? 'WFL' : 'WFH', + ], + 'zscore' => [ + 'bbu' => round($zBbu, 2), + 'tbu' => round($zTbu, 2), + 'bbtb' => round($zBbtb, 2), + ], + 'kategori' => [ + 'bbu' => $kategoriBbu, + 'tbu' => $kategoriTbu, + 'bbtb' => $kategoriBbtb, + ], + 'status_gabungan' => $statusGabungan, + 'rekomendasi' => $rekomendasi, + ]; + } catch (\Throwable $e) { + Log::error('GiziService gagal menghitung status gizi.', [ + 'message' => $e->getMessage(), + 'payload' => $payload, + 'trace' => $e->getTraceAsString(), + ]); + + return $this->errorResponse('Perhitungan status gizi gagal diproses. Silakan periksa data input dan data LMS.'); + } + } + + /** + * @return array + */ + private function validatePayload(array $payload): array + { + foreach (['jenis_kelamin', 'tanggal_lahir', 'tanggal_ukur', 'berat_badan', 'tinggi_badan', 'cara_ukur'] as $field) { + if (!array_key_exists($field, $payload)) { + return $this->errorResponse("Field {$field} wajib diisi."); + } + } + + $jk = (string) $payload['jenis_kelamin']; + if (!in_array($jk, ['L', 'P'], true)) { + return $this->errorResponse('Jenis kelamin harus L atau P.'); + } + + $caraUkur = (string) $payload['cara_ukur']; + if (!in_array($caraUkur, ['standing', 'lying'], true)) { + return $this->errorResponse('Cara ukur harus standing atau lying.'); + } + + if (!is_numeric($payload['berat_badan']) || !is_numeric($payload['tinggi_badan'])) { + return $this->errorResponse('BB dan TB harus berupa angka.'); + } + + $bb = (float) $payload['berat_badan']; + $tb = (float) $payload['tinggi_badan']; + if (!$this->isValidNumber($bb) || $bb <= 0.0) { + return $this->errorResponse('Berat badan harus lebih dari 0 kg.'); + } + + if (!$this->isValidNumber($tb) || $tb <= 0.0) { + return $this->errorResponse('Tinggi/panjang badan harus lebih dari 0 cm.'); + } + + if ($bb > 80.0 || $tb < 30.0 || $tb > 130.0) { + return $this->errorResponse('Data BB/TB berada di luar rentang balita yang wajar untuk standar WHO.'); + } + + try { + $tanggalLahir = Carbon::parse($payload['tanggal_lahir'])->startOfDay(); + $tanggalUkur = Carbon::parse($payload['tanggal_ukur'])->startOfDay(); + } catch (\Throwable $e) { + Log::warning('Format tanggal input gizi tidak valid.', ['payload' => $payload, 'message' => $e->getMessage()]); + return $this->errorResponse('Tanggal lahir atau tanggal ukur tidak valid.'); + } + + if ($tanggalUkur->lt($tanggalLahir)) { + return $this->errorResponse('Tanggal ukur tidak boleh sebelum tanggal lahir.'); + } + + return [ + 'jenis_kelamin' => $jk, + 'tanggal_lahir' => $tanggalLahir, + 'tanggal_ukur' => $tanggalUkur, + 'berat_badan' => $bb, + 'tinggi_badan' => $tb, + 'cara_ukur' => $caraUkur, + ]; + } + + /** + * @return array{L:float,M:float,S:float}|null + */ + private function lmsAgeInterpolated(string $jk, string $indikator, float $umurBulan): ?array + { + if (!$this->isValidNumber($umurBulan) || $umurBulan < 0.0 || $umurBulan > 60.0) { + Log::warning('Umur di luar rentang LMS umur.', compact('jk', 'indikator', 'umurBulan')); + return null; + } + + $umurBawah = max(0, (int) floor($umurBulan)); + $umurAtas = min(60, (int) ceil($umurBulan)); + + $row1 = Cache::remember("lms_age:{$jk}:{$indikator}:{$umurBawah}", now()->addDay(), fn () => WhoLmsAge::query() + ->where('indikator', $indikator) + ->where('jk', $jk) + ->where('umur', $umurBawah) + ->first()); + + $row2 = Cache::remember("lms_age:{$jk}:{$indikator}:{$umurAtas}", now()->addDay(), fn () => WhoLmsAge::query() + ->where('indikator', $indikator) + ->where('jk', $jk) + ->where('umur', $umurAtas) + ->first()); + + if (!$row1 || !$row2) { + Log::warning('Data LMS umur tidak ditemukan.', compact('jk', 'indikator', 'umurBulan', 'umurBawah', 'umurAtas')); + return null; + } + + if ($umurBawah === $umurAtas) { + return $this->normalizeLmsRow($row1, 'umur'); + } + + $lower = $this->normalizeLmsRow($row1, 'umur'); + $upper = $this->normalizeLmsRow($row2, 'umur'); + if ($lower === null || $upper === null) { + Log::warning('Nilai LMS umur tidak valid.', compact('jk', 'indikator', 'umurBulan')); + return null; + } + + return $this->interpolateLms($lower, $upper, $umurBulan, (float) $umurBawah, (float) $umurAtas); + } + + /** + * Tinggi (WFH) interpolasi jika tidak ada exact match. + * @return array{L:float,M:float,S:float}|null + */ + private function lmsHeightInterpolated(string $jk, float $tinggiCm): ?array + { + return $this->lmsByAnthropometry(WhoLmsWfh::class, 'tinggi', $jk, $tinggiCm, 'WFH'); + } + + /** + * Panjang (WFL) interpolasi jika tidak ada exact match. + * @return array{L:float,M:float,S:float}|null + */ + private function lmsLengthInterpolated(string $jk, float $panjangCm): ?array + { + return $this->lmsByAnthropometry(WhoLmsWfl::class, 'panjang', $jk, $panjangCm, 'WFL'); + } + + /** + * @param class-string $model + * @return array{L:float,M:float,S:float}|null + */ + private function lmsByAnthropometry(string $model, string $column, string $jk, float $value, string $label): ?array + { + if (!$this->isValidNumber($value) || $value <= 0.0) { + Log::warning("Nilai {$label} tidak valid.", compact('jk', 'value')); + return null; + } + + $cacheValue = number_format($value, 2, '.', ''); + $lower = Cache::remember("lms_{$label}:{$jk}:{$cacheValue}:lower", now()->addDay(), fn () => $model::query() + ->where('jk', $jk) + ->where($column, '<=', $value) + ->orderByDesc($column) + ->first()); + + $upper = Cache::remember("lms_{$label}:{$jk}:{$cacheValue}:upper", now()->addDay(), fn () => $model::query() + ->where('jk', $jk) + ->where($column, '>=', $value) + ->orderBy($column) + ->first()); + + if (!$lower || !$upper) { + Log::warning("Data LMS {$label} tidak ditemukan.", ['jk' => $jk, $column => $value]); + return null; + } + + $lowerValue = (float) $lower->{$column}; + $upperValue = (float) $upper->{$column}; + if (!$this->isValidNumber($lowerValue) || !$this->isValidNumber($upperValue)) { + Log::warning("Batas LMS {$label} tidak valid.", ['jk' => $jk, $column => $value]); + return null; + } + + if (abs($lowerValue - $upperValue) < 0.0000001) { + return $this->normalizeLmsRow($lower, $column); + } + + $lowerLms = $this->normalizeLmsRow($lower, $column); + $upperLms = $this->normalizeLmsRow($upper, $column); + if ($lowerLms === null || $upperLms === null) { + Log::warning("Nilai LMS {$label} tidak valid.", ['jk' => $jk, $column => $value]); + return null; + } + + return $this->interpolateLms($lowerLms, $upperLms, $value, $lowerValue, $upperValue); + } + + /** + * @return array{L:float,M:float,S:float}|null + */ + private function normalizeLmsRow(object $row, string $context): ?array + { + $l = (float) ($row->L ?? NAN); + $m = (float) ($row->M ?? NAN); + $s = (float) ($row->S ?? NAN); + + if (!$this->isValidNumber($l) || !$this->isValidNumber($m) || !$this->isValidNumber($s) || $m <= 0.0 || $s <= 0.0) { + Log::warning('Nilai LMS tidak valid.', ['context' => $context, 'L' => $row->L ?? null, 'M' => $row->M ?? null, 'S' => $row->S ?? null]); + return null; + } + + return ['L' => $l, 'M' => $m, 'S' => $s]; + } + + /** + * @param array{L:float,M:float,S:float} $lower + * @param array{L:float,M:float,S:float} $upper + * @return array{L:float,M:float,S:float}|null + */ + private function interpolateLms(array $lower, array $upper, float $value, float $lowerValue, float $upperValue): ?array + { + $range = $upperValue - $lowerValue; + if (abs($range) < 0.0000001) { + return $lower; + } + + $t = ($value - $lowerValue) / $range; + if (!$this->isValidNumber($t)) { + return null; + } + + $result = [ + 'L' => $lower['L'] + ($upper['L'] - $lower['L']) * $t, + 'M' => $lower['M'] + ($upper['M'] - $lower['M']) * $t, + 'S' => $lower['S'] + ($upper['S'] - $lower['S']) * $t, + ]; + + return $this->validLms($result) ? $result : null; + } + + /** + * @param array{L:float,M:float,S:float} $lms + */ + private function validLms(array $lms): bool + { + return $this->isValidNumber($lms['L'] ?? null) + && $this->isValidNumber($lms['M'] ?? null) + && $this->isValidNumber($lms['S'] ?? null) + && $lms['M'] > 0.0 + && $lms['S'] > 0.0; + } + + private function kategoriBbu(float $z): string + { + if ($z < -3.0) return 'Sangat Kurang'; + if ($z < -2.0) return 'Kurang'; + if ($z <= 2.0) return 'Normal'; + return 'Risiko Lebih'; + } + + private function kategoriTbu(float $z): string + { + if ($z < -3.0) return 'Sangat Pendek'; + if ($z < -2.0) return 'Pendek'; + return 'Normal'; + } + + private function kategoriBbtb(float $z): string + { + if ($z < -3.0) return 'Gizi Buruk'; + if ($z < -2.0) return 'Gizi Kurang'; + if ($z <= 1.0) return 'Normal'; + if ($z <= 2.0) return 'Risiko Lebih'; + if ($z <= 3.0) return 'Gizi Lebih'; + return 'Obesitas'; + } + + private function statusGabungan(?float $zTbu, ?float $zBbtb, ?float $zBbu): string + { + if (!$this->validZScores($zBbu, $zTbu, $zBbtb)) { + return 'Data tidak valid'; + } + + if ($zTbu < -3.0) { + if ($zBbtb > 3.0) return 'Severely Stunted + Obesitas'; + if ($zBbtb > 2.0) return 'Severely Stunted + Overweight'; + if ($zBbtb < -3.0) return 'Severely Stunted + Severe Wasting'; + if ($zBbtb < -2.0) return 'Severely Stunted + Wasting'; + if ($zBbu < -3.0) return 'Severely Stunted + Severe Underweight'; + if ($zBbu < -2.0) return 'Severely Stunted + Underweight'; + return 'Severely Stunted'; + } + + if ($zTbu < -2.0) { + if ($zBbtb > 3.0) return 'Stunting + Obesitas'; + if ($zBbtb > 2.0) return 'Stunting + Overweight'; + if ($zBbtb < -3.0) return 'Stunting + Severe Wasting'; + if ($zBbtb < -2.0) return 'Stunting + Wasting'; + if ($zBbu < -3.0) return 'Stunting + Severe Underweight'; + if ($zBbu < -2.0) return 'Stunting + Underweight'; + return 'Stunting'; + } + + if ($zBbtb < -3.0) { + if ($zBbu < -3.0) return 'Severe Wasting + Severe Underweight'; + if ($zBbu < -2.0) return 'Severe Wasting + Underweight'; + return 'Severe Wasting'; + } + + if ($zBbtb < -2.0) { + if ($zBbu < -3.0) return 'Wasting + Severe Underweight'; + if ($zBbu < -2.0) return 'Wasting + Underweight'; + return 'Wasting'; + } + + if ($zBbu < -3.0) return 'Severe Underweight'; + if ($zBbu < -2.0) return 'Underweight'; + + if ($zBbtb > 3.0) return 'Obesitas'; + if ($zBbtb > 2.0) return 'Overweight'; + if ($zBbtb > 1.0) return 'Risiko Lebih'; + + return 'Normal'; + } + + private function kondisiKhusus(float $zTbu, float $zBbtb, float $zBbu): string + { + if ($zTbu < -2.0 && $zBbtb > 2.0) return 'Stunting_Obesitas'; + if ($zTbu < -2.0) return 'Stunting'; + if ($zBbtb < -2.0) return 'Wasting'; + if ($zBbu < -2.0) return 'Underweight'; + if ($zBbtb > 2.0) return 'Obesitas'; + return 'Normal'; + } + + /** + * @return array + */ + private function rekomendasiMakananAdvanced(float $umurBulan, string $kondisi): array + { + $umur = max(0, (int) floor($umurBulan)); + + $items = Makanan::query() + ->where('usia_min', '<=', $umur) + ->where('usia_max', '>=', $umur) + ->where(function ($q) use ($kondisi) { + if ($kondisi === 'Stunting_Obesitas') { + $q->whereIn('kategori_status', ['Stunting', 'Normal']); + } else { + $q->where('kategori_status', $kondisi); + } + }) + ->get(); + + if ($kondisi === 'Stunting_Obesitas') { + $items = $items->filter(fn ($m) => (float) $m->protein >= 10.0 && (float) $m->lemak <= 10.0); + } + + if ($items->isEmpty()) { + $items = Makanan::query() + ->where('usia_min', '<=', $umur) + ->where('usia_max', '>=', $umur) + ->where('kategori_status', 'Normal') + ->get(); + } + + return $items->take(5)->map(fn ($m) => [ + 'menu' => (string) $m->nama, + 'kalori' => (int) $m->kalori, + 'protein' => (int) $m->protein, + 'lemak' => (int) $m->lemak, + 'karbohidrat' => (int) $m->karbohidrat, + 'alasan' => $this->generateAlasan($kondisi), + ])->values()->all(); + } + + private function generateAlasan(string $kondisi): string + { + return match ($kondisi) { + 'Stunting_Obesitas' => 'Mendukung pertumbuhan tinggi badan dengan asupan protein tinggi, serta mengontrol kelebihan berat badan melalui pembatasan lemak dan kalori.', + 'Stunting' => 'Membantu mengejar pertumbuhan tinggi badan dengan meningkatkan asupan protein dan nutrisi penting.', + 'Wasting' => 'Meningkatkan berat badan dengan asupan energi dan kalori yang lebih tinggi.', + 'Underweight' => 'Meningkatkan berat badan melalui asupan energi dan protein yang cukup.', + 'Obesitas' => 'Mengontrol berat badan dengan mengurangi asupan kalori dan lemak berlebih.', + default => 'Menjaga keseimbangan asupan nutrisi untuk mendukung pertumbuhan dan kesehatan.', + }; + } + + private function saveHasilGizi(float $bb, float $tb, float $umurBulan, float $zBbu, float $zTbu, float $zBbtb, string $statusGabungan): void + { + try { + HasilGizi::query()->create([ + 'berat' => $bb, + 'tinggi' => $tb, + 'umur' => $umurBulan, + 'z_bbu' => $zBbu, + 'z_tbu' => $zTbu, + 'z_bbtb' => $zBbtb, + 'status_gabungan' => $statusGabungan, + 'created_at' => now(), + ]); + } catch (\Throwable $e) { + Log::warning('Hasil gizi berhasil dihitung tetapi gagal disimpan.', [ + 'message' => $e->getMessage(), + 'status_gabungan' => $statusGabungan, + 'bb' => $bb, + 'tb' => $tb, + 'umur_bulan' => $umurBulan, + ]); + } + } + + private function validZScores(?float ...$scores): bool + { + foreach ($scores as $score) { + if (!$this->isValidNumber($score)) { + return false; + } + } + + return true; + } + + private function isValidNumber(?float $value): bool + { + return $value !== null && is_finite($value); + } + + /** + * @param array $context + * @return array{error:string} + */ + private function errorResponse(string $message, array $context = []): array + { + Log::warning('Perhitungan gizi ditolak.', ['error' => $message, 'context' => $context]); + + return ['error' => $message]; + } +} diff --git a/Website/s_gizi/app/Services/LmsService.php b/Website/s_gizi/app/Services/LmsService.php new file mode 100644 index 0000000..b7a09ac --- /dev/null +++ b/Website/s_gizi/app/Services/LmsService.php @@ -0,0 +1,148 @@ +where('indikator', $indikator) + ->where('jenis_kelamin', $jenisKelamin) + ->where('umur_bulan', '<=', $umurBulan) + ->orderByDesc('umur_bulan') + ->first(); + + $upper = WhoLmsAge::query() + ->where('indikator', $indikator) + ->where('jenis_kelamin', $jenisKelamin) + ->where('umur_bulan', '>=', $umurBulan) + ->orderBy('umur_bulan') + ->first(); + + if (!$lower && !$upper) { + throw new ModelNotFoundException("LMS WHO tidak ditemukan untuk indikator={$indikator}, jk={$jenisKelamin}"); + } + + if (!$lower) { + return ['l' => (float) $upper->l, 'm' => (float) $upper->m, 's' => (float) $upper->s]; + } + + if (!$upper) { + return ['l' => (float) $lower->l, 'm' => (float) $lower->m, 's' => (float) $lower->s]; + } + + if ((float) $lower->umur_bulan === (float) $upper->umur_bulan) { + return ['l' => (float) $lower->l, 'm' => (float) $lower->m, 's' => (float) $lower->s]; + } + + $t = ($umurBulan - (float) $lower->umur_bulan) / ((float) $upper->umur_bulan - (float) $lower->umur_bulan); + + return [ + 'l' => (float) $lower->l + $t * ((float) $upper->l - (float) $lower->l), + 'm' => (float) $lower->m + $t * ((float) $upper->m - (float) $lower->m), + 's' => (float) $lower->s + $t * ((float) $upper->s - (float) $lower->s), + ]; + } + + /** + * Ambil LMS indikator BB/TB berdasarkan tinggi (cm). + * Tinggi biasanya step 0.1; jika tidak tepat, interpolasi linear. + * + * @return array{l: float, m: float, s: float} + */ + public function lmsByHeight(string $jenisKelamin, float $tinggiCm): array + { + $lower = WhoLmsWfh::query() + ->where('jenis_kelamin', $jenisKelamin) + ->where('tinggi', '<=', $tinggiCm) + ->orderByDesc('tinggi') + ->first(); + + $upper = WhoLmsWfh::query() + ->where('jenis_kelamin', $jenisKelamin) + ->where('tinggi', '>=', $tinggiCm) + ->orderBy('tinggi') + ->first(); + + if (!$lower && !$upper) { + throw new ModelNotFoundException("LMS WHO BB/TB tidak ditemukan untuk jk={$jenisKelamin}"); + } + + if (!$lower) { + return ['l' => (float) $upper->l, 'm' => (float) $upper->m, 's' => (float) $upper->s]; + } + + if (!$upper) { + return ['l' => (float) $lower->l, 'm' => (float) $lower->m, 's' => (float) $lower->s]; + } + + if ((float) $lower->tinggi === (float) $upper->tinggi) { + return ['l' => (float) $lower->l, 'm' => (float) $lower->m, 's' => (float) $lower->s]; + } + + $t = ($tinggiCm - (float) $lower->tinggi) / ((float) $upper->tinggi - (float) $lower->tinggi); + + return [ + 'l' => (float) $lower->l + $t * ((float) $upper->l - (float) $lower->l), + 'm' => (float) $lower->m + $t * ((float) $upper->m - (float) $lower->m), + 's' => (float) $lower->s + $t * ((float) $upper->s - (float) $lower->s), + ]; + } + + /** + * Ambil LMS indikator BB/PB (wfl) berdasarkan panjang (cm). + * + * @return array{l: float, m: float, s: float} + */ + public function lmsByLength(string $jenisKelamin, float $panjangCm): array + { + $lower = WhoLmsWfl::query() + ->where('jenis_kelamin', $jenisKelamin) + ->where('panjang', '<=', $panjangCm) + ->orderByDesc('panjang') + ->first(); + + $upper = WhoLmsWfl::query() + ->where('jenis_kelamin', $jenisKelamin) + ->where('panjang', '>=', $panjangCm) + ->orderBy('panjang') + ->first(); + + if (!$lower && !$upper) { + throw new ModelNotFoundException("LMS WHO BB/PB tidak ditemukan untuk jk={$jenisKelamin}"); + } + + if (!$lower) { + return ['l' => (float) $upper->l, 'm' => (float) $upper->m, 's' => (float) $upper->s]; + } + + if (!$upper) { + return ['l' => (float) $lower->l, 'm' => (float) $lower->m, 's' => (float) $lower->s]; + } + + if ((float) $lower->panjang === (float) $upper->panjang) { + return ['l' => (float) $lower->l, 'm' => (float) $lower->m, 's' => (float) $lower->s]; + } + + $t = ($panjangCm - (float) $lower->panjang) / ((float) $upper->panjang - (float) $lower->panjang); + + return [ + 'l' => (float) $lower->l + $t * ((float) $upper->l - (float) $lower->l), + 'm' => (float) $lower->m + $t * ((float) $upper->m - (float) $lower->m), + 's' => (float) $lower->s + $t * ((float) $upper->s - (float) $lower->s), + ]; + } +} + diff --git a/Website/s_gizi/app/Services/MenuService.php b/Website/s_gizi/app/Services/MenuService.php new file mode 100644 index 0000000..1b377bd --- /dev/null +++ b/Website/s_gizi/app/Services/MenuService.php @@ -0,0 +1,46 @@ +, siang: array, malam: array, snack: array} + */ + public function generate(string $statusGabungan): array + { + $foodsForStatus = Food::query() + ->select('foods.*') + ->join('food_conditions', 'food_conditions.food_id', '=', 'foods.id') + ->where('food_conditions.status_gizi', $statusGabungan) + ->inRandomOrder() + ->get(); + + $snacks = Food::query() + ->whereRaw('LOWER(kategori) = ?', ['vitamin']) + ->inRandomOrder() + ->limit(2) + ->get(); + + $pick = fn ($collection, int $n) => $collection + ->take($n) + ->map(fn (Food $f) => ['id' => $f->id, 'nama' => $f->nama, 'kategori' => $f->kategori]) + ->values() + ->all(); + + return [ + 'pagi' => $pick($foodsForStatus->slice(0), 2), + 'siang' => $pick($foodsForStatus->slice(2), 2), + 'malam' => $pick($foodsForStatus->slice(4), 2), + 'snack' => $snacks->map(fn (Food $f) => ['id' => $f->id, 'nama' => $f->nama, 'kategori' => $f->kategori])->values()->all(), + ]; + } +} + diff --git a/Website/s_gizi/app/Services/PenjelasanService.php b/Website/s_gizi/app/Services/PenjelasanService.php new file mode 100644 index 0000000..8ca508f --- /dev/null +++ b/Website/s_gizi/app/Services/PenjelasanService.php @@ -0,0 +1,22 @@ + 'BB/TB sangat rendah. Perlu peningkatan asupan energi-protein, pemantauan ketat, dan evaluasi kemungkinan infeksi/penyakit penyerta.', + 'Gizi Kurang' => 'Berat badan relatif kurang. Fokus pada menu padat energi dan protein, frekuensi makan cukup, dan pemantauan rutin.', + 'Stunting Berat' => 'TB/U sangat rendah (stunting berat). Perlu perbaikan kualitas asupan jangka panjang (protein hewani, mikronutrien) dan pantau tumbuh kembang.', + 'Stunting' => 'TB/U rendah (stunting). Tingkatkan kualitas asupan (protein hewani, zat besi, zinc) dan pastikan pola makan serta kebersihan baik.', + 'Stunting + Risiko Gizi Lebih' => 'Tinggi badan rendah tetapi BB/TB cenderung tinggi. Perbaiki kualitas makanan tanpa berlebihan kalori kosong, fokus protein dan mikronutrien.', + 'Obesitas' => 'BB/TB sangat tinggi. Perlu pengaturan porsi, batasi gula/lemak berlebih, dan dorong aktivitas fisik sesuai usia.', + 'Gizi Lebih' => 'BB/TB tinggi. Perhatikan porsi dan kualitas makanan, kurangi minuman manis dan snack tinggi kalori.', + 'Risiko Gizi Lebih' => 'BB/TB mulai meningkat. Jaga pola makan seimbang dan porsi sesuai kebutuhan agar tidak berkembang menjadi gizi lebih.', + default => 'Status gizi dalam batas normal. Pertahankan pola makan seimbang (karbo, protein, sayur-buah) dan lakukan pemantauan berkala.', + }; + } +} + diff --git a/Website/s_gizi/app/Services/StatusGiziService.php b/Website/s_gizi/app/Services/StatusGiziService.php new file mode 100644 index 0000000..d212388 --- /dev/null +++ b/Website/s_gizi/app/Services/StatusGiziService.php @@ -0,0 +1,53 @@ + 1.0) { + return 'Stunting + Risiko Gizi Lebih'; + } + if ($zTbu < -2.0) { + return 'Stunting'; + } + + // Aturan BB/TB (gizi lebih) + if ($zBbtb > 3.0) { + return 'Obesitas'; + } + if ($zBbtb > 2.0) { + return 'Gizi Lebih'; + } + if ($zBbtb > 1.0) { + return 'Risiko Gizi Lebih'; + } + + // Aturan BB/U (underweight) + if ($zBbu < -2.0) { + return 'Gizi Kurang'; + } + + return 'Normal'; + } +} + diff --git a/Website/s_gizi/app/Services/ZScoreService.php b/Website/s_gizi/app/Services/ZScoreService.php new file mode 100644 index 0000000..0f96e32 --- /dev/null +++ b/Website/s_gizi/app/Services/ZScoreService.php @@ -0,0 +1,51 @@ +isValidNumber($x) || !$this->isValidNumber($l) || !$this->isValidNumber($m) || !$this->isValidNumber($s)) { + return null; + } + + if ($x <= 0.0 || $m <= 0.0 || $s <= 0.0) { + return null; + } + + $ratio = $x / $m; + if ($ratio <= 0.0 || !is_finite($ratio)) { + return null; + } + + if (abs($l) < 0.0000001) { + $z = log($ratio) / $s; + } else { + $denominator = $l * $s; + if ($denominator == 0.0) { + return null; + } + + $powered = pow($ratio, $l); + if (!is_finite($powered)) { + return null; + } + + $z = ($powered - 1.0) / $denominator; + } + + return is_finite($z) ? $z : null; + } + + private function isValidNumber(?float $value): bool + { + return $value !== null && is_finite($value); + } +} + diff --git a/Website/s_gizi/artisan b/Website/s_gizi/artisan new file mode 100644 index 0000000..c35e31d --- /dev/null +++ b/Website/s_gizi/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/Website/s_gizi/bootstrap/app.php b/Website/s_gizi/bootstrap/app.php new file mode 100644 index 0000000..c3928c5 --- /dev/null +++ b/Website/s_gizi/bootstrap/app.php @@ -0,0 +1,19 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // + }) + ->withExceptions(function (Exceptions $exceptions): void { + // + })->create(); diff --git a/Website/s_gizi/bootstrap/cache/.gitignore b/Website/s_gizi/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/Website/s_gizi/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/Website/s_gizi/bootstrap/providers.php b/Website/s_gizi/bootstrap/providers.php new file mode 100644 index 0000000..fc94ae6 --- /dev/null +++ b/Website/s_gizi/bootstrap/providers.php @@ -0,0 +1,7 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-03-10T16:41:02+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.56.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "dac16d424b59debb2273910dde88eb7050a2a709" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/dac16d424b59debb2273910dde88eb7050a2a709", + "reference": "dac16d424b59debb2273910dde88eb7050a2a709", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.1.41", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-03-26T14:51:54+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.17", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/6a82ac19a28b916ae0885828795dbd4c59d9a818", + "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.17" + }, + "time": "2026-04-20T16:07:33+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.12", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/a6abb4e54f6fcd3138120b9ad497f0bd146f9919", + "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-04-14T13:33:34+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.11.1" + }, + "time": "2026-02-06T14:12:35+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.33.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "570b8871e0ce693764434b29154c54b434905350" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/570b8871e0ce693764434b29154c54b434905350", + "reference": "570b8871e0ce693764434b29154c54b434905350", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.33.0" + }, + "time": "2026-03-25T07:59:30+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.4", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-04-07T09:57:54+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.3" + }, + "time": "2026-02-13T03:05:33+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.22", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.22" + }, + "time": "2026-03-22T23:03:24+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:54:39+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "b055f228a4178a1d6774909903905e3475f3eac8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b055f228a4178a1d6774909903905e3475f3eac8", + "reference": "b055f228a4178a1d6774909903905e3475f3eac8", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "f57b899fa736fd71121168ef268f23c206083f0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a", + "reference": "f57b899fa736fd71121168ef268f23c206083f0a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:54:39+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e0be088d22278583a82da281886e8c3592fbf149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab", + "reference": "9381209597ec66c25be154cbf2289076e64d1eab", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "017e76ad089bac281553389269e259e155935e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/017e76ad089bac281553389269e259e155935e1a", + "reference": "017e76ad089bac281553389269e259e155935e1a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-31T20:57:01+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f6ea532250b476bfc1b56699b388a1bdbf168f62", + "reference": "f6ea532250b476bfc1b56699b388a1bdbf168f62", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/6df02f99998081032da3407a8d6c4e1dcb5d4379", + "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T14:11:46+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T18:47:49+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "2c408a6bb0313e6001a83628dc5506100474254e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e", + "reference": "2c408a6bb0313e6001a83628dc5506100474254e", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:50:15+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.36.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.36.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a", + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b", + "reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "114ac57257d75df748eda23dd003878080b8e688" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688", + "reference": "114ac57257d75df748eda23dd003878080b8e688", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "33600f8489485425bfcddd0d983391038d3422e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/33600f8489485425bfcddd0d983391038d3422e7", + "reference": "33600f8489485425bfcddd0d983391038d3422e7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/6883ebdf7bf6a12b37519dbc0df62b0222401b56", + "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:44:50+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "d870a33f0f79d2b4579740b0620200221ee44aeb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/d870a33f0f79d2b4579740b0620200221ee44aeb", + "reference": "d870a33f0f79d2b4579740b0620200221ee44aeb", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-16T23:10:39+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-02-09T13:44:54+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95.1", + "illuminate/view": "^12.56.0", + "larastan/larastan": "^3.9.6", + "laravel-zero/framework": "^12.1.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.3" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-04-20T15:26:14+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.57.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "fa8d057b6e9310380ccbc3a209ed7f927d54f648" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/fa8d057b6e9310380ccbc3a209ed7f927d54f648", + "reference": "fa8d057b6e9310380ccbc3a209ed7f927d54f648", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2026-04-14T13:32:04+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.3", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "b0d8ab95b29c3189aeeb902d81215231df4c1b64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/b0d8ab95b29c3189aeeb902d81215231df4c1b64", + "reference": "b0d8ab95b29c3189aeeb902d81215231df4c1b64", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.4" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.3", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.2.0", + "laravel/pint": "^1.29.0", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.0.0", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-04-06T19:25:53+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/Website/s_gizi/config/app.php b/Website/s_gizi/config/app.php new file mode 100644 index 0000000..423eed5 --- /dev/null +++ b/Website/s_gizi/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/Website/s_gizi/config/auth.php b/Website/s_gizi/config/auth.php new file mode 100644 index 0000000..d7568ff --- /dev/null +++ b/Website/s_gizi/config/auth.php @@ -0,0 +1,117 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/Website/s_gizi/config/cache.php b/Website/s_gizi/config/cache.php new file mode 100644 index 0000000..b32aead --- /dev/null +++ b/Website/s_gizi/config/cache.php @@ -0,0 +1,117 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/Website/s_gizi/config/database.php b/Website/s_gizi/config/database.php new file mode 100644 index 0000000..64709ce --- /dev/null +++ b/Website/s_gizi/config/database.php @@ -0,0 +1,184 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/Website/s_gizi/config/filesystems.php b/Website/s_gizi/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/Website/s_gizi/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/Website/s_gizi/config/logging.php b/Website/s_gizi/config/logging.php new file mode 100644 index 0000000..b09cb25 --- /dev/null +++ b/Website/s_gizi/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/Website/s_gizi/config/mail.php b/Website/s_gizi/config/mail.php new file mode 100644 index 0000000..e32e88d --- /dev/null +++ b/Website/s_gizi/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + ], + +]; diff --git a/Website/s_gizi/config/queue.php b/Website/s_gizi/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/Website/s_gizi/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/Website/s_gizi/config/services.php b/Website/s_gizi/config/services.php new file mode 100644 index 0000000..6a90eb8 --- /dev/null +++ b/Website/s_gizi/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/Website/s_gizi/config/session.php b/Website/s_gizi/config/session.php new file mode 100644 index 0000000..5b541b7 --- /dev/null +++ b/Website/s_gizi/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/Website/s_gizi/database/.gitignore b/Website/s_gizi/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/Website/s_gizi/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/Website/s_gizi/database/factories/UserFactory.php b/Website/s_gizi/database/factories/UserFactory.php new file mode 100644 index 0000000..c4ceb07 --- /dev/null +++ b/Website/s_gizi/database/factories/UserFactory.php @@ -0,0 +1,45 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/Website/s_gizi/database/migrations/0001_01_01_000000_create_users_table.php b/Website/s_gizi/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..d5a639b --- /dev/null +++ b/Website/s_gizi/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,54 @@ +id(); + $table->string('phone')->unique(); + $table->string('name')->nullable(); + $table->string('email')->nullable()->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password')->nullable(); + $table->string('role')->default('orang_tua'); + $table->string('otp_code')->nullable(); + $table->dateTime('otp_expires_at')->nullable(); + $table->rememberToken(); + $table->string('api_token', 100)->nullable()->unique(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/Website/s_gizi/database/migrations/0001_01_01_000001_create_cache_table.php b/Website/s_gizi/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..ed758bd --- /dev/null +++ b/Website/s_gizi/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/Website/s_gizi/database/migrations/0001_01_01_000002_create_jobs_table.php b/Website/s_gizi/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/Website/s_gizi/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/Website/s_gizi/database/migrations/2026_04_21_000001_create_children_table.php b/Website/s_gizi/database/migrations/2026_04_21_000001_create_children_table.php new file mode 100644 index 0000000..bf6305d --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_04_21_000001_create_children_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('nama'); + $table->date('tanggal_lahir'); + $table->enum('jenis_kelamin', ['L', 'P']); // L= laki-laki, P= perempuan + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('children'); + } +}; + diff --git a/Website/s_gizi/database/migrations/2026_04_21_000002_create_measurements_table.php b/Website/s_gizi/database/migrations/2026_04_21_000002_create_measurements_table.php new file mode 100644 index 0000000..8c6bc8c --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_04_21_000002_create_measurements_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('child_id')->constrained('children')->cascadeOnDelete(); + + $table->double('berat'); // kg + $table->double('tinggi'); // cm + $table->date('tanggal_ukur'); + + $table->double('umur_bulan'); // desimal + + $table->double('z_bbu')->nullable(); + $table->double('z_tbu')->nullable(); + $table->double('z_bbtb')->nullable(); + + $table->string('status_gabungan'); + $table->timestamp('created_at')->useCurrent(); + }); + } + + public function down(): void + { + Schema::dropIfExists('measurements'); + } +}; + diff --git a/Website/s_gizi/database/migrations/2026_04_21_000003_create_foods_table.php b/Website/s_gizi/database/migrations/2026_04_21_000003_create_foods_table.php new file mode 100644 index 0000000..47ff244 --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_04_21_000003_create_foods_table.php @@ -0,0 +1,24 @@ +id(); + $table->string('nama'); + $table->string('kategori'); // protein, karbo, vitamin, dll + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('foods'); + } +}; + diff --git a/Website/s_gizi/database/migrations/2026_04_21_000004_create_food_conditions_table.php b/Website/s_gizi/database/migrations/2026_04_21_000004_create_food_conditions_table.php new file mode 100644 index 0000000..38906c5 --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_04_21_000004_create_food_conditions_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('food_id')->constrained('foods')->cascadeOnDelete(); + $table->string('status_gizi'); // contoh: "Stunting tanpa Wasting" + $table->timestamps(); + + $table->unique(['food_id', 'status_gizi']); + $table->index('status_gizi'); + }); + } + + public function down(): void + { + Schema::dropIfExists('food_conditions'); + } +}; + diff --git a/Website/s_gizi/database/migrations/2026_04_21_000005_create_who_lms_tables.php b/Website/s_gizi/database/migrations/2026_04_21_000005_create_who_lms_tables.php new file mode 100644 index 0000000..7d4c04a --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_04_21_000005_create_who_lms_tables.php @@ -0,0 +1,43 @@ +id(); + $table->integer('umur'); + $table->enum('jk', ['L', 'P']); + $table->enum('indikator', ['bbu', 'tbu']); + $table->double('L'); + $table->double('M'); + $table->double('S'); + + $table->unique(['umur', 'jk', 'indikator'], 'who_lms_age_umur_jk_indikator_unique'); + $table->index(['jk', 'indikator'], 'who_lms_age_jk_indikator_index'); + }); + + Schema::create('who_lms_wfh', function (Blueprint $table) { + $table->id(); + $table->double('tinggi'); + $table->enum('jk', ['L', 'P']); + $table->double('L'); + $table->double('M'); + $table->double('S'); + + $table->unique(['tinggi', 'jk'], 'who_lms_wfh_tinggi_jk_unique'); + $table->index('jk', 'who_lms_wfh_jk_index'); + }); + } + + public function down(): void + { + Schema::dropIfExists('who_lms_wfh'); + Schema::dropIfExists('who_lms_age'); + } +}; + diff --git a/Website/s_gizi/database/migrations/2026_04_21_000006_create_who_lms_wfl_table.php b/Website/s_gizi/database/migrations/2026_04_21_000006_create_who_lms_wfl_table.php new file mode 100644 index 0000000..e3a15a5 --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_04_21_000006_create_who_lms_wfl_table.php @@ -0,0 +1,29 @@ +id(); + $table->double('panjang'); + $table->enum('jk', ['L', 'P']); + $table->double('L'); + $table->double('M'); + $table->double('S'); + + $table->unique(['panjang', 'jk'], 'who_lms_wfl_panjang_jk_unique'); + $table->index('jk', 'who_lms_wfl_jk_index'); + }); + } + + public function down(): void + { + Schema::dropIfExists('who_lms_wfl'); + } +}; + diff --git a/Website/s_gizi/database/migrations/2026_04_21_000007_create_makanan_table.php b/Website/s_gizi/database/migrations/2026_04_21_000007_create_makanan_table.php new file mode 100644 index 0000000..8bc0747 --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_04_21_000007_create_makanan_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('nama'); + $table->integer('usia_min'); + $table->integer('usia_max'); + $table->enum('kategori_status', ['Stunting', 'Wasting', 'Underweight', 'Obesitas', 'Normal']); + $table->integer('kalori'); + $table->integer('protein'); + $table->integer('lemak'); + $table->integer('karbohidrat'); + $table->text('alasan'); + $table->timestamps(); + + $table->index(['kategori_status', 'usia_min', 'usia_max'], 'makanan_kat_usia_index'); + }); + } + + public function down(): void + { + Schema::dropIfExists('makanan'); + } +}; + diff --git a/Website/s_gizi/database/migrations/2026_04_25_000008_create_hasil_gizi_table.php b/Website/s_gizi/database/migrations/2026_04_25_000008_create_hasil_gizi_table.php new file mode 100644 index 0000000..729d1ed --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_04_25_000008_create_hasil_gizi_table.php @@ -0,0 +1,28 @@ +id(); + $table->double('berat'); + $table->double('tinggi'); + $table->double('umur'); + $table->double('z_bbu'); + $table->double('z_tbu'); + $table->double('z_bbtb'); + $table->enum('status_gabungan', ['Stunting', 'Wasting', 'Underweight', 'Obesitas', 'Normal']); + $table->timestamp('created_at')->useCurrent(); + }); + } + + public function down(): void + { + Schema::dropIfExists('hasil_gizi'); + } +}; diff --git a/Website/s_gizi/database/migrations/2026_04_25_000009_drop_food_mapping_tables.php b/Website/s_gizi/database/migrations/2026_04_25_000009_drop_food_mapping_tables.php new file mode 100644 index 0000000..3e5871e --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_04_25_000009_drop_food_mapping_tables.php @@ -0,0 +1,18 @@ +string('phone')->nullable()->unique()->after('email'); + } + if (!Schema::hasColumn('users', 'otp_code')) { + $table->string('otp_code')->nullable()->after('phone'); + } + if (!Schema::hasColumn('users', 'otp_expires_at')) { + $table->timestamp('otp_expires_at')->nullable()->after('otp_code'); + } + if (!Schema::hasColumn('users', 'api_token')) { + $table->string('api_token', 100)->nullable()->unique()->after('remember_token'); + } + }); + + Schema::table('children', function (Blueprint $table) { + if (!Schema::hasColumn('children', 'user_id')) { + $table->foreignId('user_id')->nullable()->after('id')->constrained('users')->nullOnDelete(); + } + }); + + Schema::table('measurements', function (Blueprint $table) { + if (!Schema::hasColumn('measurements', 'cara_ukur')) { + $table->string('cara_ukur')->default('standing')->after('tanggal_ukur'); + } + if (!Schema::hasColumn('measurements', 'kategori')) { + $table->json('kategori')->nullable()->after('z_bbtb'); + } + }); + } + + public function down(): void + { + Schema::table('measurements', function (Blueprint $table) { + if (Schema::hasColumn('measurements', 'kategori')) { + $table->dropColumn('kategori'); + } + if (Schema::hasColumn('measurements', 'cara_ukur')) { + $table->dropColumn('cara_ukur'); + } + }); + + Schema::table('children', function (Blueprint $table) { + if (Schema::hasColumn('children', 'user_id')) { + $table->dropConstrainedForeignId('user_id'); + } + }); + + Schema::table('users', function (Blueprint $table) { + foreach (['api_token', 'otp_expires_at', 'otp_code', 'phone'] as $column) { + if (Schema::hasColumn('users', $column)) { + $table->dropColumn($column); + } + } + }); + } +}; diff --git a/Website/s_gizi/database/migrations/2026_05_03_000011_create_articles_table.php b/Website/s_gizi/database/migrations/2026_05_03_000011_create_articles_table.php new file mode 100644 index 0000000..e93d42b --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_05_03_000011_create_articles_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('title'); + $table->string('category')->default('Edukasi Gizi'); + $table->text('excerpt')->nullable(); + $table->longText('content'); + $table->boolean('published')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('articles'); + } +}; diff --git a/Website/s_gizi/database/migrations/2026_05_03_000012_align_ta_schema_roles_and_growth_records.php b/Website/s_gizi/database/migrations/2026_05_03_000012_align_ta_schema_roles_and_growth_records.php new file mode 100644 index 0000000..138689f --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_05_03_000012_align_ta_schema_roles_and_growth_records.php @@ -0,0 +1,59 @@ +string('role')->default('orang_tua')->after('phone'); + } + }); + + Schema::table('children', function (Blueprint $table) { + if (!Schema::hasColumn('children', 'nama_anak')) { + $table->string('nama_anak')->nullable()->after('user_id'); + } + }); + + DB::table('children') + ->whereNull('nama_anak') + ->update(['nama_anak' => DB::raw('nama')]); + + Schema::create('growth_records', function (Blueprint $table) { + $table->id(); + $table->foreignId('child_id')->constrained('children')->cascadeOnDelete(); + $table->double('tinggi_badan'); + $table->double('berat_badan'); + $table->double('umur_dalam_bulan'); + $table->double('z_score'); + $table->string('status_gizi'); + $table->timestamp('created_at')->useCurrent(); + + $table->index(['child_id', 'created_at']); + $table->index('status_gizi'); + }); + } + + public function down(): void + { + Schema::dropIfExists('growth_records'); + + Schema::table('children', function (Blueprint $table) { + if (Schema::hasColumn('children', 'nama_anak')) { + $table->dropColumn('nama_anak'); + } + }); + + Schema::table('users', function (Blueprint $table) { + if (Schema::hasColumn('users', 'role')) { + $table->dropColumn('role'); + } + }); + } +}; diff --git a/Website/s_gizi/database/migrations/2026_05_03_000013_make_user_login_fields_nullable.php b/Website/s_gizi/database/migrations/2026_05_03_000013_make_user_login_fields_nullable.php new file mode 100644 index 0000000..3e8e3e9 --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_05_03_000013_make_user_login_fields_nullable.php @@ -0,0 +1,36 @@ +whereNull('phone') + ->select('id') + ->orderBy('id') + ->get() + ->each(fn ($user) => DB::table('users') + ->where('id', $user->id) + ->update(['phone' => 'legacy-'.$user->id])); + + Schema::table('users', function (Blueprint $table) { + $table->string('name')->nullable()->change(); + $table->string('email')->nullable()->change(); + $table->string('password')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('name')->nullable(false)->change(); + $table->string('email')->nullable(false)->change(); + $table->string('password')->nullable(false)->change(); + }); + } +}; diff --git a/Website/s_gizi/database/migrations/2026_05_09_000014_create_consultation_rooms_and_messages_table.php b/Website/s_gizi/database/migrations/2026_05_09_000014_create_consultation_rooms_and_messages_table.php new file mode 100644 index 0000000..f9d0171 --- /dev/null +++ b/Website/s_gizi/database/migrations/2026_05_09_000014_create_consultation_rooms_and_messages_table.php @@ -0,0 +1,48 @@ +id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('child_id')->constrained('children')->cascadeOnDelete(); + $table->string('expert_id', 80); + $table->string('expert_name', 150); + $table->string('specialization', 150)->nullable(); + $table->string('asset_image')->nullable(); + $table->boolean('online')->default(false); + $table->string('status', 32)->default('aktif'); + $table->text('last_message')->nullable(); + $table->timestamp('last_message_at')->nullable(); + $table->unsignedInteger('unread_count')->default(0); + $table->timestamps(); + $table->unique(['user_id', 'child_id', 'expert_id']); + }); + } + + if (!Schema::hasTable('consultation_messages')) { + Schema::create('consultation_messages', function (Blueprint $table) { + $table->id(); + $table->foreignId('room_id')->constrained('consultation_rooms')->cascadeOnDelete(); + $table->string('sender_type', 32)->default('parent'); + $table->text('message'); + $table->boolean('is_read')->default(false); + $table->timestamps(); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('consultation_messages'); + Schema::dropIfExists('consultation_rooms'); + } +}; + diff --git a/Website/s_gizi/database/seeders/DatabaseSeeder.php b/Website/s_gizi/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..1c0c206 --- /dev/null +++ b/Website/s_gizi/database/seeders/DatabaseSeeder.php @@ -0,0 +1,16 @@ +call([ + LMSSeeder::class, + MakananSeeder::class, + ]); + } +} diff --git a/Website/s_gizi/database/seeders/LMSSeeder.php b/Website/s_gizi/database/seeders/LMSSeeder.php new file mode 100644 index 0000000..de495ce --- /dev/null +++ b/Website/s_gizi/database/seeders/LMSSeeder.php @@ -0,0 +1,94 @@ + 'L', 'female' => 'P']; + + $ageRows = []; + $wflRows = []; + $wfhRows = []; + + foreach ($mapSex as $sexKey => $jk) { + $sexData = $decoded[$sexKey] ?? null; + if (!is_array($sexData)) continue; + + // wfa -> bbu, hfa -> tbu + foreach (($sexData['wfa'] ?? []) as $umur => $lms) { + if (!is_array($lms)) continue; + $ageRows[] = [ + 'umur' => (int) $umur, + 'jk' => $jk, + 'indikator' => 'bbu', + 'L' => (float) ($lms['L'] ?? 0), + 'M' => (float) ($lms['M'] ?? 0), + 'S' => (float) ($lms['S'] ?? 0), + ]; + } + foreach (($sexData['hfa'] ?? []) as $umur => $lms) { + if (!is_array($lms)) continue; + $ageRows[] = [ + 'umur' => (int) $umur, + 'jk' => $jk, + 'indikator' => 'tbu', + 'L' => (float) ($lms['L'] ?? 0), + 'M' => (float) ($lms['M'] ?? 0), + 'S' => (float) ($lms['S'] ?? 0), + ]; + } + + foreach (($sexData['wfl'] ?? []) as $panjang => $lms) { + if (!is_array($lms)) continue; + $wflRows[] = [ + 'panjang' => (float) $panjang, + 'jk' => $jk, + 'L' => (float) ($lms['L'] ?? 0), + 'M' => (float) ($lms['M'] ?? 0), + 'S' => (float) ($lms['S'] ?? 0), + ]; + } + + foreach (($sexData['wfh'] ?? []) as $tinggi => $lms) { + if (!is_array($lms)) continue; + $wfhRows[] = [ + 'tinggi' => (float) $tinggi, + 'jk' => $jk, + 'L' => (float) ($lms['L'] ?? 0), + 'M' => (float) ($lms['M'] ?? 0), + 'S' => (float) ($lms['S'] ?? 0), + ]; + } + } + + DB::table('who_lms_age')->truncate(); + DB::table('who_lms_wfl')->truncate(); + DB::table('who_lms_wfh')->truncate(); + + foreach (array_chunk($ageRows, 1000) as $chunk) { + DB::table('who_lms_age')->insert($chunk); + } + foreach (array_chunk($wflRows, 1000) as $chunk) { + DB::table('who_lms_wfl')->insert($chunk); + } + foreach (array_chunk($wfhRows, 1000) as $chunk) { + DB::table('who_lms_wfh')->insert($chunk); + } + } +} + diff --git a/Website/s_gizi/database/seeders/MakananSeeder.php b/Website/s_gizi/database/seeders/MakananSeeder.php new file mode 100644 index 0000000..1b0e554 --- /dev/null +++ b/Website/s_gizi/database/seeders/MakananSeeder.php @@ -0,0 +1,81 @@ + 'Nasi + Ayam + Sayur', + 'usia_min' => 6, + 'usia_max' => 60, + 'kategori_status' => 'Stunting', + 'kalori' => 250, + 'protein' => 15, + 'lemak' => 8, + 'karbohidrat' => 30, + 'alasan' => 'Tinggi protein untuk mendukung pertumbuhan', + ], + [ + 'nama' => 'Bubur kacang hijau + telur', + 'usia_min' => 6, + 'usia_max' => 60, + 'kategori_status' => 'Wasting', + 'kalori' => 300, + 'protein' => 14, + 'lemak' => 10, + 'karbohidrat' => 38, + 'alasan' => 'Padat energi dan protein untuk mengejar berat badan', + ], + [ + 'nama' => 'Nasi + ikan + tempe + sayur', + 'usia_min' => 12, + 'usia_max' => 60, + 'kategori_status' => 'Underweight', + 'kalori' => 280, + 'protein' => 16, + 'lemak' => 9, + 'karbohidrat' => 34, + 'alasan' => 'Meningkatkan asupan energi dan protein harian', + ], + [ + 'nama' => 'Nasi merah + ayam kukus + sayur', + 'usia_min' => 12, + 'usia_max' => 60, + 'kategori_status' => 'Obesitas', + 'kalori' => 220, + 'protein' => 18, + 'lemak' => 6, + 'karbohidrat' => 24, + 'alasan' => 'Menu seimbang dengan kontrol kalori dan lemak', + ], + [ + 'nama' => 'Menu seimbang (karbo + protein + sayur)', + 'usia_min' => 6, + 'usia_max' => 60, + 'kategori_status' => 'Normal', + 'kalori' => 240, + 'protein' => 12, + 'lemak' => 7, + 'karbohidrat' => 32, + 'alasan' => 'Menjaga status gizi tetap baik sesuai kebutuhan usia', + ], + ]; + + foreach ($rows as $r) { + Makanan::query()->updateOrCreate( + [ + 'nama' => $r['nama'], + 'kategori_status' => $r['kategori_status'], + ], + $r + ); + } + } +} + diff --git a/Website/s_gizi/package.json b/Website/s_gizi/package.json new file mode 100644 index 0000000..7686b29 --- /dev/null +++ b/Website/s_gizi/package.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.7" + } +} diff --git a/Website/s_gizi/phpunit.xml b/Website/s_gizi/phpunit.xml new file mode 100644 index 0000000..e7f0a48 --- /dev/null +++ b/Website/s_gizi/phpunit.xml @@ -0,0 +1,36 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + diff --git a/Website/s_gizi/public/.htaccess b/Website/s_gizi/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/Website/s_gizi/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/Website/s_gizi/public/favicon.ico b/Website/s_gizi/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/Website/s_gizi/public/index.php b/Website/s_gizi/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/Website/s_gizi/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/Website/s_gizi/public/robots.txt b/Website/s_gizi/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/Website/s_gizi/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/Website/s_gizi/resources/css/app.css b/Website/s_gizi/resources/css/app.css new file mode 100644 index 0000000..3e6abea --- /dev/null +++ b/Website/s_gizi/resources/css/app.css @@ -0,0 +1,11 @@ +@import 'tailwindcss'; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; +@source '../**/*.blade.php'; +@source '../**/*.js'; + +@theme { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; +} diff --git a/Website/s_gizi/resources/js/app.js b/Website/s_gizi/resources/js/app.js new file mode 100644 index 0000000..e59d6a0 --- /dev/null +++ b/Website/s_gizi/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/Website/s_gizi/resources/js/bootstrap.js b/Website/s_gizi/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/Website/s_gizi/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/Website/s_gizi/resources/views/admin/articles/index.blade.php b/Website/s_gizi/resources/views/admin/articles/index.blade.php new file mode 100644 index 0000000..d1b140a --- /dev/null +++ b/Website/s_gizi/resources/views/admin/articles/index.blade.php @@ -0,0 +1,44 @@ + +
+
+

Artikel Edukasi

+
Kelola konten edukasi untuk dashboard mobile.
+
+
+ +
+
+
+ @csrf +
+
+
+
+
+
+
+
+
+ +
+ @foreach ($articles as $article) +
+
+
+
+ {{ $article->category }} + {{ $article->published ? 'Published' : 'Draft' }} +
+
{{ $article->title }}
+

{{ $article->excerpt }}

+
+ @csrf @method('delete') + +
+
+
+
+ @endforeach +
+
{{ $articles->links('pagination::bootstrap-5') }}
+
diff --git a/Website/s_gizi/resources/views/admin/children/create.blade.php b/Website/s_gizi/resources/views/admin/children/create.blade.php new file mode 100644 index 0000000..afcdea0 --- /dev/null +++ b/Website/s_gizi/resources/views/admin/children/create.blade.php @@ -0,0 +1,35 @@ + +
+

Tambah Data Anak

+
Data ini diperlukan untuk perhitungan umur berdasarkan tanggal lahir.
+
+ +
+
+
+ @csrf +
+ + +
+
+ + +
+
+ + +
+
+ + Kembali +
+
+
+
+
+ diff --git a/Website/s_gizi/resources/views/admin/children/edit.blade.php b/Website/s_gizi/resources/views/admin/children/edit.blade.php new file mode 100644 index 0000000..00c14d9 --- /dev/null +++ b/Website/s_gizi/resources/views/admin/children/edit.blade.php @@ -0,0 +1,35 @@ + +
+

Edit Data Anak

+
Perbarui data balita.
+
+ +
+
+
+ @csrf + @method('PUT') +
+ + +
+
+ + +
+
+ + +
+
+ + Kembali +
+
+
+
+
+ diff --git a/Website/s_gizi/resources/views/admin/children/index.blade.php b/Website/s_gizi/resources/views/admin/children/index.blade.php new file mode 100644 index 0000000..257c7bf --- /dev/null +++ b/Website/s_gizi/resources/views/admin/children/index.blade.php @@ -0,0 +1,58 @@ + +
+
+

Data Anak

+
Data balita yang dipakai untuk perhitungan umur (bulan).
+
+ Tambah +
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + @forelse ($children as $c) + + + + + + + + @empty + + @endforelse + +
#NamaTanggal LahirJKAksi
{{ $c->id }}{{ $c->nama }}{{ $c->tanggal_lahir?->format('Y-m-d') }}{{ $c->jenis_kelamin }} + Edit +
+ @csrf + @method('DELETE') + +
+
Belum ada data.
+
+
+ {{ $children->links('pagination::bootstrap-5') }} +
+
+
+ diff --git a/Website/s_gizi/resources/views/admin/dashboard.blade.php b/Website/s_gizi/resources/views/admin/dashboard.blade.php new file mode 100644 index 0000000..7eb4abe --- /dev/null +++ b/Website/s_gizi/resources/views/admin/dashboard.blade.php @@ -0,0 +1,64 @@ + +
+
+

Dashboard S-Gizi

+
Ringkasan pengguna, anak, pengukuran, dan distribusi status gizi
+
+
+ +
+
+
+
+
Total Users
+
{{ $countUsers }}
+
+
+
+
+
+
+
Total Children
+
{{ $countChildren }}
+
+
+
+
+
+
+
Riwayat Gizi
+
{{ $countMeasurements }}
+
+
+
+
+
+
+
Menu Makanan
+
{{ $countFoods }}
+
+
+
+
+ +
+
+
Status Distribution
+ @forelse ($statusDistribution as $row) + @php($percent = $countMeasurements > 0 ? round(($row->total / $countMeasurements) * 100) : 0) +
+
+ {{ $row->status_gabungan }} + {{ $row->total }} data +
+
+
+
+
+ @empty +
Belum ada data pengukuran.
+ @endforelse +
+
+
+ diff --git a/Website/s_gizi/resources/views/admin/food_conditions/index.blade.php b/Website/s_gizi/resources/views/admin/food_conditions/index.blade.php new file mode 100644 index 0000000..aad5c8d --- /dev/null +++ b/Website/s_gizi/resources/views/admin/food_conditions/index.blade.php @@ -0,0 +1,79 @@ + +
+

Mapping Status Gizi

+
Hubungkan makanan ke status gabungan (dipakai API untuk rekomendasi).
+
+ +
+
+
+ @csrf +
+ + +
+
+ + +
Harus sama persis dengan output `status_gabungan` API.
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + @forelse ($mappings as $m) + + + + + + + + @empty + + @endforelse + +
#StatusMakananKategoriAksi
{{ $m->id }}{{ $m->status_gizi }}{{ $m->food?->nama }}{{ $m->food?->kategori }} +
+ @csrf + @method('DELETE') + +
+
Belum ada mapping.
+
+
+ {{ $mappings->links('pagination::bootstrap-5') }} +
+
+
+ diff --git a/Website/s_gizi/resources/views/admin/foods/create.blade.php b/Website/s_gizi/resources/views/admin/foods/create.blade.php new file mode 100644 index 0000000..10af424 --- /dev/null +++ b/Website/s_gizi/resources/views/admin/foods/create.blade.php @@ -0,0 +1,28 @@ + +
+

Tambah Makanan

+
Isi nama dan kategori (protein, karbo, vitamin, dll).
+
+ +
+
+
+ @csrf +
+ + +
+
+ + +
Untuk snack, gunakan kategori vitamin (huruf kecil) agar dipilih otomatis.
+
+
+ + Kembali +
+
+
+
+
+ diff --git a/Website/s_gizi/resources/views/admin/foods/edit.blade.php b/Website/s_gizi/resources/views/admin/foods/edit.blade.php new file mode 100644 index 0000000..e979e1e --- /dev/null +++ b/Website/s_gizi/resources/views/admin/foods/edit.blade.php @@ -0,0 +1,28 @@ + +
+

Edit Makanan

+
Perbarui data makanan.
+
+ +
+
+
+ @csrf + @method('PUT') +
+ + +
+
+ + +
+
+ + Kembali +
+
+
+
+
+ diff --git a/Website/s_gizi/resources/views/admin/foods/index.blade.php b/Website/s_gizi/resources/views/admin/foods/index.blade.php new file mode 100644 index 0000000..f2b9553 --- /dev/null +++ b/Website/s_gizi/resources/views/admin/foods/index.blade.php @@ -0,0 +1,56 @@ + +
+
+

Manajemen Makanan

+
Kelola data makanan yang akan dipakai rekomendasi.
+
+ Tambah +
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + @forelse ($foods as $food) + + + + + + + @empty + + @endforelse + +
#NamaKategoriAksi
{{ $food->id }}{{ $food->nama }}{{ $food->kategori }} + Edit +
+ @csrf + @method('DELETE') + +
+
Belum ada data.
+
+
+ {{ $foods->links('pagination::bootstrap-5') }} +
+
+
+ diff --git a/Website/s_gizi/resources/views/admin/layout.blade.php b/Website/s_gizi/resources/views/admin/layout.blade.php new file mode 100644 index 0000000..589e7d1 --- /dev/null +++ b/Website/s_gizi/resources/views/admin/layout.blade.php @@ -0,0 +1,48 @@ + + + + + + {{ $title ?? 'Admin Gizi Balita' }} + + + + + +
+ @if (session('success')) +
{{ session('success') }}
+ @endif + @if ($errors->any()) +
+
Validasi gagal
+
    + @foreach ($errors->all() as $e) +
  • {{ $e }}
  • + @endforeach +
+
+ @endif + + {{ $slot }} +
+ + + + + diff --git a/Website/s_gizi/resources/views/admin/lms/index.blade.php b/Website/s_gizi/resources/views/admin/lms/index.blade.php new file mode 100644 index 0000000..35b0323 --- /dev/null +++ b/Website/s_gizi/resources/views/admin/lms/index.blade.php @@ -0,0 +1,63 @@ + +
+
+

LMS Data Management

+
Kelola who_lms_age, who_lms_wfh, dan who_lms_wfl.
+
+
+ Age + WFH + WFL +
+
+ +
+
+
+ @csrf + + @if ($type === 'age') +
+
+ @else +
+ @endif +
+
+
+
+
+
+
+
+ +
+
+ + + + + @if ($type === 'age') @else @endif + + + + + @foreach ($rows as $row) + + + @if ($type === 'age') @else @endif + + + + @endforeach + +
IDUmurIndikator{{ $type === 'wfl' ? 'Panjang' : 'Tinggi' }}JKLMS
{{ $row->id }}{{ $row->umur }}{{ $row->indikator }}{{ $type === 'wfl' ? $row->panjang : $row->tinggi }}{{ $row->jk }}{{ $row->L }}{{ $row->M }}{{ $row->S }} +
+ @csrf @method('delete') + +
+
+
+
{{ $rows->links('pagination::bootstrap-5') }}
+
+
diff --git a/Website/s_gizi/resources/views/admin/measurements/index.blade.php b/Website/s_gizi/resources/views/admin/measurements/index.blade.php new file mode 100644 index 0000000..533c9f2 --- /dev/null +++ b/Website/s_gizi/resources/views/admin/measurements/index.blade.php @@ -0,0 +1,51 @@ + +
+
+

Riwayat Pengukuran

+
Daftar pengukuran (urut terbaru).
+
+ Export CSV +
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + @forelse ($measurements as $m) + + + + + + + + @empty + + @endforelse + +
Nama AnakTanggalBerat (kg)Tinggi (cm)Status
{{ $m->child?->nama }}{{ $m->tanggal_ukur?->format('Y-m-d') }}{{ number_format((float) $m->berat, 2) }}{{ number_format((float) $m->tinggi, 2) }}{{ $m->status_gabungan }}
Belum ada data.
+
+
+ {{ $measurements->links('pagination::bootstrap-5') }} +
+
+
+ diff --git a/Website/s_gizi/resources/views/components/admin-layout.blade.php b/Website/s_gizi/resources/views/components/admin-layout.blade.php new file mode 100644 index 0000000..54ce485 --- /dev/null +++ b/Website/s_gizi/resources/views/components/admin-layout.blade.php @@ -0,0 +1,61 @@ +@props(['title' => 'Admin Gizi Balita']) + + + + + + + {{ $title }} + + + + + + +
+ @if (session('success')) +
{{ session('success') }}
+ @endif + @if ($errors->any()) +
+
Validasi gagal
+
    + @foreach ($errors->all() as $e) +
  • {{ $e }}
  • + @endforeach +
+
+ @endif + + {{ $slot }} +
+ + + + + diff --git a/Website/s_gizi/resources/views/welcome.blade.php b/Website/s_gizi/resources/views/welcome.blade.php new file mode 100644 index 0000000..b7355d7 --- /dev/null +++ b/Website/s_gizi/resources/views/welcome.blade.php @@ -0,0 +1,277 @@ + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + +
+ @if (Route::has('login')) + + @endif +
+
+
+
+

Let's get started

+

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

+ + +
+
+ {{-- Laravel Logo --}} + + + + + + + + + + + {{-- Light Mode 12 SVG --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{-- Dark Mode 12 SVG --}} + +
+
+
+
+ + @if (Route::has('login')) + + @endif + + diff --git a/Website/s_gizi/routes/api.php b/Website/s_gizi/routes/api.php new file mode 100644 index 0000000..a18fbdd --- /dev/null +++ b/Website/s_gizi/routes/api.php @@ -0,0 +1,34 @@ +middleware('throttle:5,1'); +Route::post('/verify-otp', [AuthController::class, 'verifyOtp'])->middleware('throttle:10,1'); +Route::post('/auth/send-otp', [AuthController::class, 'sendOtp'])->middleware('throttle:5,1'); +Route::post('/auth/verify-otp', [AuthController::class, 'verifyOtp'])->middleware('throttle:10,1'); +Route::get('/profile', [ProfileController::class, 'show']); +Route::put('/profile', [ProfileController::class, 'update']); +Route::apiResource('/children', ChildController::class)->except(['show']); +Route::post('/gizi/hitung', [GiziController::class, 'hitung']); +Route::get('/gizi/riwayat/{childId}', [GiziController::class, 'riwayat']); +Route::get('/riwayat/{childId}', [GiziController::class, 'riwayat']); +Route::get('/rekomendasi/{status}', [RecommendationController::class, 'show']); +Route::get('/news', [NewsController::class, 'index']); +Route::get('/articles', [ArticleController::class, 'index']); +Route::get('/consultation/rooms', [ConsultationController::class, 'rooms']); +Route::post('/consultation/rooms', [ConsultationController::class, 'openRoom']); +Route::get('/consultation/rooms/{roomId}/messages', [ConsultationController::class, 'messages']); +Route::post('/consultation/rooms/{roomId}/messages', [ConsultationController::class, 'sendMessage']); +Route::post('/consultation/rooms/{roomId}/expert-reply', [ConsultationController::class, 'sendExpertReply']); +Route::patch('/consultation/rooms/{roomId}/status', [ConsultationController::class, 'updateStatus']); + diff --git a/Website/s_gizi/routes/console.php b/Website/s_gizi/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/Website/s_gizi/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/Website/s_gizi/routes/web.php b/Website/s_gizi/routes/web.php new file mode 100644 index 0000000..2962e55 --- /dev/null +++ b/Website/s_gizi/routes/web.php @@ -0,0 +1,32 @@ + redirect()->route('admin.dashboard')); + +Route::prefix('admin')->name('admin.')->group(function () { + Route::get('/', fn () => redirect()->route('admin.dashboard')); + Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); + + Route::resource('children', ChildController::class)->except(['show']); + Route::resource('foods', FoodController::class)->except(['show']); + Route::resource('articles', ArticleController::class)->only(['index', 'store', 'update', 'destroy']); + Route::get('/lms', [LmsDataController::class, 'index'])->name('lms.index'); + Route::post('/lms', [LmsDataController::class, 'store'])->name('lms.store'); + Route::put('/lms/{type}/{id}', [LmsDataController::class, 'update'])->name('lms.update'); + Route::delete('/lms/{type}/{id}', [LmsDataController::class, 'destroy'])->name('lms.destroy'); + + Route::get('/food-conditions', [FoodConditionController::class, 'index'])->name('food_conditions.index'); + Route::post('/food-conditions', [FoodConditionController::class, 'store'])->name('food_conditions.store'); + Route::delete('/food-conditions/{foodCondition}', [FoodConditionController::class, 'destroy'])->name('food_conditions.destroy'); + + Route::get('/measurements', [MeasurementController::class, 'index'])->name('measurements.index'); + Route::get('/measurements/export', [MeasurementController::class, 'export'])->name('measurements.export'); +}); diff --git a/Website/s_gizi/storage/app/.gitignore b/Website/s_gizi/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/Website/s_gizi/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/Website/s_gizi/storage/app/private/.gitignore b/Website/s_gizi/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/Website/s_gizi/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/Website/s_gizi/storage/app/public/.gitignore b/Website/s_gizi/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/Website/s_gizi/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/Website/s_gizi/storage/framework/.gitignore b/Website/s_gizi/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/Website/s_gizi/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/Website/s_gizi/storage/framework/cache/.gitignore b/Website/s_gizi/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/Website/s_gizi/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/Website/s_gizi/storage/framework/cache/data/.gitignore b/Website/s_gizi/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/Website/s_gizi/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/Website/s_gizi/storage/framework/sessions/.gitignore b/Website/s_gizi/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/Website/s_gizi/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/Website/s_gizi/storage/framework/testing/.gitignore b/Website/s_gizi/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/Website/s_gizi/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/Website/s_gizi/storage/framework/views/.gitignore b/Website/s_gizi/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/Website/s_gizi/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/Website/s_gizi/storage/logs/.gitignore b/Website/s_gizi/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/Website/s_gizi/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/Website/s_gizi/tests/Feature/ExampleTest.php b/Website/s_gizi/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8973927 --- /dev/null +++ b/Website/s_gizi/tests/Feature/ExampleTest.php @@ -0,0 +1,19 @@ +get('/'); + + $response->assertRedirect(route('admin.dashboard')); + } +} diff --git a/Website/s_gizi/tests/TestCase.php b/Website/s_gizi/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/Website/s_gizi/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/Website/s_gizi/vite.config.js b/Website/s_gizi/vite.config.js new file mode 100644 index 0000000..f35b4e7 --- /dev/null +++ b/Website/s_gizi/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + tailwindcss(), + ], + server: { + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, +});