commit 653cee119d4076406ea4ed6f294a9d01c3bbaa6a Author: Yanuar Tr <82995911+Greek-Cp@users.noreply.github.com> Date: Thu Jun 19 20:25:09 2025 +0700 feat: init project diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c5f9746 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c485074 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# forward_chaining_man_app + +A new Flutter project. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..55afd91 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..59db11b --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,47 @@ +plugins { + id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + // END: FlutterFire Configuration + 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.forward_chaining_man_app" + compileSdk = 34 + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.forward_chaining_man_app" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 23 + 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.debug + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..00dd5f9 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,114 @@ +{ + "project_info": { + "project_number": "1072369607324", + "project_id": "sirekomendasi-dc7de", + "storage_bucket": "sirekomendasi-dc7de.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1072369607324:android:f76a929d6f6258726f4359", + "android_client_info": { + "package_name": "com.e" + } + }, + "oauth_client": [ + { + "client_id": "1072369607324-pnvpbn64ceilqdub09qfj2h6re5199tj.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.e", + "certificate_hash": "54c882833621e66da646ffb2d8ee7237fa7f0a24" + } + }, + { + "client_id": "1072369607324-t2qd5p0r7lviqemnvjidshbud49es45l.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.e", + "certificate_hash": "3bb526cd960a2c584c325eb17221250f321e34d0" + } + }, + { + "client_id": "1072369607324-ul5feal98a8o8g9fo23ddp7ob886ermd.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBIHdD_rH3YSfuZYcNKFd1YCMTXxYb6gdc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1072369607324-ul5feal98a8o8g9fo23ddp7ob886ermd.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "1072369607324-0e0k7pn3nqk6suq97jnegljp3k0161o5.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.example.forwardChainingManApp" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1072369607324:android:ed7767d8870c32e96f4359", + "android_client_info": { + "package_name": "com.example.forward_chaining_man_app" + } + }, + "oauth_client": [ + { + "client_id": "1072369607324-hp2vujt95ipkft4n348a96ndbiocqrdo.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.forward_chaining_man_app", + "certificate_hash": "54c882833621e66da646ffb2d8ee7237fa7f0a24" + } + }, + { + "client_id": "1072369607324-pbgftae664k56gli4fb0ftrhot88t8qi.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.forward_chaining_man_app", + "certificate_hash": "3bb526cd960a2c584c325eb17221250f321e34d0" + } + }, + { + "client_id": "1072369607324-ul5feal98a8o8g9fo23ddp7ob886ermd.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBIHdD_rH3YSfuZYcNKFd1YCMTXxYb6gdc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "1072369607324-ul5feal98a8o8g9fo23ddp7ob886ermd.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "1072369607324-0e0k7pn3nqk6suq97jnegljp3k0161o5.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.example.forwardChainingManApp" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f4096a9 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/forward_chaining_man_app/MainActivity.kt b/android/app/src/main/kotlin/com/example/forward_chaining_man_app/MainActivity.kt new file mode 100644 index 0000000..8315b35 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/forward_chaining_man_app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.forward_chaining_man_app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..59c534f Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..6406493 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..50d5e19 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..6a7c44f Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b14eab7 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..d2ffbff --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..2597170 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8bc9958 --- /dev/null +++ b/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.0-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..11a106e --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,28 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return 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.1.0" apply false + // START: FlutterFire Configuration + id "com.google.gms.google-services" version "4.3.15" apply false + // END: FlutterFire Configuration + id "org.jetbrains.kotlin.android" version "1.9.24" apply false +} + +include ":app" diff --git a/assets/biaya_kuliah_d4_ugm.json b/assets/biaya_kuliah_d4_ugm.json new file mode 100644 index 0000000..ed7e694 --- /dev/null +++ b/assets/biaya_kuliah_d4_ugm.json @@ -0,0 +1,279 @@ + +[ + { + "NO": "", + "PROGRAM": "", + "NAMA PROGRAM STUDI": "", + "BKT PER SEMESTER": "", + "UKT_KELOMPOK_1": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI_100%", + "UKT_KELOMPOK_2": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI_100%", + "UKT_KELOMPOK_3": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI 75%", + "UKT_KELOMPOK_4": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI 50%", + "UKT_KELOMPOK_5": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI 25%", + "UKT_KELOMPOK_6": "UKT_PENDIDIKAN_UNGGUL" + }, + { + "NO": "1", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknologi Rekayasa Perangkat Lunak", + "BKT PER SEMESTER": "28.008.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "2", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknologi Rekayasa Mesin", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "3", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknologi Rekayasa Elektro", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "4", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknologi Rekayasa Instrumentasi dan Kontrol", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "5", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknologi Survei dan Pemetaan Dasar", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "6", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Sistem Informasi Geografis", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "7", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Pengembangan Produk Agroindustri", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "8", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknik Pengelolaan dan Perawatan Alat Berat", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "9", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknologi Rekayasa Internet", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "10", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknik Pengelolaan dan Pemeliharaan Infrastruktur Sipil", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "11", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknologi Rekayasa Pelaksanaan Bangunan Sipil", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "12", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Teknologi Veteriner", + "BKT PER SEMESTER": "27.128.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "13", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Manajemen Informasi Kesehatan", + "BKT PER SEMESTER": "25.038.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "14", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Pengelolaan Hutan", + "BKT PER SEMESTER": "23.995.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.325.000", + "UKT_KELOMPOK_4": "6.650.000", + "UKT_KELOMPOK_5": "9.975.000", + "UKT_KELOMPOK_6": "13.300.000" + }, + { + "NO": "15", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Bisnis Perjalanan Wisata", + "BKT PER SEMESTER": "23.467.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.850.000", + "UKT_KELOMPOK_4": "5.700.000", + "UKT_KELOMPOK_5": "8.550.000", + "UKT_KELOMPOK_6": "11.400.000" + }, + { + "NO": "16", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Manajemen dan Penilaian Properti", + "BKT PER SEMESTER": "13.826.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.850.000", + "UKT_KELOMPOK_4": "5.700.000", + "UKT_KELOMPOK_5": "8.550.000", + "UKT_KELOMPOK_6": "11.400.000" + }, + { + "NO": "17", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Pengelolaan Arsip dan Rekaman Informasi", + "BKT PER SEMESTER": "13.298.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.850.000", + "UKT_KELOMPOK_4": "5.700.000", + "UKT_KELOMPOK_5": "8.550.000", + "UKT_KELOMPOK_6": "11.400.000" + }, + { + "NO": "18", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Bahasa Inggris", + "BKT PER SEMESTER": "13.298.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.850.000", + "UKT_KELOMPOK_4": "5.700.000", + "UKT_KELOMPOK_5": "8.550.000", + "UKT_KELOMPOK_6": "11.400.000" + }, + { + "NO": "19", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Bahasa Jepang untuk Komunikasi Bisnis dan Profesional", + "BKT PER SEMESTER": "13.298.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.850.000", + "UKT_KELOMPOK_4": "5.700.000", + "UKT_KELOMPOK_5": "8.550.000", + "UKT_KELOMPOK_6": "11.400.000" + }, + { + "NO": "20", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Akuntansi Sektor Publik", + "BKT PER SEMESTER": "13.298.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.850.000", + "UKT_KELOMPOK_4": "5.700.000", + "UKT_KELOMPOK_5": "8.550.000", + "UKT_KELOMPOK_6": "11.400.000" + }, + { + "NO": "21", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Pembangunan Ekonomi Kewilayahan", + "BKT PER SEMESTER": "13.298.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.850.000", + "UKT_KELOMPOK_4": "5.700.000", + "UKT_KELOMPOK_5": "8.550.000", + "UKT_KELOMPOK_6": "11.400.000" + }, + { + "NO": "22", + "PROGRAM": "D4", + "NAMA PROGRAM STUDI": "Perbankan", + "BKT PER SEMESTER": "13.298.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.850.000", + "UKT_KELOMPOK_4": "5.700.000", + "UKT_KELOMPOK_5": "8.550.000", + "UKT_KELOMPOK_6": "11.400.000" + } +] \ No newline at end of file diff --git a/assets/biaya_kuliah_s1_ugm.json b/assets/biaya_kuliah_s1_ugm.json new file mode 100644 index 0000000..f07d881 --- /dev/null +++ b/assets/biaya_kuliah_s1_ugm.json @@ -0,0 +1,866 @@ +[ + { + "NO": "", + "PROGRAM": "", + "NAMA PROGRAM STUDI": "", + "BKT PER SEMESTER": "", + "UKT_KELOMPOK_1": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI_100%", + "UKT_KELOMPOK_2": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI_100%", + "UKT_KELOMPOK_3": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI_75%", + "UKT_KELOMPOK_4": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI_50%", + "UKT_KELOMPOK_5": "UKT_PENDIDIKAN_UNGGUL_BERSUBSIDI_25%", + "UKT_KELOMPOK_6": "UKT_PENDIDIKAN_UNGGUL" + }, + { + "NO": "1", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Kedokteran", + "BKT PER SEMESTER": "35.145.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "6.175.000", + "UKT_KELOMPOK_4": "12.350.000", + "UKT_KELOMPOK_5": "18.525.000", + "UKT_KELOMPOK_6": "24.700.000" + }, + { + "NO": "2", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Kedokteran Hewan", + "BKT PER SEMESTER": "35.145.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "6.175.000", + "UKT_KELOMPOK_4": "12.350.000", + "UKT_KELOMPOK_5": "18.525.000", + "UKT_KELOMPOK_6": "24.700.000" + }, + { + "NO": "3", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Kedokteran Gigi", + "BKT PER SEMESTER": "33.740.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "6.175.000", + "UKT_KELOMPOK_4": "12.350.000", + "UKT_KELOMPOK_5": "18.525.000", + "UKT_KELOMPOK_6": "24.700.000" + }, + { + "NO": "4", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Farmasi", + "BKT PER SEMESTER": "27.919.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "4.325.000", + "UKT_KELOMPOK_4": "8.650.000", + "UKT_KELOMPOK_5": "12.975.000", + "UKT_KELOMPOK_6": "17.300.000" + }, + { + "NO": "5", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Ilmu Keperawatan", + "BKT PER SEMESTER": "26.802.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "4.325.000", + "UKT_KELOMPOK_4": "8.650.000", + "UKT_KELOMPOK_5": "12.975.000", + "UKT_KELOMPOK_6": "17.300.000" + }, + { + "NO": "6", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Mesin", + "BKT PER SEMESTER": "20.949.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.375.000", + "UKT_KELOMPOK_4": "6.750.000", + "UKT_KELOMPOK_5": "10.125.000", + "UKT_KELOMPOK_6": "13.500.000" + }, + { + "NO": "7", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Nuklir", + "BKT PER SEMESTER": "20.949.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "8", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Psikologi", + "BKT PER SEMESTER": "20.692.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.750.000", + "UKT_KELOMPOK_4": "5.500.000", + "UKT_KELOMPOK_5": "8.250.000", + "UKT_KELOMPOK_6": "11.000.000" + }, + { + "NO": "9", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Geologi", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "10", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknologi Hasil Perikanan", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "11", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Perencanaan Wilayah dan Kota", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "12", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Arsitektur", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "13", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Geodesi", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.750.000", + "UKT_KELOMPOK_4": "5.500.000", + "UKT_KELOMPOK_5": "8.250.000", + "UKT_KELOMPOK_6": "11.000.000" + }, + { + "NO": "14", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Kimia", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.375.000", + "UKT_KELOMPOK_4": "6.750.000", + "UKT_KELOMPOK_5": "10.125.000", + "UKT_KELOMPOK_6": "13.500.000" + }, + { + "NO": "15", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Elektro", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "16", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Industri", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "17", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknologi Industri Pertanian", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.750.000", + "UKT_KELOMPOK_4": "5.500.000", + "UKT_KELOMPOK_5": "8.250.000", + "UKT_KELOMPOK_6": "11.000.000" + }, + { + "NO": "18", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Fisika", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.750.000", + "UKT_KELOMPOK_4": "5.500.000", + "UKT_KELOMPOK_5": "8.250.000", + "UKT_KELOMPOK_6": "11.000.000" + }, + { + "NO": "19", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Sipil", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "20", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Elektronika dan Instrumentasi", + "BKT PER SEMESTER": "19.908.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "21", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Gizi", + "BKT PER SEMESTER": "19.865.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "22", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Biomedis", + "BKT PER SEMESTER": "19.273.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "23", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Pertanian", + "BKT PER SEMESTER": "19.112.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.750.000", + "UKT_KELOMPOK_4": "5.500.000", + "UKT_KELOMPOK_5": "8.250.000", + "UKT_KELOMPOK_6": "11.000.000" + }, + { + "NO": "24", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknologi Pangan dan Hasil Pertanian", + "BKT PER SEMESTER": "19.112.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.750.000", + "UKT_KELOMPOK_4": "5.500.000", + "UKT_KELOMPOK_5": "8.250.000", + "UKT_KELOMPOK_6": "11.000.000" + }, + { + "NO": "25", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Higiene Gigi", + "BKT PER SEMESTER": "19.037.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.375.000", + "UKT_KELOMPOK_4": "6.750.000", + "UKT_KELOMPOK_5": "10.125.000", + "UKT_KELOMPOK_6": "13.500.000" + }, + { + "NO": "26", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Biologi", + "BKT PER SEMESTER": "18.399.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "27", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Kimia", + "BKT PER SEMESTER": "18.399.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "28", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Infrastruktur Lingkungan", + "BKT PER SEMESTER": "18.315.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "29", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknik Sumber Daya Air", + "BKT PER SEMESTER": "18.315.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "30", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Geofisika", + "BKT PER SEMESTER": "17.663.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "31", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Fisika", + "BKT PER SEMESTER": "17.663.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "32", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Mikrobiologi Pertanian", + "BKT PER SEMESTER": "15.335.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "33", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Kehutanan", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.750.000", + "UKT_KELOMPOK_4": "5.500.000", + "UKT_KELOMPOK_5": "8.250.000", + "UKT_KELOMPOK_6": "11.000.000" + }, + { + "NO": "34", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Ilmu dan Industri Peternakan", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.750.000", + "UKT_KELOMPOK_4": "5.500.000", + "UKT_KELOMPOK_5": "8.250.000", + "UKT_KELOMPOK_6": "11.000.000" + }, + { + "NO": "35", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Agronomi", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "36", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Akuakultur", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "37", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Ekonomi Pertanian dan Agribisnis", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "38", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Ilmu Tanah", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "39", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Manajemen Sumberdaya Akuatik", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "40", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Penyuluhan dan Komunikasi Pertanian", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "41", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Proteksi Tanaman", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "42", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Teknologi Informasi", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "3.075.000", + "UKT_KELOMPOK_4": "6.150.000", + "UKT_KELOMPOK_5": "9.225.000", + "UKT_KELOMPOK_6": "12.300.000" + }, + { + "NO": "43", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Geografi Lingkungan", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "44", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Kartografi dan Penginderaan Jauh", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "45", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Akuntansi", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.300.000", + "UKT_KELOMPOK_4": "4.600.000", + "UKT_KELOMPOK_5": "6.900.000", + "UKT_KELOMPOK_6": "9.200.000" + }, + { + "NO": "46", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Manajemen", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.300.000", + "UKT_KELOMPOK_4": "4.600.000", + "UKT_KELOMPOK_5": "6.900.000", + "UKT_KELOMPOK_6": "9.200.000" + }, + { + "NO": "47", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Hukum", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.300.000", + "UKT_KELOMPOK_4": "4.600.000", + "UKT_KELOMPOK_5": "6.900.000", + "UKT_KELOMPOK_6": "9.200.000" + }, + { + "NO": "48", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Ilmu Komputer", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "49", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Ilmu Ekonomi", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.300.000", + "UKT_KELOMPOK_4": "4.600.000", + "UKT_KELOMPOK_5": "6.900.000", + "UKT_KELOMPOK_6": "9.200.000" + }, + { + "NO": "50", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Manajemen dan Kebijakan Publik", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.300.000", + "UKT_KELOMPOK_4": "4.600.000", + "UKT_KELOMPOK_5": "6.900.000", + "UKT_KELOMPOK_6": "9.200.000" + }, + { + "NO": "51", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Ilmu Hubungan Internasional", + "BKT PER SEMESTER": "14.667.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.200.000", + "UKT_KELOMPOK_4": "4.400.000", + "UKT_KELOMPOK_5": "6.600.000", + "UKT_KELOMPOK_6": "8.800.000" + }, + { + "NO": "52", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Ilmu Komunikasi", + "BKT PER SEMESTER": "14.081.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.300.000", + "UKT_KELOMPOK_4": "4.600.000", + "UKT_KELOMPOK_5": "6.900.000", + "UKT_KELOMPOK_6": "9.200.000" + }, + { + "NO": "53", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Pembangunan Sosial dan Kesejahteraan", + "BKT PER SEMESTER": "14.081.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.200.000", + "UKT_KELOMPOK_4": "4.400.000", + "UKT_KELOMPOK_5": "6.600.000", + "UKT_KELOMPOK_6": "8.800.000" + }, + { + "NO": "54", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Pariwisata", + "BKT PER SEMESTER": "14.081.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "1.900.000", + "UKT_KELOMPOK_4": "3.800.000", + "UKT_KELOMPOK_5": "5.700.000", + "UKT_KELOMPOK_6": "7.600.000" + }, + { + "NO": "55", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Statistika", + "BKT PER SEMESTER": "13.360.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "56", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Matematika", + "BKT PER SEMESTER": "13.360.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "57", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Ilmu Aktuaria", + "BKT PER SEMESTER": "12.292.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.500.000", + "UKT_KELOMPOK_4": "5.000.000", + "UKT_KELOMPOK_5": "7.500.000", + "UKT_KELOMPOK_6": "10.000.000" + }, + { + "NO": "58", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Arkeologi", + "BKT PER SEMESTER": "9.442.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.300.000", + "UKT_KELOMPOK_4": "4.600.000", + "UKT_KELOMPOK_5": "6.900.000", + "UKT_KELOMPOK_6": "9.200.000" + }, + { + "NO": "59", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Pembangunan Wilayah", + "BKT PER SEMESTER": "9.025.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.256.000", + "UKT_KELOMPOK_4": "4.512.000", + "UKT_KELOMPOK_5": "6.768.000", + "UKT_KELOMPOK_6": "9.025.000" + }, + { + "NO": "60", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Bahasa dan Sastra Prancis", + "BKT PER SEMESTER": "8.664.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "1.900.000", + "UKT_KELOMPOK_4": "3.800.000", + "UKT_KELOMPOK_5": "5.700.000", + "UKT_KELOMPOK_6": "7.600.000" + }, + { + "NO": "61", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Sastra Arab", + "BKT PER SEMESTER": "8.664.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "1.900.000", + "UKT_KELOMPOK_4": "3.800.000", + "UKT_KELOMPOK_5": "5.700.000", + "UKT_KELOMPOK_6": "7.600.000" + }, + { + "NO": "62", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Bahasa dan Sastra Indonesia", + "BKT PER SEMESTER": "8.664.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "1.900.000", + "UKT_KELOMPOK_4": "3.800.000", + "UKT_KELOMPOK_5": "5.700.000", + "UKT_KELOMPOK_6": "7.600.000" + }, + { + "NO": "63", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Bahasa dan Kebudayaan Korea", + "BKT PER SEMESTER": "8.664.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "1.900.000", + "UKT_KELOMPOK_4": "3.800.000", + "UKT_KELOMPOK_5": "5.700.000", + "UKT_KELOMPOK_6": "7.600.000" + }, + { + "NO": "64", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Bahasa dan Kebudayaan Jepang", + "BKT PER SEMESTER": "8.664.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "1.900.000", + "UKT_KELOMPOK_4": "3.800.000", + "UKT_KELOMPOK_5": "5.700.000", + "UKT_KELOMPOK_6": "7.600.000" + }, + { + "NO": "65", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Bahasa, Sastra, dan Budaya Jawa", + "BKT PER SEMESTER": "8.664.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "1.900.000", + "UKT_KELOMPOK_4": "3.800.000", + "UKT_KELOMPOK_5": "5.700.000", + "UKT_KELOMPOK_6": "7.600.000" + }, + { + "NO": "66", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Sastra Inggris", + "BKT PER SEMESTER": "8.664.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.200.000", + "UKT_KELOMPOK_4": "4.400.000", + "UKT_KELOMPOK_5": "6.600.000", + "UKT_KELOMPOK_6": "8.664.000" + }, + { + "NO": "67", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Politik dan Pemerintahan", + "BKT PER SEMESTER": "8.214.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.053.000", + "UKT_KELOMPOK_4": "4.107.000", + "UKT_KELOMPOK_5": "6.160.000", + "UKT_KELOMPOK_6": "8.214.000" + }, + { + "NO": "68", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Antropologi Budaya", + "BKT PER SEMESTER": "7.885.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.053.000", + "UKT_KELOMPOK_4": "4.107.000", + "UKT_KELOMPOK_5": "6.160.000", + "UKT_KELOMPOK_6": "7.885.000" + }, + { + "NO": "69", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Sejarah", + "BKT PER SEMESTER": "7.885.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "2.053.000", + "UKT_KELOMPOK_4": "4.107.000", + "UKT_KELOMPOK_5": "6.160.000", + "UKT_KELOMPOK_6": "7.885.000" + }, + { + "NO": "70", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Sosiologi", + "BKT PER SEMESTER": "7.885.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "1.889.000", + "UKT_KELOMPOK_4": "3.778.000", + "UKT_KELOMPOK_5": "5.667.000", + "UKT_KELOMPOK_6": "7.557.000" + }, + { + "NO": "71", + "PROGRAM": "S1", + "NAMA PROGRAM STUDI": "Filsafat", + "BKT PER SEMESTER": "7.885.000", + "UKT_KELOMPOK_1": "0", + "UKT_KELOMPOK_2": "0", + "UKT_KELOMPOK_3": "1.889.000", + "UKT_KELOMPOK_4": "3.778.000", + "UKT_KELOMPOK_5": "5.667.000", + "UKT_KELOMPOK_6": "7.557.000" + } +] diff --git a/assets/data_beasiswa.json b/assets/data_beasiswa.json new file mode 100644 index 0000000..da4ec00 --- /dev/null +++ b/assets/data_beasiswa.json @@ -0,0 +1,184 @@ +{ + "beasiswa": [ + { + "nama": "Beasiswa Indonesia Maju (BIM)", + "penyelenggara": "Kementerian Pendidikan, Kebudayaan, Riset, dan Teknologi", + "jenjang": ["S1", "S2"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Beasiswa bergelar (S1 dan S2) serta beasiswa non-gelar (Program Persiapan S1 Luar Negeri).", + "persyaratan": [ + "WNI", + "Memiliki IPK minimal 3.0", + "Lolos seleksi administrasi, substansi, dan wawancara" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://bim-pusatprestasinasional.kemdikbud.go.id/" + }, + { + "nama": "Beasiswa KIP Kuliah", + "penyelenggara": "Kementerian Pendidikan, Kebudayaan, Riset, dan Teknologi", + "jenjang": ["S1"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Bantuan biaya pendidikan dan biaya hidup bagi mahasiswa dari keluarga kurang mampu.", + "persyaratan": [ + "WNI", + "Memiliki KIP", + "Lolos seleksi administrasi" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://kip-kuliah.kemdikbud.go.id/" + }, + { + "nama": "Beasiswa LPDP (Lembaga Pengelola Dana Pendidikan)", + "penyelenggara": "Kementerian Keuangan RI", + "jenjang": ["S1", "S2", "S3"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Beasiswa penuh yang mencakup biaya pendidikan dan biaya hidup untuk pendidikan dalam dan luar negeri.", + "persyaratan": [ + "WNI", + "Memiliki IPK minimal 3.0 untuk S1, 3.25 untuk S2/S3", + "Usia maksimal 21 tahun untuk S1, 35 tahun untuk S2, 40 tahun untuk S3", + "Lolos seleksi administrasi, substansi, dan wawancara" + ], + "deadline": "Biasanya dibuka 2-3 kali setahun", + "link": "https://www.lpdp.kemenkeu.go.id" + }, + { + "nama": "Beasiswa S1 Glow and Lovely Bintang Beasiswa", + "penyelenggara": "Glow and Lovely", + "jenjang": ["S1"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Beasiswa penuh untuk program S1 di bidang yang relevan.", + "persyaratan": [ + "WNI", + "Memiliki prestasi akademik yang baik" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://www.glowandlovely.in" + }, + { + "nama": "Beasiswa Bank Indonesia", + "penyelenggara": "Bank Indonesia", + "jenjang": ["S1"], + "bidang_studi": ["Ekonomi", "Keuangan", "Pendidikan"], + "deskripsi": "Bantuan dana pendidikan bulanan, pelatihan soft skill dan hard skill, serta workshop.", + "persyaratan": [ + "WNI", + "Memiliki IPK minimal 3.0", + "Lolos seleksi administrasi dan wawancara" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://www.kompas.com/edu/read/2025/01/27/125955371/beasiswa-bank-indonesia-2025-dibuka-cek-syarat-dan-cara-daftar" + }, + { + "nama": "Beasiswa Van Deventer-Maas Indonesia (VDMI)", + "penyelenggara": "VDMI", + "jenjang": ["S1"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Bantuan dana bulanan, bonus lulus tepat waktu, bonus TOEFL, serta pelatihan pengembangan kapasitas pribadi.", + "persyaratan": [ + "WNI", + "Memiliki prestasi akademik yang baik", + "Lolos seleksi administrasi dan wawancara" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://beasiswapascasarjana.com" + }, + { + "nama": "Beasiswa Dataprint", + "penyelenggara": "Dataprint", + "jenjang": ["S1"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Bantuan dana sebesar Rp500.000 yang diberikan satu kali.", + "persyaratan": [ + "WNI", + "Memiliki prestasi akademik yang baik" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://indbeasiswa.com/beasiswa-dataprint/" + }, + { + "nama": "Beasiswa Pertamina Sobat Bumi", + "penyelenggara": "Pertamina", + "jenjang": ["S1"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Dana hidup bulanan dan dana program pengabdian untuk desa.", + "persyaratan": [ + "WNI", + "Memiliki IPK minimal 3.0", + "Lolos seleksi administrasi" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://indbeasiswa.com/beasiswa-pertamina-sobat-bumi/" + }, + { + "nama": "Beasiswa Karya Salemba Empat (KSE)", + "penyelenggara": "Karya Salemba Empat", + "jenjang": ["S1"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Bantuan dana pendidikan dan pengembangan diri.", + "persyaratan": [ + "WNI", + "Memiliki IPK minimal 3.0", + "Lolos seleksi administrasi" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://indbeasiswa.com/beasiswa-karya-salemba-empat/" + }, + { + "nama": "Beasiswa Djarum Plus", + "penyelenggara": "Djarum Foundation", + "jenjang": ["S1"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Bantuan dana bulanan dan program pengembangan diri.", + "persyaratan": [ + "WNI", + "Memiliki prestasi akademik yang baik" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://indbeasiswa.com/beasiswa-djarum-plus/" + }, + { + "nama": "Beasiswa Tanoto Foundation", + "penyelenggara": "Tanoto Foundation", + "jenjang": ["S1"], + "bidang_studi": ["Semua Bidang Studi"], + "deskripsi": "Bantuan biaya pendidikan dan biaya hidup.", + "persyaratan": [ + "WNI", + "Memiliki IPK minimal 3.0", + "Lolos seleksi administrasi dan wawancara" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://indbeasiswa.com/beasiswa-tanoto-foundation/" + }, + { + "nama": "Beasiswa Bakti BCA", + "penyelenggara": "BCA", + "jenjang": ["S1"], + "bidang_studi": ["Ekonomi", "Pendidikan", "Keuangan"], + "deskripsi": "Uang saku bulanan dan bantuan UKT (Uang Kuliah Tunggal).", + "persyaratan": [ + "WNI", + "Memiliki IPK minimal 3.0", + "Lolos seleksi administrasi dan wawancara" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://indbeasiswa.com/beasiswa-bakti-bca/" + }, + { + "nama": "Beasiswa BCA Finance", + "penyelenggara": "BCA Finance", + "jenjang": ["S1"], + "bidang_studi": ["Ekonomi", "Keuangan"], + "deskripsi": "Bantuan biaya kuliah per semester.", + "persyaratan": [ + "WNI", + "Memiliki IPK minimal 3.0" + ], + "deadline": "Biasanya dibuka setiap tahun", + "link": "https://indbeasiswa.com/beasiswa-bca-finance/" + } + ] + } + \ No newline at end of file diff --git a/assets/data_ekonomi.json b/assets/data_ekonomi.json new file mode 100644 index 0000000..a852cd3 --- /dev/null +++ b/assets/data_ekonomi.json @@ -0,0 +1,20 @@ +{ + "data_ekonomi": [ + { + "kode": "E01", + "deskripsi": "Saya dan keluarga memiliki kondisi ekonomi yang cukup untuk melanjutkan kuliah" + }, + { + "kode": "E02", + "deskripsi": "Saya perlu mempertimbangkan biaya karena kondisi ekonomi keluarga terbatas" + }, + { + "kode": "E03", + "deskripsi": "Saya berminat kuliah tapi berencana mencari beasiswa atau bantuan pendanaan" + }, + { + "kode": "E04", + "deskripsi": "Saya lebih memilih bekerja atau usaha karena kondisi ekonomi saat ini" + } + ] + } \ No newline at end of file diff --git a/assets/ic_denis.jpg b/assets/ic_denis.jpg new file mode 100644 index 0000000..4962e48 Binary files /dev/null and b/assets/ic_denis.jpg differ diff --git a/assets/ic_google.png b/assets/ic_google.png new file mode 100644 index 0000000..554ec0d Binary files /dev/null and b/assets/ic_google.png differ diff --git a/assets/ipa_sains_kerja.json b/assets/ipa_sains_kerja.json new file mode 100644 index 0000000..78ee2ab --- /dev/null +++ b/assets/ipa_sains_kerja.json @@ -0,0 +1,734 @@ +{ + "J01A-Kerja": { + "name": "IPA (Sains Murni) - Kerja", + "description": "Fokus: Biologi, Kimia, Fisika (pekerjaan di bidang kesehatan, sains, farmasi, laboratorium, lingkungan) tanpa kuliah formal, bisa belajar mandiri melalui kursus, sertifikasi, dan pelatihan.", + "categories": [ + "Kesehatan & Sains", + "Penelitian", + "Pekerjaan Sains", + "Teknis Laboratorium" + ], + "minat": { + "Kedokteran": { + "pertanyaan": [ + "AKER01: Saya tertarik bekerja di rumah sakit atau klinik tanpa kuliah formal [6]", + "AKER02: Saya tertarik ingin mempelajari cara dasar merawat pasien atau membantu mereka sembuh [7]", + "AKER03: Saya tertarik untuk mendapatkan pengetahuan medis melalui kursus online atau pelatihan [6]", + "AKER04: Saya tertarik ingin bekerja sebagai asisten medis atau tenaga medis tanpa harus menjadi dokter [5]", + "AKER05: Saya tertarik dengan pekerjaan administrasi di lingkungan rumah sakit atau klinik [5]", + "AKER06: Saya tertarik memiliki empati tinggi dan ingin membantu pasien secara langsung [5]" + ], + "karir": [ + "AKAR01: Asisten Medis [20]", + "AKAR02: Tenaga Kesehatan [20]", + "AKAR03: Staf Klinik [20]", + "AKAR04: Administrasi Rumah Sakit [20]", + "AKAR05: Teknisi Alat Kesehatan [20]", + "AKAR06: Customer Service Asuransi Kesehatan [20]" + ], + "jurusan_terkait": [ + "D3 Keperawatan", + "D3 Teknologi Laboratorium Medik", + "D3 Farmasi", + "D3 Rekam Medis dan Informasi Kesehatan", + "D3 Teknologi Bank Darah", + "S1 Kesehatan Masyarakat" + ], + "rekomendasi_kursus": [ + "Pelatihan Bantuan Hidup Dasar (BHD)", + "Kursus Asisten Perawat Tersertifikasi", + "Pelatihan Administrasi Rumah Sakit", + "Sertifikasi Teknisi Alat Kesehatan" + ] + }, + "Sains": { + "pertanyaan": [ + "AKER07: Saya tertarik ingin menjadi peneliti atau bekerja di laboratorium [7]", + "AKER08: Saya tertarik senang melakukan eksperimen ilmiah dan mempelajari data percobaan [6]", + "AKER09: Saya tertarik untuk bekerja di bidang riset atau pengembangan produk tanpa kuliah formal [6]", + "AKER10: Saya tertarik ingin bekerja dengan tim sains di industri atau lembaga penelitian [6]", + "AKER11: Saya tertarik teliti dan mampu mengikuti protokol dengan tepat [5]", + "AKER12: Saya tertarik dalam pengolahan dan analisis data penelitian [5]" + ], + "karir": [ + "AKAR07: Teknisi Laboratorium [20]", + "AKAR08: Teknisi Peralatan Sains [20]", + "AKAR09: Staf R&D [20]", + "AKAR10: Pengumpul Data Lapangan [20]", + "AKAR11: Operator Peralatan Penelitian [20]", + "AKAR12: Staf Pengujian Kualitas [20]" + ], + "jurusan_terkait": [ + "D3 Analis Kimia", + "D3 Teknik Kimia", + "S1 Kimia", + "S1 Biologi", + "S1 Fisika", + "D3 MIPA" + ], + "rekomendasi_kursus": [ + "Sertifikasi Teknisi Laboratorium", + "Pelatihan Good Laboratory Practice (GLP)", + "Kursus Pengoperasian Alat Laboratorium", + "Pelatihan Analisis Data Dasar", + "Sertifikasi K3 Laboratorium" + ] + }, + "Farmasi": { + "pertanyaan": [ + "AKER13: Saya tertarik untuk bekerja di apotek atau di industri farmasi tanpa kuliah formal [6]", + "AKER14: Saya tertarik ingin mempelajari bagaimana obat bekerja melalui kursus atau pelatihan singkat [6]", + "AKER15: Saya tertarik dengan pengujian obat dan kualitas farmasi di industri [6]", + "AKER16: Saya tertarik ingin menjadi bagian dari tim yang mengembangkan produk obat [6]", + "AKER17: Saya tertarik teliti dalam mengukur dan mencatat informasi penting [6]", + "AKER18: Saya tertarik dalam penyimpanan dan pendistribusian obat-obatan [6]" + ], + "riasecType": [ + { + "type": [ + "R", + "C" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 6 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "C" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "C", + "R" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "AKAR13: Asisten Apoteker [20]", + "AKAR14: Teknisi Farmasi [20]", + "AKAR15: Staf Produksi Obat [20]", + "AKAR16: Pengujian Kualitas Obat [20]", + "AKAR17: Staf Logistik Farmasi [20]", + "AKAR18: Sales Representative Produk Farmasi [20]", + "AKAR19: Staf Administrasi Apotek [20]" + ], + "karir_riasec": [ + "I/C", + "I/R", + "R/C", + "I/C", + "C", + "E/S", + "C" + ], + "jurusan_terkait": [ + "D3 Farmasi", + "S1 Farmasi", + "D3 Teknik Kimia", + "D3 Analis Kimia", + "S1 Kimia Farmasi" + ], + "rekomendasi_kursus": [ + "Pelatihan Asisten Apoteker", + "Sertifikasi Good Manufacturing Practice (GMP)", + "Kursus Pengelolaan Obat", + "Pelatihan Teknik Penjualan Produk Farmasi", + "Kursus Komputer untuk Administrasi Apotek" + ] + }, + "Laboratorium": { + "pertanyaan": [ + "AKER19: Saya tertarik bekerja dengan peralatan dan instrumen laboratorium [6]", + "AKER20: Saya tertarik menyukai pekerjaan yang membutuhkan ketelitian dan konsistensi tinggi [6]", + "AKER21: Saya tertarik ingin bekerja di laboratorium klinik, industri, atau penelitian [6]", + "AKER22: Saya tertarik untuk mempelajari teknik-teknik analisis laboratorium [6]", + "AKER23: Saya tertarik mampu mengikuti protokol dan prosedur standar dengan tepat [6]", + "AKER24: Saya tertarik dalam persiapan sampel dan pengujian bahan [6]" + ], + "riasecType": [ + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "C" + ], + "bobot": [ + 6 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 6 + ] + }, + { + "type": [ + "C" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "AKAR20: Teknisi Laboratorium Klinik [20]", + "AKAR21: Teknisi Laboratorium Industri [20]", + "AKAR22: Staf Pengujian Kualitas [20]", + "AKAR23: Teknisi Instrumentasi [20]", + "AKAR24: Staf Pengambilan Sampel [20]" + ], + "karir_riasec": [ + "I/R", + "I/R", + "I/C", + "R/I", + "R" + ], + "jurusan_terkait": [ + "D3 Teknologi Laboratorium Medik", + "D3 Analis Kimia", + "D3 Analis Kesehatan", + "S1 Kimia", + "S1 Biologi", + "D3 Mikrobiologi" + ], + "rekomendasi_kursus": [ + "Sertifikasi Teknisi Laboratorium", + "Pelatihan Teknik Dasar Mikrobiologi", + "Kursus Instrumentasi Laboratorium", + "Pelatihan Sistem Manajemen Mutu Laboratorium", + "Kursus Pengambilan dan Penanganan Sampel" + ] + }, + "Lingkungan": { + "pertanyaan": [ + "AKER25: Saya tertarik untuk bekerja dalam pemantauan dan pengelolaan lingkungan [6]", + "AKER26: Saya tertarik peduli terhadap isu-isu lingkungan dan pelestarian alam [7]", + "AKER27: Saya tertarik suka bekerja di lapangan dan mengumpulkan data lingkungan [7]", + "AKER28: Saya tertarik untuk mempelajari cara mengukur polusi dan kualitas lingkungan [6]", + "AKER29: Saya tertarik ingin bekerja dalam proyek konservasi atau pengelolaan limbah [6]", + "AKER30: Saya tertarik dalam pendidikan lingkungan dan kampanye kesadaran [7]" + ], + "riasecType": [ + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "S", + "A" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "R", + "S" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "S", + "A" + ], + "bobot": [ + 2, + 2 + ] + } + ], + "karir": [ + "AKAR25: Teknisi Pemantauan Lingkungan [20]", + "AKAR26: Staf Pengelolaan Limbah [20]", + "AKAR27: Asisten Pengambil Sampel Lingkungan [20]", + "AKAR28: Operator Pengolahan Air [20]", + "AKAR29: Staf Lapangan Konservasi [20]", + "AKAR30: Teknisi Pengendalian Polusi [20]" + ], + "karir_riasec": [ + "I/R", + "R/C", + "R/I", + "R", + "R/S", + "I/R" + ], + "jurusan_terkait": [ + "D3 Teknik Lingkungan", + "S1 Teknik Lingkungan", + "S1 Ilmu Lingkungan", + "S1 Biologi Lingkungan", + "D3 Pengelolaan Sumber Daya Air dan Lingkungan" + ], + "rekomendasi_kursus": [ + "Sertifikasi Teknisi Lingkungan", + "Pelatihan Pengelolaan Limbah", + "Kursus Pengambilan Sampel Lingkungan", + "Pelatihan Operator Instalasi Pengolahan Air", + "Kursus K3 Lingkungan" + ] + }, + "Pangan dan Nutrisi": { + "pertanyaan": [ + "AKER31: Saya tertarik bekerja dalam pengujian dan pengembangan produk pangan [6]", + "AKER32: Saya tertarik ingin mempelajari cara menganalisis kualitas dan keamanan makanan [7]", + "AKER33: Saya tertarik untuk bekerja di industri pengolahan pangan [5]", + "AKER34: Saya tertarik peduli tentang nutrisi dan dampaknya terhadap kesehatan [5]", + "AKER35: Saya tertarik dalam produksi dan pengawasan makanan [5]", + "AKER36: Saya tertarik ingin membantu orang dengan masalah nutrisi dan diet [4]" + ], + "riasecType": [ + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "R" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "C" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "S" + ], + "bobot": [ + 4 + ] + } + ], + "karir": [ + "AKAR31: Teknisi Pengujian Pangan [20]", + "AKAR32: Asisten Quality Control Makanan [20]", + "AKAR33: Staf Produksi Industri Pangan [20]", + "AKAR34: Teknisi Pengolahan Makanan [20]", + "AKAR35: Staf Pengembangan Produk Pangan [20]", + "AKAR36: Asisten Food Safety [20]" + ], + "karir_riasec": [ + "I/R", + "C/I", + "R", + "R/I", + "I/R", + "C/I" + ], + "jurusan_terkait": [ + "D3 Teknologi Pangan", + "S1 Ilmu dan Teknologi Pangan", + "D3 Gizi", + "S1 Ilmu Gizi", + "D3 Pengawasan Mutu Pangan" + ], + "rekomendasi_kursus": [ + "Sertifikasi Keamanan Pangan (HACCP)", + "Pelatihan Quality Control Makanan", + "Kursus Pengujian Nutrisi Pangan", + "Pelatihan Good Manufacturing Practice (GMP) untuk Pangan", + "Kursus Dasar Nutrisi dan Diet" + ] + }, + "Peralatan Medis": { + "pertanyaan": [ + "AKER37: Saya tertarik bekerja dengan peralatan dan teknologi medis [6]", + "AKER38: Saya tertarik ingin mempelajari cara mengoperasikan dan memelihara alat medis [7]", + "AKER39: Saya tertarik untuk bekerja di rumah sakit atau perusahaan peralatan medis [5]", + "AKER40: Saya tertarik memiliki keterampilan teknis dan ingin menerapkannya di bidang kesehatan [6]", + "AKER41: Saya tertarik dalam pengenalan dan pelatihan penggunaan peralatan medis [5]", + "AKER42: Saya tertarik suka memperbaiki dan memecahkan masalah pada perangkat elektronik [6]" + ], + "riasecType": [ + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "S", + "E" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "S", + "E" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R" + ], + "bobot": [ + 5 + ] + } + ], + "karir": [ + "AKAR37: Teknisi Peralatan Medis [20]", + "AKAR38: Teknisi Laboratorium Medis [20]", + "AKAR39: Staf Penjualan Alat Kesehatan [20]", + "AKAR40: Teknisi Pemeliharaan Alat Medis [20]", + "AKAR41: Staf Logistik Peralatan Medis [20]" + ], + "karir_riasec": [ + "R/I", + "I/R", + "E/S", + "R", + "C/R" + ], + "jurusan_terkait": [ + "D3 Teknik Elektromedik", + "D3 Teknik Elektronika", + "D3 Teknik Biomedis", + "S1 Teknik Biomedis", + "D3 Teknik Instrumentasi" + ], + "rekomendasi_kursus": [ + "Sertifikasi Teknisi Peralatan Medis", + "Pelatihan Dasar Radiologi", + "Kursus Elektrokardiografi", + "Pelatihan Pemeliharaan Peralatan Laboratorium", + "Kursus Penjualan Alat Kesehatan" + ] + } + }, + "semua_pertanyaan": [ + { + "kode_pertanyaan": "AKER01", + "isi_pertanyaan": "Saya tertarik bekerja di rumah sakit atau klinik tanpa kuliah formal" + }, + { + "kode_pertanyaan": "AKER02", + "isi_pertanyaan": "Saya tertarik ingin mempelajari cara dasar merawat pasien atau membantu mereka sembuh" + }, + { + "kode_pertanyaan": "AKER03", + "isi_pertanyaan": "Saya tertarik untuk mendapatkan pengetahuan medis melalui kursus online atau pelatihan" + }, + { + "kode_pertanyaan": "AKER04", + "isi_pertanyaan": "Saya tertarik ingin bekerja sebagai asisten medis atau tenaga medis tanpa harus menjadi dokter" + }, + { + "kode_pertanyaan": "AKER05", + "isi_pertanyaan": "Saya tertarik dengan pekerjaan administrasi di lingkungan rumah sakit atau klinik" + }, + { + "kode_pertanyaan": "AKER06", + "isi_pertanyaan": "Saya tertarik memiliki empati tinggi dan ingin membantu pasien secara langsung" + }, + { + "kode_pertanyaan": "AKER07", + "isi_pertanyaan": "Saya tertarik ingin menjadi peneliti atau bekerja di laboratorium" + }, + { + "kode_pertanyaan": "AKER08", + "isi_pertanyaan": "Saya tertarik senang melakukan eksperimen ilmiah dan mempelajari data percobaan" + }, + { + "kode_pertanyaan": "AKER09", + "isi_pertanyaan": "Saya tertarik untuk bekerja di bidang riset atau pengembangan produk tanpa kuliah formal" + }, + { + "kode_pertanyaan": "AKER10", + "isi_pertanyaan": "Saya tertarik ingin bekerja dengan tim sains di industri atau lembaga penelitian" + }, + { + "kode_pertanyaan": "AKER11", + "isi_pertanyaan": "Saya tertarik teliti dan mampu mengikuti protokol dengan tepat" + }, + { + "kode_pertanyaan": "AKER12", + "isi_pertanyaan": "Saya tertarik dalam pengolahan dan analisis data penelitian" + }, + { + "kode_pertanyaan": "AKER13", + "isi_pertanyaan": "Saya tertarik untuk bekerja di apotek atau di industri farmasi tanpa kuliah formal" + }, + { + "kode_pertanyaan": "AKER14", + "isi_pertanyaan": "Saya tertarik ingin mempelajari bagaimana obat bekerja melalui kursus atau pelatihan singkat" + }, + { + "kode_pertanyaan": "AKER15", + "isi_pertanyaan": "Saya tertarik dengan pengujian obat dan kualitas farmasi di industri" + }, + { + "kode_pertanyaan": "AKER16", + "isi_pertanyaan": "Saya tertarik ingin menjadi bagian dari tim yang mengembangkan produk obat" + }, + { + "kode_pertanyaan": "AKER17", + "isi_pertanyaan": "Saya tertarik teliti dalam mengukur dan mencatat informasi penting" + }, + { + "kode_pertanyaan": "AKER18", + "isi_pertanyaan": "Saya tertarik dalam penyimpanan dan pendistribusian obat-obatan" + }, + { + "kode_pertanyaan": "AKER19", + "isi_pertanyaan": "Saya tertarik bekerja dengan peralatan dan instrumen laboratorium" + }, + { + "kode_pertanyaan": "AKER20", + "isi_pertanyaan": "Saya tertarik menyukai pekerjaan yang membutuhkan ketelitian dan konsistensi tinggi" + }, + { + "kode_pertanyaan": "AKER21", + "isi_pertanyaan": "Saya tertarik ingin bekerja di laboratorium klinik, industri, atau penelitian" + }, + { + "kode_pertanyaan": "AKER22", + "isi_pertanyaan": "Saya tertarik untuk mempelajari teknik-teknik analisis laboratorium" + }, + { + "kode_pertanyaan": "AKER23", + "isi_pertanyaan": "Saya tertarik mampu mengikuti protokol dan prosedur standar dengan tepat" + }, + { + "kode_pertanyaan": "AKER24", + "isi_pertanyaan": "Saya tertarik dalam persiapan sampel dan pengujian bahan" + }, + { + "kode_pertanyaan": "AKER25", + "isi_pertanyaan": "Saya tertarik untuk bekerja dalam pemantauan dan pengelolaan lingkungan" + }, + { + "kode_pertanyaan": "AKER26", + "isi_pertanyaan": "Saya tertarik peduli terhadap isu-isu lingkungan dan pelestarian alam" + }, + { + "kode_pertanyaan": "AKER27", + "isi_pertanyaan": "Saya tertarik suka bekerja di lapangan dan mengumpulkan data lingkungan" + }, + { + "kode_pertanyaan": "AKER28", + "isi_pertanyaan": "Saya tertarik untuk mempelajari cara mengukur polusi dan kualitas lingkungan" + }, + { + "kode_pertanyaan": "AKER29", + "isi_pertanyaan": "Saya tertarik ingin bekerja dalam proyek konservasi atau pengelolaan limbah" + }, + { + "kode_pertanyaan": "AKER30", + "isi_pertanyaan": "Saya tertarik dalam pendidikan lingkungan dan kampanye kesadaran" + }, + { + "kode_pertanyaan": "AKER31", + "isi_pertanyaan": "Saya tertarik bekerja dalam pengujian dan pengembangan produk pangan" + }, + { + "kode_pertanyaan": "AKER32", + "isi_pertanyaan": "Saya tertarik ingin mempelajari cara menganalisis kualitas dan keamanan makanan" + }, + { + "kode_pertanyaan": "AKER33", + "isi_pertanyaan": "Saya tertarik untuk bekerja di industri pengolahan pangan" + }, + { + "kode_pertanyaan": "AKER34", + "isi_pertanyaan": "Saya tertarik peduli tentang nutrisi dan dampaknya terhadap kesehatan" + }, + { + "kode_pertanyaan": "AKER35", + "isi_pertanyaan": "Saya tertarik dalam produksi dan pengawasan makanan" + }, + { + "kode_pertanyaan": "AKER36", + "isi_pertanyaan": "Saya tertarik ingin membantu orang dengan masalah nutrisi dan diet" + }, + { + "kode_pertanyaan": "AKER37", + "isi_pertanyaan": "Saya tertarik bekerja dengan peralatan dan teknologi medis" + }, + { + "kode_pertanyaan": "AKER38", + "isi_pertanyaan": "Saya tertarik ingin mempelajari cara mengoperasikan dan memelihara alat medis" + }, + { + "kode_pertanyaan": "AKER39", + "isi_pertanyaan": "Saya tertarik untuk bekerja di rumah sakit atau perusahaan peralatan medis" + }, + { + "kode_pertanyaan": "AKER40", + "isi_pertanyaan": "Saya tertarik memiliki keterampilan teknis dan ingin menerapkannya di bidang kesehatan" + }, + { + "kode_pertanyaan": "AKER41", + "isi_pertanyaan": "Saya tertarik dalam pengenalan dan pelatihan penggunaan peralatan medis" + }, + { + "kode_pertanyaan": "AKER42", + "isi_pertanyaan": "Saya tertarik suka memperbaiki dan memecahkan masalah pada perangkat elektronik" + } + ] + } +} \ No newline at end of file diff --git a/assets/ipa_sains_kuliah.json b/assets/ipa_sains_kuliah.json new file mode 100644 index 0000000..b9e4648 --- /dev/null +++ b/assets/ipa_sains_kuliah.json @@ -0,0 +1,1046 @@ +{ + "J01A": { + "name": "IPA (Sains Murni)", + "description": "Fokus: Biologi, Kimia, Fisika (kedokteran, farmasi, sains, teknik biomedis, lingkungan)", + "categories": [ + "Kesehatan & Sains" + ], + "riasec": { + "primary": "I (Investigative)", + "secondary": "R (Realistic)", + "tertiary": "S (Social)", + "code": "IRS" + }, + "minat": { + "Kedokteran": { + "pertanyaan": [ + "AKUL01: Saya tertarik untuk mempelajari bagaimana tubuh manusia bekerja [5]", + "AKUL02: Saya tertarik ingin membantu orang lain untuk sembuh dari penyakit [6]", + "AKUL03: Saya tertarik mempelajari cara-cara medis untuk mendiagnosis dan mengobati penyakit [7]", + "AKUL04: Saya tertarik suka belajar tentang sistem organ tubuh dan bagaimana mereka berfungsi [6]", + "AKUL05: Saya tertarik untuk mengikuti perkembangan teknologi dalam dunia kedokteran [6]", + "AKUL06: Saya tertarik memiliki ketelitian tinggi dan mampu bekerja di bawah tekanan [5]" + ], + "riasecType": [ + { + "type": [ + "I" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "S", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "I", + "E" + ], + "bobot": [ + 3, + 1 + ] + }, + { + "type": [ + "C", + "R" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "Dokter Umum [20]", + "Dokter Spesialis [20]", + "Perawat Medis [20]", + "Dokter Gigi [20]", + "Ahli Bedah [20]", + "Dokter Forensik [20]", + "Dosen/Peneliti Kedokteran [20]" + ], + "karir_riasec": [ + "I/S", + "I/S", + "S/I", + "I/S", + "I/R", + "I/R", + "I" + ], + "jurusan_terkait": [ + "AJUR01: Kedokteran Umum", + "AJUR02: Kedokteran Gigi", + "AJUR03: Kedokteran Hewan", + "AJUR04: Kesehatan Masyarakat", + "AJUR05: Keperawatan", + "AJUR06: Kebidanan" + ], + "universitas_rekomendasi": [ + "Universitas Indonesia (UI)", + "Universitas Gadjah Mada (UGM)", + "Universitas Airlangga (UNAIR)", + "Universitas Padjadjaran (UNPAD)", + "Universitas Hasanuddin (UNHAS)", + "Universitas Diponegoro (UNDIP)", + "Universitas Brawijaya (UB)" + ] + }, + "Sains": { + "pertanyaan": [ + "AKUL07: Saya tertarik untuk melakukan eksperimen ilmiah dan menemukan sesuatu yang baru [6]", + "AKUL08: Saya tertarik senang menganalisis data dan hasil percobaan [7]", + "AKUL09: Saya tertarik suka belajar tentang alam dan bagaimana semua itu bekerja [6]", + "AKUL10: Saya tertarik ingin memahami lebih dalam tentang dunia di sekitar Anda, seperti hewan, tumbuhan, atau planet [7]", + "AKUL11: Saya tertarik menikmati kegiatan praktikum di laboratorium [7]", + "AKUL12: Saya tertarik untuk memecahkan masalah lingkungan menggunakan pendekatan ilmiah [7]" + ], + "riasecType": [ + { + "type": [ + "I", + "A" + ], + "bobot": [ + 5, + 1 + ] + }, + { + "type": [ + "I", + "C" + ], + "bobot": [ + 4, + 1 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 6 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 5, + 1 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 4, + 1 + ] + } + ], + "karir": [ + "Peneliti Biologi [20]", + "Peneliti Kimia [20]", + "Asisten Dosen/Dosen [20]", + "Scientist R&D [20]", + "Ahli Ekologi [20]", + "Ahli Fisika [20]", + "Ahli Meteorologi [20]", + "Ahli Geologi [20]" + ], + "karir_riasec": [ + "I", + "I", + "I/S", + "I", + "I/R", + "I", + "I", + "I/R" + ], + "jurusan_terkait": [ + "AJUR07: Biologi", + "AJUR08: Kimia", + "AJUR09: Fisika", + "AJUR10: Matematika", + "AJUR11: Fisika (peminatan Astrofisika)", + "AJUR12: Geofisika", + "AJUR13: Meteorologi dan Klimatologi" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Universitas Gadjah Mada (UGM)", + "Institut Pertanian Bogor (IPB)", + "Universitas Padjadjaran (UNPAD)", + "Universitas Airlangga (UNAIR)", + "Institut Teknologi Sepuluh Nopember (ITS)" + ] + }, + "Farmasi": { + "pertanyaan": [ + "AKUL13: Saya tertarik untuk mempelajari bagaimana obat bekerja dalam tubuh manusia [6]", + "AKUL14: Saya tertarik ingin membantu menciptakan obat-obatan yang dapat menyembuhkan penyakit [6]", + "AKUL15: Saya tertarik suka belajar tentang bahan kimia dan bagaimana mereka digunakan untuk tujuan medis [7]", + "AKUL16: Saya tertarik ingin tahu lebih banyak tentang bagaimana obat dikembangkan dan diuji [6]", + "AKUL17: Saya tertarik pada pembuatan dan pengembangan kosmetik dan produk perawatan tubuh [6]", + "AKUL18: Saya tertarik memiliki ketelitian tinggi dalam mengukur dan mencampur bahan-bahan kimia [6]" + ], + "riasecType": [ + { + "type": [ + "I" + ], + "bobot": [ + 6 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 1 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "R", + "A" + ], + "bobot": [ + 2, + 2 + ] + }, + { + "type": [ + "C", + "R" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "Apoteker [20]", + "Peneliti Obat [20]", + "Quality Control Farmasi [20]", + "Asisten Apoteker [20]", + "Ahli Formulasi [20]", + "Apoteker Klinis [20]" + ], + "karir_riasec": [ + "I/C", + "I", + "C/I", + "C/I", + "I/R", + "I/S" + ], + "jurusan_terkait": [ + "AJUR14: Farmasi", + "AJUR15: Kimia", + "AJUR16: Teknologi Pangan", + "AJUR17: Bioteknologi", + "AJUR18: Teknik Kimia", + "AJUR19: Biokimia" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Universitas Gadjah Mada (UGM)", + "Universitas Airlangga (UNAIR)", + "Universitas Padjadjaran (UNPAD)", + "Universitas Sanata Dharma (USD)", + "Universitas Hasanuddin (UNHAS)" + ] + }, + "Bioteknologi": { + "pertanyaan": [ + "AKUL19: Saya tertarik untuk menggabungkan teknologi dengan ilmu biologi [7]", + "AKUL20: Saya tertarik ingin mengembangkan produk atau teknologi baru berdasarkan proses biologis [6]", + "AKUL21: Saya tertarik pada rekayasa genetika dan aplikasinya [2]", + "AKUL22: Saya tertarik ingin bekerja di laboratorium dengan peralatan canggih [7]", + "AKUL23: Saya tertarik untuk meneliti cara meningkatkan kualitas pangan melalui bioteknologi [6]", + "AKUL24: Saya tertarik ingin berkontribusi dalam pengembangan terapi gen atau obat-obatan baru [6]" + ], + "riasecType": [ + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "E" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 6 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 4, + 2 + ] + } + ], + "karir": [ + "Peneliti Bioteknologi [20]", + "Ahli Mikrobiologi [20]", + "Teknisi Laboratorium [20]", + "Peneliti Biologi Molekuler [20]", + "Peneliti Industri Pangan [20]", + "Ilmuwan Riset dan Pengembangan [20]" + ], + "karir_riasec": [ + "I", + "I", + "R/I", + "I", + "I/R", + "I" + ], + "jurusan_terkait": [ + "AJUR20: Bioteknologi", + "AJUR21: Teknologi Pangan", + "AJUR22: Mikrobiologi", + "AJUR23: Teknik Biomedis", + "AJUR24: Biologi Molekuler", + "AJUR25: Biologi (peminatan Genetika)" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Institut Pertanian Bogor (IPB)", + "Universitas Gadjah Mada (UGM)", + "Universitas Brawijaya (UB)", + "Universitas Airlangga (UNAIR)", + "Universitas Indonesia (UI)", + "Universitas Surabaya (UBAYA)" + ] + }, + "Lingkungan dan Konservasi": { + "pertanyaan": [ + "AKUL25: Saya tertarik peduli terhadap masalah lingkungan dan pelestarian alam [7]", + "AKUL26: Saya tertarik untuk mempelajari ekosistem dan keanekaragaman hayati Indonesia [6]", + "AKUL27: Saya tertarik ingin mengembangkan solusi untuk mengatasi polusi dan perubahan iklim [7]", + "AKUL28: Saya tertarik senang melakukan penelitian di lapangan/alam terbuka [6]", + "AKUL29: Saya tertarik untuk melindungi spesies terancam punah dan habitat alaminya [7]", + "AKUL30: Saya tertarik ingin mengedukasi masyarakat tentang pentingnya pelestarian lingkungan [7]" + ], + "riasecType": [ + { + "type": [ + "S", + "I" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 1 + ] + }, + { + "type": [ + "I", + "E" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "S", + "I" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "S", + "E" + ], + "bobot": [ + 2, + 2 + ] + } + ], + "karir": [ + "Ahli Konservasi [20]", + "Peneliti Lingkungan [20]", + "Konsultan AMDAL [20]", + "Spesialis Pengelolaan Limbah [20]", + "Peneliti Ekosistem [20]", + "Edukator Lingkungan [20]", + "Ahli Kehutanan [20]" + ], + "karir_riasec": [ + "I/S", + "I/R", + "I/E", + "R/C", + "I", + "S/E", + "I/R" + ], + "jurusan_terkait": [ + "AJUR26: Ilmu Lingkungan", + "AJUR27: Biologi (peminatan Konservasi)", + "AJUR28: Kehutanan", + "AJUR29: Teknik Lingkungan", + "AJUR30: Oseanografi", + "AJUR31: Meteorologi dan Geofisika" + ], + "universitas_rekomendasi": [ + "Institut Pertanian Bogor (IPB)", + "Universitas Gadjah Mada (UGM)", + "Universitas Indonesia (UI)", + "Universitas Diponegoro (UNDIP)", + "Universitas Hasanuddin (UNHAS)", + "Universitas Padjajaran (UNPAD)", + "Universitas Mulawarman (UNMUL)" + ] + }, + "Geosains": { + "pertanyaan": [ + "AKUL31: Saya tertarik mempelajari struktur dan proses yang terjadi di bumi [7]", + "AKUL32: Saya tertarik ingin memahami tentang gempa bumi, gunung berapi, dan bencana alam lainnya [6]", + "AKUL33: Saya tertarik suka mempelajari tentang batuan, mineral, dan sumber daya alam [7]", + "AKUL34: Saya tertarik pada eksplorasi sumber daya tambang dan energi [7]", + "AKUL35: Saya tertarik ingin bekerja di lapangan untuk menganalisis formasi geologis [7]", + "AKUL36: Saya tertarik pada pemetaan dan pemodelan fenomena geologi [7]" + ], + "riasecType": [ + { + "type": [ + "I" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 5, + 1 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 1 + ] + }, + { + "type": [ + "I", + "E" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "C" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "Ahli Geologi [20]", + "Ahli Vulkanologi [20]", + "Ahli Seismologi [20]", + "Ahli Hidrologi [20]", + "Geofisikawan [20]", + "Ahli Geoteknik [20]", + "Ahli Pertambangan [20]" + ], + "karir_riasec": [ + "I/R", + "I/R", + "I", + "I/R", + "I", + "I/R", + "R/I" + ], + "jurusan_terkait": [ + "AJUR32: Geologi", + "AJUR33: Geofisika", + "AJUR34: Teknik Perminyakan", + "AJUR35: Teknik Pertambangan", + "AJUR36: Oseanografi", + "AJUR37: Geodesi" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Gadjah Mada (UGM)", + "Universitas Indonesia (UI)", + "Universitas Padjadjaran (UNPAD)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Pembangunan Nasional Veteran Yogyakarta (UPN)", + "Universitas Hasanuddin (UNHAS)" + ] + }, + "Neurosains": { + "pertanyaan": [ + "AKUL37: Saya tertarik mempelajari bagaimana otak manusia bekerja [7]", + "AKUL38: Saya tertarik ingin memahami hubungan antara otak dan perilaku manusia [6]", + "AKUL39: Saya tertarik pada penelitian penyakit neurologis dan cara mengobatinya [7]", + "AKUL40: Saya tertarik suka mempelajari sistem saraf dan transmisi sinyal di otak [6]", + "AKUL41: Saya tertarik pada kecerdasan buatan dan bagaimana meniru fungsi otak [7]", + "AKUL42: Saya tertarik ingin membantu orang dengan gangguan neurologis atau psikiatri [6]" + ], + "riasecType": [ + { + "type": [ + "I" + ], + "bobot": [ + 6 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "I", + "A" + ], + "bobot": [ + 3, + 1 + ] + }, + { + "type": [ + "S", + "I" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "Peneliti di Bidang Neurologi [20]", + "Ahli Saraf (Neurolog) [20]", + "Teknisi Pemindaian Otak [20]", + "Farmakolog Klinis [20]", + "Psikiater [20]", + "Fisioterapis Spesialis Neurologis [20]" + ], + "karir_riasec": [ + "I", + "I/S", + "R/I", + "I", + "I/S", + "S/I" + ], + "jurusan_terkait": [ + "AJUR38: Kedokteran (spesialisasi Neurologi)", + "AJUR39: Psikologi", + "AJUR40: Biologi", + "AJUR41: Teknologi Medis", + "AJUR42: Ilmu Biomedis", + "AJUR43: Farmakologi" + ], + "universitas_rekomendasi": [ + "Universitas Indonesia (UI)", + "Universitas Gadjah Mada (UGM)", + "Universitas Airlangga (UNAIR)", + "Universitas Padjadjaran (UNPAD)", + "Institut Teknologi Bandung (ITB)", + "Universitas Brawijaya (UB)", + "Universitas Katolik Indonesia Atma Jaya" + ] + }, + "Teknik Biomedis": { + "pertanyaan": [ + "AKUL43: Saya tertarik menggabungkan ilmu teknik dengan medis [6]", + "AKUL44: Saya tertarik ingin merancang alat dan teknologi untuk diagnosis dan pengobatan [6]", + "AKUL45: Saya tertarik pada pengembangan organ buatan dan implan [6]", + "AKUL46: Saya tertarik suka mempelajari cara kerja peralatan medis canggih [6]", + "AKUL47: Saya tertarik pada pengolahan citra medis dan analisis data kesehatan [6]", + "AKUL48: Saya tertarik ingin berkontribusi dalam inovasi teknologi kesehatan [6]" + ], + "riasecType": [ + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "R", + "A" + ], + "bobot": [ + 2, + 2, + 1 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "C" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "E" + ], + "bobot": [ + 4, + 2 + ] + } + ], + "karir": [ + "Insinyur Biomedis [20]", + "Pengembang Alat Medis [20]", + "Radiografer [20]", + "Spesialis Teknik Elektromedis [20]", + "Peneliti Alat Kesehatan [20]", + "Teknisi Peralatan Medis [20]" + ], + "karir_riasec": [ + "I/R", + "I/R/A", + "I/R", + "I/E", + "I", + "R/I" + ], + "jurusan_terkait": [ + "AJUR44: Teknik Biomedis", + "AJUR45: Teknologi Medis", + "AJUR46: Fisika (peminatan Medis)", + "AJUR47: Teknik Elektro", + "AJUR48: Ilmu Komputer", + "AJUR49: Teknik Mesin" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Universitas Airlangga (UNAIR)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Gadjah Mada (UGM)", + "Universitas Diponegoro (UNDIP)", + "Swiss German University" + ] + } + }, + "semua_pertanyaan": [ + { + "kode_pertanyaan": "AKUL01", + "isi_pertanyaan": "Saya tertarik untuk mempelajari bagaimana tubuh manusia bekerja" + }, + { + "kode_pertanyaan": "AKUL02", + "isi_pertanyaan": "Saya tertarik ingin membantu orang lain untuk sembuh dari penyakit" + }, + { + "kode_pertanyaan": "AKUL03", + "isi_pertanyaan": "Saya tertarik mempelajari cara-cara medis untuk mendiagnosis dan mengobati penyakit" + }, + { + "kode_pertanyaan": "AKUL04", + "isi_pertanyaan": "Saya tertarik suka belajar tentang sistem organ tubuh dan bagaimana mereka berfungsi" + }, + { + "kode_pertanyaan": "AKUL05", + "isi_pertanyaan": "Saya tertarik untuk mengikuti perkembangan teknologi dalam dunia kedokteran" + }, + { + "kode_pertanyaan": "AKUL06", + "isi_pertanyaan": "Saya tertarik memiliki ketelitian tinggi dan mampu bekerja di bawah tekanan" + }, + { + "kode_pertanyaan": "AKUL07", + "isi_pertanyaan": "Saya tertarik untuk melakukan eksperimen ilmiah dan menemukan sesuatu yang baru" + }, + { + "kode_pertanyaan": "AKUL08", + "isi_pertanyaan": "Saya tertarik senang menganalisis data dan hasil percobaan" + }, + { + "kode_pertanyaan": "AKUL09", + "isi_pertanyaan": "Saya tertarik suka belajar tentang alam dan bagaimana semua itu bekerja" + }, + { + "kode_pertanyaan": "AKUL10", + "isi_pertanyaan": "Saya tertarik ingin memahami lebih dalam tentang dunia di sekitar Anda, seperti hewan, tumbuhan, atau planet" + }, + { + "kode_pertanyaan": "AKUL11", + "isi_pertanyaan": "Saya tertarik menikmati kegiatan praktikum di laboratorium" + }, + { + "kode_pertanyaan": "AKUL12", + "isi_pertanyaan": "Saya tertarik untuk memecahkan masalah lingkungan menggunakan pendekatan ilmiah" + }, + { + "kode_pertanyaan": "AKUL13", + "isi_pertanyaan": "Saya tertarik untuk mempelajari bagaimana obat bekerja dalam tubuh manusia" + }, + { + "kode_pertanyaan": "AKUL14", + "isi_pertanyaan": "Saya tertarik ingin membantu menciptakan obat-obatan yang dapat menyembuhkan penyakit" + }, + { + "kode_pertanyaan": "AKUL15", + "isi_pertanyaan": "Saya tertarik suka belajar tentang bahan kimia dan bagaimana mereka digunakan untuk tujuan medis" + }, + { + "kode_pertanyaan": "AKUL16", + "isi_pertanyaan": "Saya tertarik ingin tahu lebih banyak tentang bagaimana obat dikembangkan dan diuji" + }, + { + "kode_pertanyaan": "AKUL17", + "isi_pertanyaan": "Saya tertarik pada pembuatan dan pengembangan kosmetik dan produk perawatan tubuh" + }, + { + "kode_pertanyaan": "AKUL18", + "isi_pertanyaan": "Saya tertarik memiliki ketelitian tinggi dalam mengukur dan mencampur bahan-bahan kimia" + }, + { + "kode_pertanyaan": "AKUL19", + "isi_pertanyaan": "Saya tertarik untuk menggabungkan teknologi dengan ilmu biologi" + }, + { + "kode_pertanyaan": "AKUL20", + "isi_pertanyaan": "Saya tertarik ingin mengembangkan produk atau teknologi baru berdasarkan proses biologis" + }, + { + "kode_pertanyaan": "AKUL21", + "isi_pertanyaan": "Saya tertarik pada rekayasa genetika dan aplikasinya" + }, + { + "kode_pertanyaan": "AKUL22", + "isi_pertanyaan": "Saya tertarik ingin bekerja di laboratorium dengan peralatan canggih" + }, + { + "kode_pertanyaan": "AKUL23", + "isi_pertanyaan": "Saya tertarik untuk meneliti cara meningkatkan kualitas pangan melalui bioteknologi" + }, + { + "kode_pertanyaan": "AKUL24", + "isi_pertanyaan": "Saya tertarik ingin berkontribusi dalam pengembangan terapi gen atau obat-obatan baru" + }, + { + "kode_pertanyaan": "AKUL25", + "isi_pertanyaan": "Saya tertarik peduli terhadap masalah lingkungan dan pelestarian alam" + }, + { + "kode_pertanyaan": "AKUL26", + "isi_pertanyaan": "Saya tertarik untuk mempelajari ekosistem dan keanekaragaman hayati Indonesia" + }, + { + "kode_pertanyaan": "AKUL27", + "isi_pertanyaan": "Saya tertarik ingin mengembangkan solusi untuk mengatasi polusi dan perubahan iklim" + }, + { + "kode_pertanyaan": "AKUL28", + "isi_pertanyaan": "Saya tertarik senang melakukan penelitian di lapangan/alam terbuka" + }, + { + "kode_pertanyaan": "AKUL29", + "isi_pertanyaan": "Saya tertarik untuk melindungi spesies terancam punah dan habitat alaminya" + }, + { + "kode_pertanyaan": "AKUL30", + "isi_pertanyaan": "Saya tertarik ingin mengedukasi masyarakat tentang pentingnya pelestarian lingkungan" + }, + { + "kode_pertanyaan": "AKUL31", + "isi_pertanyaan": "Saya tertarik mempelajari struktur dan proses yang terjadi di bumi" + }, + { + "kode_pertanyaan": "AKUL32", + "isi_pertanyaan": "Saya tertarik ingin memahami tentang gempa bumi, gunung berapi, dan bencana alam lainnya" + }, + { + "kode_pertanyaan": "AKUL33", + "isi_pertanyaan": "Saya tertarik suka mempelajari tentang batuan, mineral, dan sumber daya alam" + }, + { + "kode_pertanyaan": "AKUL34", + "isi_pertanyaan": "Saya tertarik pada eksplorasi sumber daya tambang dan energi" + }, + { + "kode_pertanyaan": "AKUL35", + "isi_pertanyaan": "Saya tertarik ingin bekerja di lapangan untuk menganalisis formasi geologis" + }, + { + "kode_pertanyaan": "AKUL36", + "isi_pertanyaan": "Saya tertarik pada pemetaan dan pemodelan fenomena geologi" + }, + { + "kode_pertanyaan": "AKUL37", + "isi_pertanyaan": "Saya tertarik mempelajari bagaimana otak manusia bekerja" + }, + { + "kode_pertanyaan": "AKUL38", + "isi_pertanyaan": "Saya tertarik ingin memahami hubungan antara otak dan perilaku manusia" + }, + { + "kode_pertanyaan": "AKUL39", + "isi_pertanyaan": "Saya tertarik pada penelitian penyakit neurologis dan cara mengobatinya" + }, + { + "kode_pertanyaan": "AKUL40", + "isi_pertanyaan": "Saya tertarik suka mempelajari sistem saraf dan transmisi sinyal di otak" + }, + { + "kode_pertanyaan": "AKUL41", + "isi_pertanyaan": "Saya tertarik pada kecerdasan buatan dan bagaimana meniru fungsi otak" + }, + { + "kode_pertanyaan": "AKUL42", + "isi_pertanyaan": "Saya tertarik ingin membantu orang dengan gangguan neurologis atau psikiatri" + }, + { + "kode_pertanyaan": "AKUL43", + "isi_pertanyaan": "Saya tertarik menggabungkan ilmu teknik dengan medis" + }, + { + "kode_pertanyaan": "AKUL44", + "isi_pertanyaan": "Saya tertarik ingin merancang alat dan teknologi untuk diagnosis dan pengobatan" + }, + { + "kode_pertanyaan": "AKUL45", + "isi_pertanyaan": "Saya tertarik pada pengembangan organ buatan dan implan" + }, + { + "kode_pertanyaan": "AKUL46", + "isi_pertanyaan": "Saya tertarik suka mempelajari cara kerja peralatan medis canggih" + }, + { + "kode_pertanyaan": "AKUL47", + "isi_pertanyaan": "Saya tertarik pada pengolahan citra medis dan analisis data kesehatan" + }, + { + "kode_pertanyaan": "AKUL48", + "isi_pertanyaan": "Saya tertarik ingin berkontribusi dalam inovasi teknologi kesehatan" + } + ] + } +} \ No newline at end of file diff --git a/assets/ipa_teknik_kerja.json b/assets/ipa_teknik_kerja.json new file mode 100644 index 0000000..5f15e01 --- /dev/null +++ b/assets/ipa_teknik_kerja.json @@ -0,0 +1,1181 @@ +{ + "J01B-Kerja": { + "name": "IPA (Teknik) - Kerja", + "description": "Fokus: Matematika, Fisika, IT (arah teknik/teknologi) untuk pekerjaan langsung, bisa belajar mandiri melalui kursus, pelatihan, atau sertifikasi industri tanpa kuliah formal.", + "categories": [ + "Teknologi & IT", + "Manufaktur", + "Pekerjaan Teknik", + "Instalasi & Pemeliharaan" + ], + "riasec": { + "primary": "R (Realistic)", + "secondary": "I (Investigative)", + "tertiary": "C (Conventional)", + "code": "RIC" + }, + "minat": { + "Elektronika": { + "pertanyaan": [ + "BKER01: Saya tertarik untuk memperbaiki dan merakit perangkat elektronik tanpa kuliah formal [2]", + "BKER02: Saya tertarik ingin belajar tentang instalasi perangkat elektronik melalui kursus atau tutorial online [4]", + "BKER03: Saya tertarik untuk bekerja di industri yang mengembangkan teknologi elektronik [6]", + "BKER04: Saya tertarik ingin memecahkan masalah teknis dengan alat-alat elektronik [6]", + "BKER05: Saya tertarik suka membongkar dan mempelajari cara kerja perangkat elektronik [6]", + "BKER06: Saya tertarik untuk memodifikasi perangkat elektronik agar memiliki fungsi yang lebih baik [6]" + ], + "riasecType": [ + { + "type": [ + "R" + ], + "bobot": [ + 7 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "E" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I", + "A" + ], + "bobot": [ + 3, + 1, + 1 + ] + } + ], + "karir": [ + "BKAR01: Teknisi Elektronika [20]", + "BKAR02: Teknisi Perangkat Elektronik [20]", + "BKAR03: Teknisi Service HP/Laptop [20]", + "BKAR04: Teknisi Instalasi CCTV [20]", + "BKAR05: Assembler Perangkat Elektronik [20]", + "BKAR06: Teknisi Peralatan Audio-Visual [20]", + "BKAR07: Teknisi Telekomunikasi [20]", + "BKAR08: Operator Mesin Produksi [20]", + "BKAR09: Teknisi Maintenance Pabrik [20]", + "BKAR10: Petugas Lini Produksi [20]" + ], + "karir_riasec": [ + "R/I", + "R/I", + "R/C", + "R", + "R/C", + "R/I", + "R/I", + "I", + "I", + "I" + ], + "jurusan_terkait": [ + "D3 Teknik Elektronika", + "D3 Teknik Telekomunikasi", + "D3 Teknik Listrik", + "D3 Teknik Komputer", + "S1 Teknik Elektro", + "D3 Instrumentasi dan Kontrol Industri", + "D3 Mekatronika", + "D3 Teknik Otomasi", + "S1 Teknik Mesin" + ], + "rekomendasi_kursus": [ + "Sertifikasi Teknisi Elektronika Dasar", + "Kursus Perbaikan Smartphone dan Komputer", + "Pelatihan Instalasi Sistem Keamanan", + "Kursus Dasar-Dasar Elektronika Digital", + "Pelatihan Perakitan PCB" + ] + }, + "Mekatronika & Otomasi": { + "pertanyaan": [ + "BKER07: Saya tertarik untuk mengembangkan robot atau mesin otomatis dengan belajar mandiri [7]", + "BKER08: Saya tertarik ingin bekerja dengan sistem robot dan otomatisasi tanpa kuliah formal [7]", + "BKER09: Saya tertarik ingin mempelajari lebih lanjut tentang teknologi robotika melalui kursus online [6]", + "BKER10: Saya tertarik untuk memprogram robot dan membuatnya berfungsi sesuai keinginan [6]", + "BKER11: Saya tertarik pada integrasi sensor dan aktuator untuk sistem otomatis [5]", + "BKER12: Saya tertarik ingin bekerja di industri yang menggunakan robot dan sistem otomatisasi [6]" + ], + "riasecType": [ + { + "type": [ + "R", + "I", + "A" + ], + "bobot": [ + 3, + 3, + 1 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 3 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "E" + ], + "bobot": [ + 4, + 2 + ] + } + ], + "karir": [ + "BKAR11: Operator Mesin Produksi [18]", + "BKAR12: Teknisi Maintenance Pabrik [19]", + "BKAR13: Petugas Lini Produksi [17]" + ], + "karir_riasec": [ + "R", + "R/I", + "R/C" + ], + "jurusan_terkait": [ + "D3 Mekatronika", + "D3 Teknik Otomasi", + "S1 Teknik Elektro", + "D3 Teknik Elektronika", + "S1 Teknik Mesin", + "D3 Instrumentasi dan Kontrol Industri" + ], + "rekomendasi_kursus": [ + "Pelatihan Arduino dan Mikrokontroler", + "Sertifikasi Teknisi Otomasi Industri", + "Kursus Dasar Pemrograman Robot", + "Pelatihan IoT (Internet of Things)", + "Kursus Mekatronika Dasar" + ] + }, + "Programmer": { + "pertanyaan": [ + "BKER13: Saya tertarik untuk belajar pemrograman komputer dan membuat aplikasi secara mandiri [7]", + "BKER14: Saya tertarik ingin bekerja di bidang teknologi informasi tanpa kuliah [6]", + "BKER15: Saya tertarik suka memecahkan masalah logika dan mengembangkan algoritma [6]", + "BKER16: Saya tertarik untuk mengembangkan website atau aplikasi mobile [6]", + "BKER17: Saya tertarik tekun dan mau terus belajar teknologi baru secara mandiri [6]", + "BKER18: Saya tertarik dengan analisis data dan membuat visualisasi data [5]" + ], + "riasecType": [ + { + "type": [ + "I", + "A" + ], + "bobot": [ + 5, + 2 + ] + }, + { + "type": [ + "I", + "E" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "C" + ], + "bobot": [ + 5, + 1 + ] + }, + { + "type": [ + "I", + "A" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "C" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "A", + "C" + ], + "bobot": [ + 3, + 1, + 1 + ] + } + ], + "karir": [ + "BKAR14: Junior Web Developer [20]", + "BKAR15: Junior Mobile App Developer [20]", + "BKAR16: Front-End Developer [20]", + "BKAR17: Technical Support [20]", + "BKAR18: Content Creator Digital [20]", + "BKAR19: Freelance Programmer [20]", + "BKAR20: Teknisi Jaringan [20]", + "BKAR21: IT Support Specialist [20]", + "BKAR22: Network Technician [20]", + "BKAR23: Helpdesk Support [20]", + "BKAR24: Teknisi Instalasi Internet [20]" + ], + "karir_riasec": [ + "I/A", + "I/A", + "I/A", + "I/S", + "I/A", + "I/A/E", + "I", + "I", + "I", + "I", + "I" + ], + "jurusan_terkait": [ + "D3 Teknik Informatika", + "D3 Manajemen Informatika", + "S1 Teknik Informatika", + "S1 Ilmu Komputer", + "D3 Sistem Informasi", + "D3 Rekayasa Perangkat Lunak", + "D3 Teknik Komputer Jaringan", + "S1 Sistem Informasi", + "S1 Teknik Komputer", + "D3 Keamanan Sistem Informasi" + ], + "rekomendasi_kursus": [ + "Bootcamp Pemrograman Web/Mobile", + "Sertifikasi HTML, CSS, JavaScript", + "Pelatihan Framework Front-End (React, Vue)", + "Kursus Pemrograman Python/Java", + "Sertifikasi UI/UX Design", + "Kursus Database SQL" + ] + }, + "Jaringan & Infrastruktur IT": { + "pertanyaan": [ + "BKER19: Saya tertarik untuk mempelajari jaringan komputer dan infrastruktur IT [7]", + "BKER20: Saya tertarik ingin bekerja dengan server, router, dan perangkat jaringan lainnya [6]", + "BKER21: Saya tertarik suka mengonfigurasi dan memecahkan masalah koneksi jaringan [6]", + "BKER22: Saya tertarik untuk mempelajari keamanan jaringan dan sistem komputer [6]", + "BKER23: Saya tertarik ingin bekerja sebagai teknisi IT di perusahaan atau institusi [5]", + "BKER24: Saya tertarik untuk belajar cloud computing dan virtualisasi [5]" + ], + "riasecType": [ + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 3 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I", + "C" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "I", + "C" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "S" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "C" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "BKAR25: Teknisi Jaringan [20]", + "BKAR26: IT Support Specialist [19]", + "BKAR27: Network Technician [19]", + "BKAR28: Helpdesk Support [17]", + "BKAR29: Teknisi Instalasi Internet [18]" + ], + "karir_riasec": [ + "R/I", + "R/S/I", + "R/I", + "S/I", + "R/I" + ], + "jurusan_terkait": [ + "D3 Teknik Komputer Jaringan", + "D3 Teknik Informatika", + "S1 Sistem Informasi", + "D3 Manajemen Informatika", + "S1 Teknik Komputer", + "D3 Keamanan Sistem Informasi" + ], + "rekomendasi_kursus": [ + "Sertifikasi CompTIA A+", + "Sertifikasi CompTIA Network+", + "Kursus Cisco CCNA", + "Pelatihan Dasar Keamanan Siber", + "Kursus Windows Server Administration", + "Pelatihan Linux Fundamentals" + ] + }, + "Teknik Mesin": { + "pertanyaan": [ + "BKER25: Saya tertarik untuk bekerja dengan mesin dan perangkat mekanis tanpa gelar kuliah [7]", + "BKER26: Saya tertarik ingin mempelajari proses pembuatan dan perbaikan mesin melalui pelatihan atau kursus [6]", + "BKER27: Saya tertarik ingin bekerja di industri manufaktur dan perakitan mesin [6]", + "BKER28: Saya tertarik untuk memperbaiki mesin dan alat industri [6]", + "BKER29: Saya tertarik menyukai pekerjaan yang melibatkan alat-alat berat dan mesin industri [6]", + "BKER30: Saya tertarik untuk mempelajari desain dan pembacaan gambar teknik [5]" + ], + "riasecType": [ + { + "type": [ + "R" + ], + "bobot": [ + 7 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "C" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R" + ], + "bobot": [ + 6 + ] + }, + { + "type": [ + "R" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "R", + "C", + "A" + ], + "bobot": [ + 2, + 2, + 1 + ] + } + ], + "karir": [ + "BKAR30: Teknisi Mesin [20]", + "BKAR31: Operator Mesin CNC [20]", + "BKAR32: Teknisi Pemeliharaan Industri [20]", + "BKAR33: Teknisi Otomotif [20]", + "BKAR34: Operator Alat Berat [20]", + "BKAR35: Welder/Tukang Las [20]" + ], + "karir_riasec": [ + "R", + "R/C", + "R", + "R", + "R", + "R" + ], + "jurusan_terkait": [ + "D3 Teknik Mesin", + "D3 Teknik Manufaktur", + "D3 Otomotif", + "S1 Teknik Mesin", + "D3 Teknik Alat Berat", + "D3 Teknologi Produksi dan Manufaktur" + ], + "rekomendasi_kursus": [ + "Pelatihan Operator Mesin CNC", + "Sertifikasi Teknisi Mesin Industri", + "Kursus AutoCAD/SolidWorks", + "Pelatihan Pemeliharaan Mesin Industri", + "Sertifikasi Pengelasan (Welding)", + "Kursus Dasar Pneumatik dan Hidrolik" + ] + }, + "Teknik Sipil": { + "pertanyaan": [ + "BKER31: Saya tertarik untuk bekerja dalam perencanaan dan pengawasan proyek konstruksi tanpa kuliah [7]", + "BKER32: Saya tertarik ingin mempelajari dasar-dasar desain bangunan dan jalan melalui pelatihan [7]", + "BKER33: Saya tertarik ingin bekerja di lapangan untuk proyek konstruksi dan infrastruktur [6]", + "BKER34: Saya tertarik dengan analisis material dan struktur bangunan [5]", + "BKER35: Saya tertarik memiliki ketelitian dan kemampuan membaca gambar teknik [5]", + "BKER36: Saya tertarik dengan pekerjaan yang melibatkan pengukuran dan pemetaan lahan [6]" + ], + "riasecType": [ + { + "type": [ + "R", + "E" + ], + "bobot": [ + 4, + 3 + ] + }, + { + "type": [ + "R", + "I", + "A" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "R", + "E" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "C", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + } + ], + "karir": [ + "BKAR36: Pelaksana Lapangan Konstruksi [20]", + "BKAR37: Surveyor Lapangan [20]", + "BKAR38: Juru Gambar/Drafter [20]", + "BKAR39: Mandor Bangunan [20]", + "BKAR40: Tenaga Administrasi Proyek [20]" + ], + "karir_riasec": [ + "R/E", + "R/I", + "R/A/C", + "R/E", + "C/R" + ], + "jurusan_terkait": [ + "D3 Teknik Sipil", + "D3 Konstruksi Gedung", + "D3 Teknik Survey dan Pemetaan", + "S1 Teknik Sipil", + "D3 Arsitektur", + "D3 Konstruksi Sipil" + ], + "rekomendasi_kursus": [ + "Pelatihan AutoCAD untuk Konstruksi", + "Kursus Quantity Surveying", + "Pelatihan Pengawas Konstruksi", + "Sertifikasi Survey dan Pemetaan", + "Kursus Estimasi Biaya Konstruksi", + "Pelatihan Pengujian Bahan Konstruksi" + ] + }, + "Teknik Kimia": { + "pertanyaan": [ + "BKER37: Saya tertarik untuk bekerja di industri kimia tanpa gelar kuliah [7]", + "BKER38: Saya tertarik ingin memahami proses kimia industri melalui kursus atau pelatihan [3]", + "BKER39: Saya tertarik untuk bekerja dengan bahan kimia dan proses pembuatan produk kimia [6]", + "BKER40: Saya tertarik ingin berkarir di pengolahan limbah dan pemurnian air [6]", + "BKER41: Saya tertarik memiliki ketelitian dan disiplin dalam mengikuti prosedur operasi standar [6]", + "BKER42: Saya tertarik untuk mempelajari kontrol kualitas produk kimia [6]" + ], + "riasecType": [ + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 3 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "S" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "C", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "C" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "BKAR41: Operator Proses Kimia [20]", + "BKAR42: Petugas Pengecekan Produk [20]", + "BKAR43: Petugas Instalasi Pengolahan Limbah [20]", + "BKAR44: Operator Instalasi Pengolahan Air [20]", + "BKAR45: Petugas K3 Pabrik [20]" + ], + "karir_riasec": [ + "R/C", + "C/I", + "R/I", + "R", + "C/S" + ], + "jurusan_terkait": [ + "D3 Teknik Kimia", + "D3 Analis Kimia", + "S1 Teknik Kimia", + "D3 Teknik Lingkungan", + "D3 K3 (Keselamatan dan Kesehatan Kerja)", + "D3 Kimia Industri" + ], + "rekomendasi_kursus": [ + "Sertifikasi K3 Kimia", + "Pelatihan Operator Proses Kimia Industri", + "Kursus Teknik Laboratorium Kimia", + "Pelatihan Pengelolaan Limbah B3", + "Sertifikasi Good Manufacturing Practice (GMP)", + "Kursus Kontrol Kualitas Produk Kimia" + ] + }, + "Teknisi Energi": { + "pertanyaan": [ + "BKER43: Saya tertarik untuk bekerja di bidang pembangkit listrik dan distribusi energi [7]", + "BKER44: Saya tertarik ingin mempelajari instalasi dan pemeliharaan sistem energi terbarukan [7]", + "BKER45: Saya tertarik dengan sistem listrik dan elektronika daya [6]", + "BKER46: Saya tertarik ingin bekerja di lapangan untuk instalasi atau perbaikan sistem energi [6]", + "BKER47: Saya tertarik untuk mempelajari efisiensi energi dan konservasi [5]", + "BKER48: Saya tertarik memiliki ketelitian dan kesadaran keselamatan yang tinggi [6]" + ], + "riasecType": [ + { + "type": [ + "R", + "E" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "I", + "S" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "C", + "S" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "BKAR46: Teknisi Listrik [20]", + "BKAR47: Teknisi Panel Surya [20]", + "BKAR48: Operator Pembangkit Listrik [20]", + "BKAR49: Teknisi Instalasi Listrik [20]", + "BKAR50: Teknisi Pemeliharaan Sistem Energi [20]" + ], + "karir_riasec": [ + "R", + "R/I", + "R/C", + "R", + "R/C" + ], + "jurusan_terkait": [ + "D3 Teknik Listrik", + "D3 Teknik Elektro", + "D3 Teknik Energi Terbarukan", + "S1 Teknik Elektro", + "D3 Teknik Tenaga Listrik", + "D3 Instrumentasi dan Kontrol Industri" + ], + "rekomendasi_kursus": [ + "Sertifikasi Teknisi Listrik", + "Pelatihan Instalasi Panel Surya", + "Kursus Sistem Pembangkit Listrik", + "Pelatihan K3 Listrik", + "Kursus Instalasi Kabel Industri", + "Pelatihan Teknisi Energi Terbarukan" + ] + }, + "Teknik Otomotif": { + "pertanyaan": [ + "BKER49: Saya tertarik untuk memperbaiki dan merawat kendaraan bermotor [7]", + "BKER50: Saya tertarik ingin mempelajari sistem mekanik dan elektronik pada kendaraan modern [6]", + "BKER51: Saya tertarik dengan diagnosis dan troubleshooting kerusakan kendaraan [6]", + "BKER52: Saya tertarik suka bekerja dengan tangan dan alat-alat mekanik [7]", + "BKER53: Saya tertarik untuk mempelajari sistem kendaraan listrik dan hybrid [5]", + "BKER54: Saya tertarik ingin bekerja di bengkel atau industri otomotif [6]" + ], + "riasecType": [ + { + "type": [ + "R" + ], + "bobot": [ + 7 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "R" + ], + "bobot": [ + 5 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "E" + ], + "bobot": [ + 4, + 2 + ] + } + ], + "karir": [ + "BKAR51: Mekanik Otomotif [20]", + "BKAR52: Mekanik Sepeda Motor [20]", + "BKAR53: Teknisi Kelistrikan Otomotif [20]", + "BKAR54: Teknisi AC Mobil [20]", + "BKAR55: Service Advisor [20]", + "BKAR56: Petugas Bengkel Umum [20]" + ], + "karir_riasec": [ + "R", + "R", + "R/I", + "R", + "S/E/R", + "R" + ], + "jurusan_terkait": [ + "D3 Teknik Otomotif", + "D3 Teknik Mesin", + "S1 Teknik Otomotif", + "D3 Teknologi Kendaraan Ringan", + "D3 Mekatronika", + "D3 Alat Berat" + ], + "rekomendasi_kursus": [ + "Sertifikasi Mekanik Otomotif", + "Pelatihan Teknisi Mobil Listrik", + "Kursus Sistem Manajemen Mesin", + "Pelatihan Kelistrikan Otomotif", + "Kursus Diagnosis Komputer Kendaraan", + "Pelatihan Sistem Pengereman ABS" + ] + } + }, + "semua_pertanyaan": [ + { + "kode_pertanyaan": "BKER01", + "isi_pertanyaan": "Saya tertarik untuk memperbaiki dan merakit perangkat elektronik tanpa kuliah formal" + }, + { + "kode_pertanyaan": "BKER02", + "isi_pertanyaan": "Saya tertarik ingin belajar tentang instalasi perangkat elektronik melalui kursus atau tutorial online" + }, + { + "kode_pertanyaan": "BKER03", + "isi_pertanyaan": "Saya tertarik untuk bekerja di industri yang mengembangkan teknologi elektronik" + }, + { + "kode_pertanyaan": "BKER04", + "isi_pertanyaan": "Saya tertarik ingin memecahkan masalah teknis dengan alat-alat elektronik" + }, + { + "kode_pertanyaan": "BKER05", + "isi_pertanyaan": "Saya tertarik suka membongkar dan mempelajari cara kerja perangkat elektronik" + }, + { + "kode_pertanyaan": "BKER06", + "isi_pertanyaan": "Saya tertarik untuk memodifikasi perangkat elektronik agar memiliki fungsi yang lebih baik" + }, + { + "kode_pertanyaan": "BKER07", + "isi_pertanyaan": "Saya tertarik untuk mengembangkan robot atau mesin otomatis dengan belajar mandiri" + }, + { + "kode_pertanyaan": "BKER08", + "isi_pertanyaan": "Saya tertarik ingin bekerja dengan sistem robot dan otomatisasi tanpa kuliah formal" + }, + { + "kode_pertanyaan": "BKER09", + "isi_pertanyaan": "Saya tertarik ingin mempelajari lebih lanjut tentang teknologi robotika melalui kursus online" + }, + { + "kode_pertanyaan": "BKER10", + "isi_pertanyaan": "Saya tertarik untuk memprogram robot dan membuatnya berfungsi sesuai keinginan" + }, + { + "kode_pertanyaan": "BKER11", + "isi_pertanyaan": "Saya tertarik pada integrasi sensor dan aktuator untuk sistem otomatis" + }, + { + "kode_pertanyaan": "BKER12", + "isi_pertanyaan": "Saya tertarik ingin bekerja di industri yang menggunakan robot dan sistem otomatisasi" + }, + { + "kode_pertanyaan": "BKER13", + "isi_pertanyaan": "Saya tertarik untuk belajar pemrograman komputer dan membuat aplikasi secara mandiri" + }, + { + "kode_pertanyaan": "BKER14", + "isi_pertanyaan": "Saya tertarik ingin bekerja di bidang teknologi informasi tanpa kuliah" + }, + { + "kode_pertanyaan": "BKER15", + "isi_pertanyaan": "Saya tertarik suka memecahkan masalah logika dan mengembangkan algoritma" + }, + { + "kode_pertanyaan": "BKER16", + "isi_pertanyaan": "Saya tertarik untuk mengembangkan website atau aplikasi mobile" + }, + { + "kode_pertanyaan": "BKER17", + "isi_pertanyaan": "Saya tertarik tekun dan mau terus belajar teknologi baru secara mandiri" + }, + { + "kode_pertanyaan": "BKER18", + "isi_pertanyaan": "Saya tertarik dengan analisis data dan membuat visualisasi data" + }, + { + "kode_pertanyaan": "BKER19", + "isi_pertanyaan": "Saya tertarik untuk mempelajari jaringan komputer dan infrastruktur IT" + }, + { + "kode_pertanyaan": "BKER20", + "isi_pertanyaan": "Saya tertarik ingin bekerja dengan server, router, dan perangkat jaringan lainnya" + }, + { + "kode_pertanyaan": "BKER21", + "isi_pertanyaan": "Saya tertarik suka mengonfigurasi dan memecahkan masalah koneksi jaringan" + }, + { + "kode_pertanyaan": "BKER22", + "isi_pertanyaan": "Saya tertarik untuk mempelajari keamanan jaringan dan sistem komputer" + }, + { + "kode_pertanyaan": "BKER23", + "isi_pertanyaan": "Saya tertarik ingin bekerja sebagai teknisi IT di perusahaan atau institusi" + }, + { + "kode_pertanyaan": "BKER24", + "isi_pertanyaan": "Saya tertarik untuk belajar cloud computing dan virtualisasi" + }, + { + "kode_pertanyaan": "BKER25", + "isi_pertanyaan": "Saya tertarik untuk bekerja dengan mesin dan perangkat mekanis tanpa gelar kuliah" + }, + { + "kode_pertanyaan": "BKER26", + "isi_pertanyaan": "Saya tertarik ingin mempelajari proses pembuatan dan perbaikan mesin melalui pelatihan atau kursus" + }, + { + "kode_pertanyaan": "BKER27", + "isi_pertanyaan": "Saya tertarik ingin bekerja di industri manufaktur dan perakitan mesin" + }, + { + "kode_pertanyaan": "BKER28", + "isi_pertanyaan": "Saya tertarik untuk memperbaiki mesin dan alat industri" + }, + { + "kode_pertanyaan": "BKER29", + "isi_pertanyaan": "Saya tertarik menyukai pekerjaan yang melibatkan alat-alat berat dan mesin industri" + }, + { + "kode_pertanyaan": "BKER30", + "isi_pertanyaan": "Saya tertarik untuk mempelajari desain dan pembacaan gambar teknik" + }, + { + "kode_pertanyaan": "BKER31", + "isi_pertanyaan": "Saya tertarik untuk bekerja dalam perencanaan dan pengawasan proyek konstruksi tanpa kuliah" + }, + { + "kode_pertanyaan": "BKER32", + "isi_pertanyaan": "Saya tertarik ingin mempelajari dasar-dasar desain bangunan dan jalan melalui pelatihan" + }, + { + "kode_pertanyaan": "BKER33", + "isi_pertanyaan": "Saya tertarik ingin bekerja di lapangan untuk proyek konstruksi dan infrastruktur" + }, + { + "kode_pertanyaan": "BKER34", + "isi_pertanyaan": "Saya tertarik dengan analisis material dan struktur bangunan" + }, + { + "kode_pertanyaan": "BKER35", + "isi_pertanyaan": "Saya tertarik memiliki ketelitian dan kemampuan membaca gambar teknik" + }, + { + "kode_pertanyaan": "BKER36", + "isi_pertanyaan": "Saya tertarik dengan pekerjaan yang melibatkan pengukuran dan pemetaan lahan" + }, + { + "kode_pertanyaan": "BKER37", + "isi_pertanyaan": "Saya tertarik untuk bekerja di industri kimia tanpa gelar kuliah" + }, + { + "kode_pertanyaan": "BKER38", + "isi_pertanyaan": "Saya tertarik ingin memahami proses kimia industri melalui kursus atau pelatihan" + }, + { + "kode_pertanyaan": "BKER39", + "isi_pertanyaan": "Saya tertarik untuk bekerja dengan bahan kimia dan proses pembuatan produk kimia" + }, + { + "kode_pertanyaan": "BKER40", + "isi_pertanyaan": "Saya tertarik ingin berkarir di pengolahan limbah dan pemurnian air" + }, + { + "kode_pertanyaan": "BKER41", + "isi_pertanyaan": "Saya tertarik memiliki ketelitian dan disiplin dalam mengikuti prosedur operasi standar" + }, + { + "kode_pertanyaan": "BKER42", + "isi_pertanyaan": "Saya tertarik untuk mempelajari kontrol kualitas produk kimia" + }, + { + "kode_pertanyaan": "BKER43", + "isi_pertanyaan": "Saya tertarik untuk bekerja di bidang pembangkit listrik dan distribusi energi" + }, + { + "kode_pertanyaan": "BKER44", + "isi_pertanyaan": "Saya tertarik ingin mempelajari instalasi dan pemeliharaan sistem energi terbarukan" + }, + { + "kode_pertanyaan": "BKER45", + "isi_pertanyaan": "Saya tertarik dengan sistem listrik dan elektronika daya" + }, + { + "kode_pertanyaan": "BKER46", + "isi_pertanyaan": "Saya tertarik ingin bekerja di lapangan untuk instalasi atau perbaikan sistem energi" + }, + { + "kode_pertanyaan": "BKER47", + "isi_pertanyaan": "Saya tertarik untuk mempelajari efisiensi energi dan konservasi" + }, + { + "kode_pertanyaan": "BKER48", + "isi_pertanyaan": "Saya tertarik memiliki ketelitian dan kesadaran keselamatan yang tinggi" + }, + { + "kode_pertanyaan": "BKER49", + "isi_pertanyaan": "Saya tertarik untuk memperbaiki dan merawat kendaraan bermotor" + }, + { + "kode_pertanyaan": "BKER50", + "isi_pertanyaan": "Saya tertarik ingin mempelajari sistem mekanik dan elektronik pada kendaraan modern" + }, + { + "kode_pertanyaan": "BKER51", + "isi_pertanyaan": "Saya tertarik dengan diagnosis dan troubleshooting kerusakan kendaraan" + }, + { + "kode_pertanyaan": "BKER52", + "isi_pertanyaan": "Saya tertarik suka bekerja dengan tangan dan alat-alat mekanik" + }, + { + "kode_pertanyaan": "BKER53", + "isi_pertanyaan": "Saya tertarik untuk mempelajari sistem kendaraan listrik dan hybrid" + }, + { + "kode_pertanyaan": "BKER54", + "isi_pertanyaan": "Saya tertarik ingin bekerja di bengkel atau industri otomotif" + } + ] + } +} \ No newline at end of file diff --git a/assets/ipa_teknik_kuliah.json b/assets/ipa_teknik_kuliah.json new file mode 100644 index 0000000..aaa94ac --- /dev/null +++ b/assets/ipa_teknik_kuliah.json @@ -0,0 +1,1284 @@ +{ + "J01B": { + "name": "IPA (Teknik)", + "description": "Fokus: Matematika, Fisika, IT (arah teknik/teknologi) untuk persiapan kuliah di bidang teknik dan terapannya.", + "categories": [ + "Teknologi & IT", + "Rekayasa", + "Konstruksi", + "Manufaktur" + ], + "riasec": { + "primary": "R (Realistic)", + "secondary": "I (Investigative)", + "tertiary": "C (Conventional)", + "code": "RIC" + }, + "minat": { + "Elektronika": { + "pertanyaan": [ + "BKUL01: Saya tertarik dengan dunia elektronik dan rangkaian listrik [6]", + "BKUL02: Saya tertarik ingin belajar cara membuat perangkat elektronik sederhana [6]", + "BKUL03: Saya tertarik suka memecahkan masalah teknis dengan alat-alat elektronik [7]", + "BKUL04: Saya tertarik untuk mengembangkan teknologi elektronik untuk kehidupan sehari-hari [5]", + "BKUL05: Saya tertarik mempelajari sistem kendali dan otomasi berbasis elektronika [7]", + "BKUL06: Saya tertarik ingin tahu lebih dalam tentang sistem komputer dan mikroprosesor [6]" + ], + "riasecType": [ + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "I", + "R", + "E" + ], + "bobot": [ + 2, + 2, + 1 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "Insinyur Elektro [20]", + "Teknisi Elektronika [20]", + "R&D Specialist [20]", + "Insinyur Telekomunikasi [20]", + "Perancang Sistem Elektronik [20]", + "Insinyur Kontrol [20]", + "Konsultan Elektronika [20]" + ], + "karir_riasec": [ + "R/I", + "R/C", + "I/R", + "R/I", + "R/I/C", + "R/I", + "I/R/E" + ], + "jurusan_terkait": [ + "BJUR01: Teknik Elektro", + "BJUR02: Teknik Elektronika", + "BJUR03: Teknik Mekatronika", + "BJUR04: Teknik Telekomunikasi", + "BJUR05: Teknik Komputer", + "BJUR06: Elektronika Industri", + "BJUR07: Teknik Instrumentasi" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Gadjah Mada (UGM)", + "Universitas Diponegoro (UNDIP)", + "Politeknik Elektronika Negeri Surabaya (PENS)" + ] + }, + "Robotika": { + "pertanyaan": [ + "BKUL07: Saya tertarik untuk mengembangkan robot atau mesin otomatis [6]", + "BKUL08: Saya tertarik suka bekerja dengan teknologi yang menggabungkan perangkat keras dan perangkat lunak [6]", + "BKUL09: Saya tertarik dengan dunia sensor dan bagaimana mereka bekerja dalam robot [7]", + "BKUL10: Saya tertarik menikmati belajar cara membuat robot bergerak atau berinteraksi dengan lingkungannya [7]", + "BKUL11: Saya tertarik dengan perkembangan kecerdasan buatan dan implementasinya pada robot [6]", + "BKUL12: Saya tertarik ingin belajar tentang pengembangan sistem otomasi industri [7]" + ], + "riasecType": [ + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "R", + "I", + "C" + ], + "bobot": [ + 3, + 3, + 1 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I", + "C" + ], + "bobot": [ + 3, + 1, + 1 + ] + } + ], + "karir": [ + "Robotic Engineer [20]", + "Automation Engineer [20]", + "Teknisi Robotik [20]", + "AI & Robotics Researcher [20]", + "Insinyur Mekatronika [20]", + "Pengembang Sistem Otomasi Industri [20]", + "Konsultan Robotika [20]" + ], + "karir_riasec": [ + "R/I", + "R/I/C", + "R/C", + "I/R", + "R/I", + "R/I/C", + "I/R/E" + ], + "jurusan_terkait": [ + "BJUR08: Teknik Mekatronika", + "BJUR09: Teknik Elektro", + "BJUR10: Teknik Informatika (Konsentrasi Robotika)", + "BJUR11: Teknik Komputer", + "BJUR12: Teknik Otomasi", + "BJUR13: Teknik Sistem Kendali", + "BJUR14: Teknik Mesin" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Indonesia (UI)", + "Universitas Gadjah Mada (UGM)", + "Politeknik Elektronika Negeri Surabaya (PENS)", + "Universitas Telkom" + ] + }, + "Ilmu Komputer": { + "pertanyaan": [ + "BKUL13: Saya tertarik untuk membuat perangkat lunak atau aplikasi untuk komputer atau ponsel [7]", + "BKUL14: Saya tertarik suka bekerja dengan kode dan pengembangan teknologi [7]", + "BKUL15: Saya tertarik ingin memahami cara kerja jaringan komputer dan internet [7]", + "BKUL16: Saya tertarik menikmati tantangan dalam menciptakan solusi teknologi untuk masalah sehari-hari [7]", + "BKUL17: Saya tertarik mempelajari algoritma dan struktur data [7]", + "BKUL18: Saya tertarik ingin mengembangkan keterampilan dalam kecerdasan buatan dan pembelajaran mesin [7]" + ], + "riasecType": [ + { + "type": [ + "I", + "R", + "A" + ], + "bobot": [ + 3, + 2, + 2 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 3 + ] + }, + { + "type": [ + "I", + "R", + "C" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "I", + "E", + "R" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "I", + "C" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 2 + ] + } + ], + "karir": [ + "Software Engineer [20]", + "Data Scientist [20]", + "Machine Learning Engineer [20]", + "Web Developer [20]", + "Mobile App Developer [20]", + "Security Engineer [20]", + "DevOps Engineer [20]", + "Network Engineer [20]", + "System Administrator [20]", + "IT Infrastructure Manager [20]", + "Cloud Architect [20]", + "Security Specialist [20]", + "Database Administrator [20]", + "IT Consultant [20]" + ], + "karir_riasec": [ + "I/R", + "I/C", + "I/R", + "I/A/R", + "I/A/R", + "I/C/R", + "I/R/C", + "I", + "I", + "I", + "I", + "I", + "I", + "I" + ], + "jurusan_terkait": [ + "BJUR15: Teknik Informatika", + "BJUR16: Ilmu Komputer", + "BJUR17: Sistem Informasi", + "BJUR18: Sains Data", + "BJUR19: Kecerdasan Buatan", + "BJUR20: Rekayasa Perangkat Lunak", + "BJUR21: Teknologi Informasi", + "BJUR22: Teknik Komputer", + "BJUR23: Teknik Telekomunikasi", + "BJUR24: Keamanan Siber", + "BJUR25: Teknik Jaringan" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Gadjah Mada (UGM)", + "BINUS University", + "Universitas Brawijaya", + "Telkom University" + ] + }, + "Sistem & Infrastruktur IT": { + "pertanyaan": [ + "BKUL19: Saya tertarik untuk mengelola sistem komputer dan jaringan [6]", + "BKUL20: Saya tertarik ingin belajar tentang keamanan sistem dan jaringan [6]", + "BKUL21: Saya tertarik suka mengatur dan mengoptimalkan infrastruktur IT [5]", + "BKUL22: Saya tertarik dengan cloud computing dan virtualisasi [6]", + "BKUL23: Saya tertarik ingin mempelajari cara membangun dan mengatur server [5]", + "BKUL24: Saya tertarik dengan pengelolaan big data dan infrastrukturnya [5]" + ], + "riasecType": [ + { + "type": [ + "R", + "C", + "I" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "I", + "C", + "R" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "C", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "R", + "C" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "I", + "C", + "R" + ], + "bobot": [ + 2, + 2, + 1 + ] + } + ], + "karir": [ + "Network Engineer [19]", + "System Administrator [18]", + "IT Infrastructure Manager [19]", + "Cloud Architect [20]", + "Security Specialist [18]", + "Database Administrator [17]", + "IT Consultant [18]" + ], + "karir_riasec": [ + "R/C", + "C/R", + "C/R/E", + "I/R/C", + "I/C", + "C/I", + "I/E/C" + ], + "jurusan_terkait": [ + "BJUR26: Teknik Informatika", + "BJUR27: Sistem Informasi", + "BJUR28: Teknologi Informasi", + "BJUR29: Teknik Komputer", + "BJUR30: Teknik Telekomunikasi", + "BJUR31: Keamanan Siber", + "BJUR32: Teknik Jaringan" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "BINUS University", + "Universitas Gadjah Mada (UGM)", + "Universitas Brawijaya", + "Telkom University" + ] + }, + "Teknik Mesin": { + "pertanyaan": [ + "BKUL25: Saya tertarik dengan cara kerja mesin dan perangkat mekanis [6]", + "BKUL26: Saya tertarik ingin belajar tentang proses manufaktur dan pembuatan mesin [6]", + "BKUL27: Saya tertarik suka mempelajari cara benda-benda bergerak atau bekerja dalam kehidupan sehari-hari [5]", + "BKUL28: Saya tertarik untuk merancang alat atau mesin yang bisa membantu memecahkan masalah [6]", + "BKUL29: Saya tertarik dengan pengembangan sistem termal dan fluida [5]", + "BKUL30: Saya tertarik ingin mempelajari tentang desain dan analisis struktural mekanik [5]" + ], + "riasecType": [ + { + "type": [ + "R", + "I" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "R", + "I", + "C" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I", + "E" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "I", + "R" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "Insinyur Mesin [20]", + "Teknisi Mesin [20]", + "R&D Mechanical Engineer [20]", + "Insinyur Perancangan [20]", + "Insinyur Manufaktur [20]", + "Insinyur Otomotif [20]", + "Konsultan Teknik Mesin [20]" + ], + "karir_riasec": [ + "R/I", + "R/C", + "I/R", + "R/I/A", + "R/C/I", + "R/I", + "I/R/E" + ], + "jurusan_terkait": [ + "BJUR33: Teknik Mesin", + "BJUR34: Teknik Industri", + "BJUR35: Teknik Otomotif", + "BJUR36: Teknik Aeronautika", + "BJUR37: Teknik Material", + "BJUR38: Teknik Manufaktur", + "BJUR39: Teknik Perkapalan" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Gadjah Mada (UGM)", + "Universitas Diponegoro (UNDIP)", + "Universitas Brawijaya", + "Politeknik Manufaktur Bandung" + ] + }, + "Teknik Sipil": { + "pertanyaan": [ + "BKUL31: Saya tertarik dengan pembangunan dan perancangan bangunan [7]", + "BKUL32: Saya tertarik suka belajar tentang bagaimana gedung atau jembatan dibangun [7]", + "BKUL33: Saya tertarik untuk mempelajari cara bangunan menahan beban [7]", + "BKUL34: Saya tertarik menikmati mempelajari teknik dan cara-cara konstruksi dalam kehidupan sehari-hari [6]", + "BKUL35: Saya tertarik dengan perencanaan infrastruktur seperti jalan dan sistem transportasi [6]", + "BKUL36: Saya tertarik ingin belajar tentang manajemen sumber daya air dan penanganan bencana [6]" + ], + "riasecType": [ + { + "type": [ + "R", + "A", + "I" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "I", + "R", + "C" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + }, + { + "type": [ + "R", + "I", + "C" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "I", + "R", + "S" + ], + "bobot": [ + 2, + 2, + 1 + ] + } + ], + "karir": [ + "Insinyur Sipil [20]", + "Pengawas Konstruksi [20]", + "Perencana Infrastruktur [20]", + "Konsultan Struktur [20]", + "Project Manager Konstruksi [20]", + "Insinyur Geoteknik [20]", + "Urban Planner [20]" + ], + "karir_riasec": [ + "R/I", + "R/C", + "R/I/C", + "I/R", + "E/C/R", + "I/R", + "I/S/E" + ], + "jurusan_terkait": [ + "BJUR40: Teknik Sipil", + "BJUR41: Teknik Konstruksi", + "BJUR42: Teknik Transportasi", + "BJUR43: Teknik Pengairan", + "BJUR44: Teknik Lingkungan", + "BJUR45: Arsitektur", + "BJUR46: Perencanaan Wilayah dan Kota" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Universitas Gadjah Mada (UGM)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Diponegoro (UNDIP)", + "Universitas Brawijaya", + "Politeknik Negeri Jakarta" + ] + }, + "Teknik Kimia": { + "pertanyaan": [ + "BKUL37: Saya tertarik dengan reaksi kimia yang terjadi di sekitar kita [6]", + "BKUL38: Saya tertarik ingin memahami bagaimana bahan kimia digunakan dalam industri [6]", + "BKUL39: Saya tertarik untuk mempelajari cara proses kimia bekerja dalam skala industri [6]", + "BKUL40: Saya tertarik ingin tahu lebih banyak tentang pengolahan limbah atau kualitas air [7]", + "BKUL41: Saya tertarik mengembangkan produk berbasis kimia yang ramah lingkungan [7]", + "BKUL42: Saya tertarik ingin mempelajari teknologi pemrosesan minyak bumi dan gas alam [6]" + ], + "riasecType": [ + { + "type": [ + "I", + "R" + ], + "bobot": [ + 4, + 2 + ] + }, + { + "type": [ + "I", + "R", + "C" + ], + "bobot": [ + 3, + 1, + 1 + ] + }, + { + "type": [ + "R", + "I", + "C" + ], + "bobot": [ + 2, + 2, + 1 + ] + }, + { + "type": [ + "I", + "S", + "R" + ], + "bobot": [ + 2, + 2, + 1 + ] + }, + { + "type": [ + "I", + "R", + "S" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 2 + ] + } + ], + "karir": [ + "Insinyur Kimia [20]", + "Proses Engineer [20]", + "Ahli Lingkungan [20]", + "R&D Kimia [20]", + "Quality Control Specialist [20]", + "Insinyur Bioproses [20]", + "Konsultan Teknik Kimia [20]" + ], + "karir_riasec": [ + "I/R", + "R/I/C", + "I/S", + "I/R", + "C/I", + "I/R", + "I/E/C" + ], + "jurusan_terkait": [ + "BJUR47: Teknik Kimia", + "BJUR48: Teknik Lingkungan", + "BJUR49: Teknik Bioproses", + "BJUR50: Biokimia Terapan", + "BJUR51: Teknik Perminyakan", + "BJUR52: Teknik Material", + "BJUR53: Teknologi Pangan" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Gadjah Mada (UGM)", + "Universitas Diponegoro (UNDIP)", + "Universitas Brawijaya", + "Politeknik Negeri Malang" + ] + }, + "Teknik Industri": { + "pertanyaan": [ + "BKUL43: Saya tertarik pada optimasi sistem dan proses [6]", + "BKUL44: Saya tertarik suka menyelesaikan masalah yang melibatkan efisiensi dan produktivitas [7]", + "BKUL45: Saya tertarik dengan manajemen operasi dan rantai pasok [5]", + "BKUL46: Saya tertarik ingin mempelajari cara meningkatkan kualitas produk dan layanan [5]", + "BKUL47: Saya tertarik menyukai kombinasi antara teknik, bisnis, dan manajemen [6]", + "BKUL48: Saya tertarik ingin mempelajari cara mendesain sistem kerja yang ergonomis [5]" + ], + "riasecType": [ + { + "type": [ + "I", + "C", + "R" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "I", + "C", + "E" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "C", + "E", + "I" + ], + "bobot": [ + 2, + 2, + 1 + ] + }, + { + "type": [ + "I", + "E", + "C" + ], + "bobot": [ + 2, + 2, + 1 + ] + }, + { + "type": [ + "E", + "C", + "I" + ], + "bobot": [ + 3, + 2, + 1 + ] + }, + { + "type": [ + "I", + "R", + "S" + ], + "bobot": [ + 3, + 1, + 1 + ] + } + ], + "karir": [ + "Insinyur Industri [20]", + "Supply Chain Manager [20]", + "Quality Assurance Manager [20]", + "Konsultan Produktivitas [20]", + "Operation Manager [20]", + "Business Process Analyst [20]", + "Project Manager [20]" + ], + "karir_riasec": [ + "I/C/R", + "C/E", + "C/I", + "I/E/C", + "E/C", + "I/C/E", + "E/C/I" + ], + "jurusan_terkait": [ + "BJUR54: Teknik Industri", + "BJUR55: Manajemen Rekayasa", + "BJUR56: Teknik Logistik", + "BJUR57: Manajemen Operasi", + "BJUR58: Teknik Sistem", + "BJUR59: Teknik Manufaktur", + "BJUR60: Manajemen Kualitas" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Universitas Indonesia (UI)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Gadjah Mada (UGM)", + "Universitas Diponegoro (UNDIP)", + "BINUS University", + "Universitas Telkom" + ] + }, + "Teknik Energi": { + "pertanyaan": [ + "BKUL49: Saya tertarik dengan sumber energi terbarukan dan konservasi energi [7]", + "BKUL50: Saya tertarik ingin mempelajari sistem pembangkit listrik dan distribusi energi [7]", + "BKUL51: Saya tertarik peduli tentang efisiensi energi dan dampak lingkungan [7]", + "BKUL52: Saya tertarik dengan teknologi panel surya, turbin angin, atau bioenergi [7]", + "BKUL53: Saya tertarik ingin berkontribusi pada solusi energi masa depan [6]", + "BKUL54: Saya tertarik dengan manajemen energi di industri dan bangunan [7]" + ], + "riasecType": [ + { + "type": [ + "I", + "R", + "S" + ], + "bobot": [ + 2, + 2, + 2 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "S", + "I", + "R" + ], + "bobot": [ + 2, + 2, + 1 + ] + }, + { + "type": [ + "R", + "I" + ], + "bobot": [ + 3, + 3 + ] + }, + { + "type": [ + "I", + "S", + "E" + ], + "bobot": [ + 2, + 2, + 1 + ] + }, + { + "type": [ + "C", + "R", + "I" + ], + "bobot": [ + 2, + 2, + 1 + ] + } + ], + "karir": [ + "Insinyur Energi [20]", + "Renewable Energy Specialist [20]", + "Power System Engineer [20]", + "Energy Efficiency Consultant [20]", + "Insinyur Pembangkit Listrik [20]", + "Environmental Engineer [20]", + "Peneliti Energi Terbarukan [20]" + ], + "karir_riasec": [ + "R/I", + "I/R/S", + "R/I", + "I/S/C", + "R/I", + "I/S", + "I/R" + ], + "jurusan_terkait": [ + "BJUR61: Teknik Energi", + "BJUR62: Teknik Elektro (Konsentrasi Energi)", + "BJUR63: Energi Terbarukan", + "BJUR64: Teknik Fisika", + "BJUR65: Teknik Lingkungan", + "BJUR66: Teknik Perminyakan", + "BJUR67: Teknologi Energi" + ], + "universitas_rekomendasi": [ + "Institut Teknologi Bandung (ITB)", + "Institut Teknologi Sepuluh Nopember (ITS)", + "Universitas Indonesia (UI)", + "Universitas Gadjah Mada (UGM)", + "Universitas Diponegoro (UNDIP)", + "Politeknik Negeri Bandung", + "Universitas Pertamina" + ] + } + }, + "semua_pertanyaan": [ + { + "kode_pertanyaan": "BKUL01", + "isi_pertanyaan": "Saya tertarik dengan dunia elektronik dan rangkaian listrik" + }, + { + "kode_pertanyaan": "BKUL02", + "isi_pertanyaan": "Saya tertarik ingin belajar cara membuat perangkat elektronik sederhana" + }, + { + "kode_pertanyaan": "BKUL03", + "isi_pertanyaan": "Saya tertarik suka memecahkan masalah teknis dengan alat-alat elektronik" + }, + { + "kode_pertanyaan": "BKUL04", + "isi_pertanyaan": "Saya tertarik untuk mengembangkan teknologi elektronik untuk kehidupan sehari-hari" + }, + { + "kode_pertanyaan": "BKUL05", + "isi_pertanyaan": "Saya tertarik mempelajari sistem kendali dan otomasi berbasis elektronika" + }, + { + "kode_pertanyaan": "BKUL06", + "isi_pertanyaan": "Saya tertarik ingin tahu lebih dalam tentang sistem komputer dan mikroprosesor" + }, + { + "kode_pertanyaan": "BKUL07", + "isi_pertanyaan": "Saya tertarik untuk mengembangkan robot atau mesin otomatis" + }, + { + "kode_pertanyaan": "BKUL08", + "isi_pertanyaan": "Saya tertarik suka bekerja dengan teknologi yang menggabungkan perangkat keras dan perangkat lunak" + }, + { + "kode_pertanyaan": "BKUL09", + "isi_pertanyaan": "Saya tertarik dengan dunia sensor dan bagaimana mereka bekerja dalam robot" + }, + { + "kode_pertanyaan": "BKUL10", + "isi_pertanyaan": "Saya tertarik menikmati belajar cara membuat robot bergerak atau berinteraksi dengan lingkungannya" + }, + { + "kode_pertanyaan": "BKUL11", + "isi_pertanyaan": "Saya tertarik dengan perkembangan kecerdasan buatan dan implementasinya pada robot" + }, + { + "kode_pertanyaan": "BKUL12", + "isi_pertanyaan": "Saya tertarik ingin belajar tentang pengembangan sistem otomasi industri" + }, + { + "kode_pertanyaan": "BKUL13", + "isi_pertanyaan": "Saya tertarik untuk membuat perangkat lunak atau aplikasi untuk komputer atau ponsel" + }, + { + "kode_pertanyaan": "BKUL14", + "isi_pertanyaan": "Saya tertarik suka bekerja dengan kode dan pengembangan teknologi" + }, + { + "kode_pertanyaan": "BKUL15", + "isi_pertanyaan": "Saya tertarik ingin memahami cara kerja jaringan komputer dan internet" + }, + { + "kode_pertanyaan": "BKUL16", + "isi_pertanyaan": "Saya tertarik menikmati tantangan dalam menciptakan solusi teknologi untuk masalah sehari-hari" + }, + { + "kode_pertanyaan": "BKUL17", + "isi_pertanyaan": "Saya tertarik mempelajari algoritma dan struktur data" + }, + { + "kode_pertanyaan": "BKUL18", + "isi_pertanyaan": "Saya tertarik ingin mengembangkan keterampilan dalam kecerdasan buatan dan pembelajaran mesin" + }, + { + "kode_pertanyaan": "BKUL19", + "isi_pertanyaan": "Saya tertarik untuk mengelola sistem komputer dan jaringan" + }, + { + "kode_pertanyaan": "BKUL20", + "isi_pertanyaan": "Saya tertarik ingin belajar tentang keamanan sistem dan jaringan" + }, + { + "kode_pertanyaan": "BKUL21", + "isi_pertanyaan": "Saya tertarik suka mengatur dan mengoptimalkan infrastruktur IT" + }, + { + "kode_pertanyaan": "BKUL22", + "isi_pertanyaan": "Saya tertarik dengan cloud computing dan virtualisasi" + }, + { + "kode_pertanyaan": "BKUL23", + "isi_pertanyaan": "Saya tertarik ingin mempelajari cara membangun dan mengatur server" + }, + { + "kode_pertanyaan": "BKUL24", + "isi_pertanyaan": "Saya tertarik dengan pengelolaan big data dan infrastrukturnya" + }, + { + "kode_pertanyaan": "BKUL25", + "isi_pertanyaan": "Saya tertarik dengan cara kerja mesin dan perangkat mekanis" + }, + { + "kode_pertanyaan": "BKUL26", + "isi_pertanyaan": "Saya tertarik ingin belajar tentang proses manufaktur dan pembuatan mesin" + }, + { + "kode_pertanyaan": "BKUL27", + "isi_pertanyaan": "Saya tertarik suka mempelajari cara benda-benda bergerak atau bekerja dalam kehidupan sehari-hari" + }, + { + "kode_pertanyaan": "BKUL28", + "isi_pertanyaan": "Saya tertarik untuk merancang alat atau mesin yang bisa membantu memecahkan masalah" + }, + { + "kode_pertanyaan": "BKUL29", + "isi_pertanyaan": "Saya tertarik dengan pengembangan sistem termal dan fluida" + }, + { + "kode_pertanyaan": "BKUL30", + "isi_pertanyaan": "Saya tertarik ingin mempelajari tentang desain dan analisis struktural mekanik" + }, + { + "kode_pertanyaan": "BKUL31", + "isi_pertanyaan": "Saya tertarik dengan pembangunan dan perancangan bangunan" + }, + { + "kode_pertanyaan": "BKUL32", + "isi_pertanyaan": "Saya tertarik suka belajar tentang bagaimana gedung atau jembatan dibangun" + }, + { + "kode_pertanyaan": "BKUL33", + "isi_pertanyaan": "Saya tertarik untuk mempelajari cara bangunan menahan beban" + }, + { + "kode_pertanyaan": "BKUL34", + "isi_pertanyaan": "Saya tertarik menikmati mempelajari teknik dan cara-cara konstruksi dalam kehidupan sehari-hari" + }, + { + "kode_pertanyaan": "BKUL35", + "isi_pertanyaan": "Saya tertarik dengan perencanaan infrastruktur seperti jalan dan sistem transportasi" + }, + { + "kode_pertanyaan": "BKUL36", + "isi_pertanyaan": "Saya tertarik ingin belajar tentang manajemen sumber daya air dan penanganan bencana" + }, + { + "kode_pertanyaan": "BKUL37", + "isi_pertanyaan": "Saya tertarik dengan reaksi kimia yang terjadi di sekitar kita" + }, + { + "kode_pertanyaan": "BKUL38", + "isi_pertanyaan": "Saya tertarik ingin memahami bagaimana bahan kimia digunakan dalam industri" + }, + { + "kode_pertanyaan": "BKUL39", + "isi_pertanyaan": "Saya tertarik untuk mempelajari cara proses kimia bekerja dalam skala industri" + }, + { + "kode_pertanyaan": "BKUL40", + "isi_pertanyaan": "Saya tertarik ingin tahu lebih banyak tentang pengolahan limbah atau kualitas air" + }, + { + "kode_pertanyaan": "BKUL41", + "isi_pertanyaan": "Saya tertarik mengembangkan produk berbasis kimia yang ramah lingkungan" + }, + { + "kode_pertanyaan": "BKUL42", + "isi_pertanyaan": "Saya tertarik ingin mempelajari teknologi pemrosesan minyak bumi dan gas alam" + }, + { + "kode_pertanyaan": "BKUL43", + "isi_pertanyaan": "Saya tertarik pada optimasi sistem dan proses" + }, + { + "kode_pertanyaan": "BKUL44", + "isi_pertanyaan": "Saya tertarik suka menyelesaikan masalah yang melibatkan efisiensi dan produktivitas" + }, + { + "kode_pertanyaan": "BKUL45", + "isi_pertanyaan": "Saya tertarik dengan manajemen operasi dan rantai pasok" + }, + { + "kode_pertanyaan": "BKUL46", + "isi_pertanyaan": "Saya tertarik ingin mempelajari cara meningkatkan kualitas produk dan layanan" + }, + { + "kode_pertanyaan": "BKUL47", + "isi_pertanyaan": "Saya tertarik menyukai kombinasi antara teknik, bisnis, dan manajemen" + }, + { + "kode_pertanyaan": "BKUL48", + "isi_pertanyaan": "Saya tertarik ingin mempelajari cara mendesain sistem kerja yang ergonomis" + }, + { + "kode_pertanyaan": "BKUL49", + "isi_pertanyaan": "Saya tertarik dengan sumber energi terbarukan dan konservasi energi" + }, + { + "kode_pertanyaan": "BKUL50", + "isi_pertanyaan": "Saya tertarik ingin mempelajari sistem pembangkit listrik dan distribusi energi" + }, + { + "kode_pertanyaan": "BKUL51", + "isi_pertanyaan": "Saya tertarik peduli tentang efisiensi energi dan dampak lingkungan" + }, + { + "kode_pertanyaan": "BKUL52", + "isi_pertanyaan": "Saya tertarik dengan teknologi panel surya, turbin angin, atau bioenergi" + }, + { + "kode_pertanyaan": "BKUL53", + "isi_pertanyaan": "Saya tertarik ingin berkontribusi pada solusi energi masa depan" + }, + { + "kode_pertanyaan": "BKUL54", + "isi_pertanyaan": "Saya tertarik dengan manajemen energi di industri dan bangunan" + } + ] + } +} \ No newline at end of file diff --git a/assets/profile_dev.png b/assets/profile_dev.png new file mode 100644 index 0000000..c82793d Binary files /dev/null and b/assets/profile_dev.png differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..369521e --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"sirekomendasi-dc7de","appId":"1:1072369607324:android:f76a929d6f6258726f4359","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"sirekomendasi-dc7de","configurations":{"android":"1:1072369607324:android:f76a929d6f6258726f4359","ios":"1:1072369607324:ios:1d8cb1dbadb289ff6f4359","macos":"1:1072369607324:ios:1d8cb1dbadb289ff6f4359","web":"1:1072369607324:web:d17906f3452e68cb6f4359","windows":"1:1072369607324:web:7c9de102a40074a66f4359"}}},"ios":{"default":{"projectId":"sirekomendasi-dc7de","appId":"1:1072369607324:ios:1d8cb1dbadb289ff6f4359","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"sirekomendasi-dc7de","appId":"1:1072369607324:ios:1d8cb1dbadb289ff6f4359","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}}}}} \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/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/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/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 + 12.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..5fb3e95 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,59 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + pod 'Firebase/Analytics', :modular_headers => true + pod 'Firebase/Auth', :modular_headers => true + pod 'Firebase/Core', :modular_headers => true + pod 'Firebase/Firestore', :modular_headers => true + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + if target.name == 'BoringSSL-GRPC' + target.source_build_phase.files.each do |file| + if file.settings && file.settings['COMPILER_FLAGS'] + flags = file.settings['COMPILER_FLAGS'].split + flags.reject! { |flag| flag == '-GCC_WARN_INHIBIT_ALL_WARNINGS' } + file.settings['COMPILER_FLAGS'] = flags.join(' ') + end + end + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..716777c --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,1531 @@ +PODS: + - abseil/algorithm (1.20240116.2): + - abseil/algorithm/algorithm (= 1.20240116.2) + - abseil/algorithm/container (= 1.20240116.2) + - abseil/algorithm/algorithm (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/algorithm/container (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/nullability + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base (1.20240116.2): + - abseil/base/atomic_hook (= 1.20240116.2) + - abseil/base/base (= 1.20240116.2) + - abseil/base/base_internal (= 1.20240116.2) + - abseil/base/config (= 1.20240116.2) + - abseil/base/core_headers (= 1.20240116.2) + - abseil/base/cycleclock_internal (= 1.20240116.2) + - abseil/base/dynamic_annotations (= 1.20240116.2) + - abseil/base/endian (= 1.20240116.2) + - abseil/base/errno_saver (= 1.20240116.2) + - abseil/base/fast_type_id (= 1.20240116.2) + - abseil/base/log_severity (= 1.20240116.2) + - abseil/base/malloc_internal (= 1.20240116.2) + - abseil/base/no_destructor (= 1.20240116.2) + - abseil/base/nullability (= 1.20240116.2) + - abseil/base/prefetch (= 1.20240116.2) + - abseil/base/pretty_function (= 1.20240116.2) + - abseil/base/raw_logging_internal (= 1.20240116.2) + - abseil/base/spinlock_wait (= 1.20240116.2) + - abseil/base/strerror (= 1.20240116.2) + - abseil/base/throw_delegate (= 1.20240116.2) + - abseil/base/atomic_hook (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/base (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/cycleclock_internal + - abseil/base/dynamic_annotations + - abseil/base/log_severity + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/spinlock_wait + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/base_internal (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/config (1.20240116.2): + - abseil/xcprivacy + - abseil/base/core_headers (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/cycleclock_internal (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/xcprivacy + - abseil/base/dynamic_annotations (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/endian (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/xcprivacy + - abseil/base/errno_saver (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/fast_type_id (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/log_severity (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/malloc_internal (1.20240116.2): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/base/no_destructor (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/nullability (1.20240116.2): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/prefetch (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/pretty_function (1.20240116.2): + - abseil/xcprivacy + - abseil/base/raw_logging_internal (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/log_severity + - abseil/xcprivacy + - abseil/base/spinlock_wait (1.20240116.2): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/strerror (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/throw_delegate (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/cleanup/cleanup_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup_internal (1.20240116.2): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/common (1.20240116.2): + - abseil/meta/type_traits + - abseil/types/optional + - abseil/xcprivacy + - abseil/container/common_policy_traits (1.20240116.2): + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/compressed_tuple (1.20240116.2): + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/container_memory (1.20240116.2): + - abseil/base/config + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/fixed_array (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/flat_hash_map (1.20240116.2): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_function_defaults + - abseil/container/raw_hash_map + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/flat_hash_set (1.20240116.2): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_function_defaults + - abseil/container/raw_hash_set + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/hash_function_defaults (1.20240116.2): + - abseil/base/config + - abseil/hash/hash + - abseil/strings/cord + - abseil/strings/strings + - abseil/xcprivacy + - abseil/container/hash_policy_traits (1.20240116.2): + - abseil/container/common_policy_traits + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/hashtable_debug_hooks (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/container/hashtablez_sampler (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/memory/memory + - abseil/profiling/exponential_biased + - abseil/profiling/sample_recorder + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/inlined_vector (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/inlined_vector_internal + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/inlined_vector_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/span + - abseil/xcprivacy + - abseil/container/layout (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/demangle_internal + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/raw_hash_map (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/container_memory + - abseil/container/raw_hash_set + - abseil/xcprivacy + - abseil/container/raw_hash_set (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/container/common + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/hash_policy_traits + - abseil/container/hashtable_debug_hooks + - abseil/container/hashtablez_sampler + - abseil/hash/hash + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/crc/cpu_detect (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/crc32c (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/crc/cpu_detect + - abseil/crc/crc_internal + - abseil/crc/non_temporal_memcpy + - abseil/strings/str_format + - abseil/strings/strings + - abseil/xcprivacy + - abseil/crc/crc_cord_state (1.20240116.2): + - abseil/base/config + - abseil/crc/crc32c + - abseil/numeric/bits + - abseil/strings/strings + - abseil/xcprivacy + - abseil/crc/crc_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/crc/cpu_detect + - abseil/memory/memory + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/crc/non_temporal_arm_intrinsics (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/non_temporal_memcpy (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/crc/non_temporal_arm_intrinsics + - abseil/xcprivacy + - abseil/debugging/debugging_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/errno_saver + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/debugging/demangle_internal (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/debugging/examine_stack (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/xcprivacy + - abseil/debugging/stacktrace (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/xcprivacy + - abseil/debugging/symbolize (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/debugging/demangle_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/commandlineflag (1.20240116.2): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/commandlineflag_internal (1.20240116.2): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/xcprivacy + - abseil/flags/config (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/flags/program_name + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/flag (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/config + - abseil/flags/flag_internal + - abseil/flags/reflection + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/flag_internal (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/marshalling + - abseil/flags/reflection + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/utility/utility + - abseil/xcprivacy + - abseil/flags/marshalling (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/numeric/int128 + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/path_util (1.20240116.2): + - abseil/base/config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/private_handle_accessor (1.20240116.2): + - abseil/base/config + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/program_name (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/reflection (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/container/flat_hash_map + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/private_handle_accessor + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/functional/any_invocable (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/bind_front (1.20240116.2): + - abseil/base/base_internal + - abseil/container/compressed_tuple + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/function_ref (1.20240116.2): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/functional/any_invocable + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/hash/city (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/xcprivacy + - abseil/hash/hash (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/container/fixed_array + - abseil/functional/function_ref + - abseil/hash/city + - abseil/hash/low_level_hash + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/types/optional + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/hash/low_level_hash (1.20240116.2): + - abseil/base/config + - abseil/base/endian + - abseil/base/prefetch + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/log/absl_check (1.20240116.2): + - abseil/log/internal/check_impl + - abseil/xcprivacy + - abseil/log/absl_log (1.20240116.2): + - abseil/log/internal/log_impl + - abseil/xcprivacy + - abseil/log/absl_vlog_is_on (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/log/internal/vlog_config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/check (1.20240116.2): + - abseil/log/internal/check_impl + - abseil/log/internal/check_op + - abseil/log/internal/conditions + - abseil/log/internal/log_message + - abseil/log/internal/strip + - abseil/xcprivacy + - abseil/log/globals (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/hash/hash + - abseil/log/internal/vlog_config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/append_truncated (1.20240116.2): + - abseil/base/config + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/check_impl (1.20240116.2): + - abseil/base/core_headers + - abseil/log/internal/check_op + - abseil/log/internal/conditions + - abseil/log/internal/log_message + - abseil/log/internal/strip + - abseil/xcprivacy + - abseil/log/internal/check_op (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/log/internal/nullguard + - abseil/log/internal/nullstream + - abseil/log/internal/strip + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/conditions (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/log/internal/voidify + - abseil/xcprivacy + - abseil/log/internal/config (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/log/internal/fnmatch (1.20240116.2): + - abseil/base/config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/format (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/log/internal/append_truncated + - abseil/log/internal/config + - abseil/log/internal/globals + - abseil/strings/str_format + - abseil/strings/strings + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/globals (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/strings/strings + - abseil/time/time + - abseil/xcprivacy + - abseil/log/internal/log_impl (1.20240116.2): + - abseil/log/absl_vlog_is_on + - abseil/log/internal/conditions + - abseil/log/internal/log_message + - abseil/log/internal/strip + - abseil/xcprivacy + - abseil/log/internal/log_message (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/base/strerror + - abseil/container/inlined_vector + - abseil/debugging/examine_stack + - abseil/log/globals + - abseil/log/internal/append_truncated + - abseil/log/internal/format + - abseil/log/internal/globals + - abseil/log/internal/log_sink_set + - abseil/log/internal/nullguard + - abseil/log/internal/proto + - abseil/log/log_entry + - abseil/log/log_sink + - abseil/log/log_sink_registry + - abseil/memory/memory + - abseil/strings/strings + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/log_sink_set (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/no_destructor + - abseil/base/raw_logging_internal + - abseil/cleanup/cleanup + - abseil/log/globals + - abseil/log/internal/config + - abseil/log/internal/globals + - abseil/log/log_entry + - abseil/log/log_sink + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/nullguard (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/log/internal/nullstream (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/strings/strings + - abseil/xcprivacy + - abseil/log/internal/proto (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/log/internal/strip (1.20240116.2): + - abseil/base/log_severity + - abseil/log/internal/log_message + - abseil/log/internal/nullstream + - abseil/xcprivacy + - abseil/log/internal/vlog_config (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/log/internal/fnmatch + - abseil/memory/memory + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/types/optional + - abseil/xcprivacy + - abseil/log/internal/voidify (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/log/log (1.20240116.2): + - abseil/log/internal/log_impl + - abseil/log/vlog_is_on + - abseil/xcprivacy + - abseil/log/log_entry (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/log/internal/config + - abseil/strings/strings + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/log/log_sink (1.20240116.2): + - abseil/base/config + - abseil/log/log_entry + - abseil/xcprivacy + - abseil/log/log_sink_registry (1.20240116.2): + - abseil/base/config + - abseil/log/internal/log_sink_set + - abseil/log/log_sink + - abseil/xcprivacy + - abseil/log/vlog_is_on (1.20240116.2): + - abseil/log/absl_vlog_is_on + - abseil/xcprivacy + - abseil/memory (1.20240116.2): + - abseil/memory/memory (= 1.20240116.2) + - abseil/memory/memory (1.20240116.2): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/meta (1.20240116.2): + - abseil/meta/type_traits (= 1.20240116.2) + - abseil/meta/type_traits (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/bits (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/int128 (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/numeric/representation (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/profiling/exponential_biased (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/profiling/sample_recorder (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/xcprivacy + - abseil/random/bit_gen_ref (1.20240116.2): + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/random + - abseil/xcprivacy + - abseil/random/distributions (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/internal/fastmath + - abseil/random/internal/generate_real + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/traits + - abseil/random/internal/uniform_helper + - abseil/random/internal/wide_multiply + - abseil/strings/strings + - abseil/xcprivacy + - abseil/random/internal/distribution_caller (1.20240116.2): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/utility/utility + - abseil/xcprivacy + - abseil/random/internal/fast_uniform_bits (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/fastmath (1.20240116.2): + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/random/internal/generate_real (1.20240116.2): + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/fastmath + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/iostream_state_saver (1.20240116.2): + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/nonsecure_base (1.20240116.2): + - abseil/base/core_headers + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/pcg_engine (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/fastmath + - abseil/random/internal/iostream_state_saver + - abseil/xcprivacy + - abseil/random/internal/platform (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/internal/pool_urbg (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/random/internal/randen + - abseil/random/internal/seed_material + - abseil/random/internal/traits + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/randen (1.20240116.2): + - abseil/base/raw_logging_internal + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes + - abseil/random/internal/randen_slow + - abseil/xcprivacy + - abseil/random/internal/randen_engine (1.20240116.2): + - abseil/base/endian + - abseil/meta/type_traits + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/randen + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes (1.20240116.2): + - abseil/base/config + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes_impl + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes_impl (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/randen_slow (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/salted_seed_seq (1.20240116.2): + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/seed_material + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/seed_material (1.20240116.2): + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/random/internal/fast_uniform_bits + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/traits (1.20240116.2): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/uniform_helper (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/wide_multiply (1.20240116.2): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/random (1.20240116.2): + - abseil/random/distributions + - abseil/random/internal/nonsecure_base + - abseil/random/internal/pcg_engine + - abseil/random/internal/pool_urbg + - abseil/random/internal/randen_engine + - abseil/random/seed_sequences + - abseil/xcprivacy + - abseil/random/seed_gen_exception (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/seed_sequences (1.20240116.2): + - abseil/base/config + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/xcprivacy + - abseil/status/status (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/strerror + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/functional/function_ref + - abseil/memory/memory + - abseil/strings/cord + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/status/statusor (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/status/status + - abseil/strings/has_ostream_operator + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/charset (1.20240116.2): + - abseil/base/core_headers + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/strings/cord (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/crc/crc32c + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_info + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_scope + - abseil/strings/cordz_update_tracker + - abseil/strings/internal + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cord_internal (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/inlined_vector + - abseil/container/layout + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_functions (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/profiling/exponential_biased + - abseil/xcprivacy + - abseil/strings/cordz_handle (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/strings/cordz_info (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_handle + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_tracker + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_statistics (1.20240116.2): + - abseil/base/config + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_scope (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/cord_internal + - abseil/strings/cordz_info + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_tracker (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/has_ostream_operator (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/strings/str_format (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/strings/str_format_internal + - abseil/strings/string_view + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/str_format_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/container/fixed_array + - abseil/container/inlined_vector + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/numeric/representation + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/string_view (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/xcprivacy + - abseil/strings/strings (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/charset + - abseil/strings/internal + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/synchronization/graphcycles_internal (1.20240116.2): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/synchronization/kernel_timeout_internal (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/synchronization/synchronization (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/synchronization/graphcycles_internal + - abseil/synchronization/kernel_timeout_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/time (1.20240116.2): + - abseil/time/internal (= 1.20240116.2) + - abseil/time/time (= 1.20240116.2) + - abseil/time/internal (1.20240116.2): + - abseil/time/internal/cctz (= 1.20240116.2) + - abseil/time/internal/cctz (1.20240116.2): + - abseil/time/internal/cctz/civil_time (= 1.20240116.2) + - abseil/time/internal/cctz/time_zone (= 1.20240116.2) + - abseil/time/internal/cctz/civil_time (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/time/internal/cctz/time_zone (1.20240116.2): + - abseil/base/config + - abseil/time/internal/cctz/civil_time + - abseil/xcprivacy + - abseil/time/time (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/time/internal/cctz/civil_time + - abseil/time/internal/cctz/time_zone + - abseil/types/optional + - abseil/xcprivacy + - abseil/types (1.20240116.2): + - abseil/types/any (= 1.20240116.2) + - abseil/types/bad_any_cast (= 1.20240116.2) + - abseil/types/bad_any_cast_impl (= 1.20240116.2) + - abseil/types/bad_optional_access (= 1.20240116.2) + - abseil/types/bad_variant_access (= 1.20240116.2) + - abseil/types/compare (= 1.20240116.2) + - abseil/types/optional (= 1.20240116.2) + - abseil/types/span (= 1.20240116.2) + - abseil/types/variant (= 1.20240116.2) + - abseil/types/any (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/types/bad_any_cast + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/bad_any_cast (1.20240116.2): + - abseil/base/config + - abseil/types/bad_any_cast_impl + - abseil/xcprivacy + - abseil/types/bad_any_cast_impl (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_optional_access (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_variant_access (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/compare (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/optional (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/bad_optional_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/span (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/variant (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/types/bad_variant_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/utility/utility (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/xcprivacy (1.20240116.2) + - AppAuth (1.7.6): + - AppAuth/Core (= 1.7.6) + - AppAuth/ExternalUserAgent (= 1.7.6) + - AppAuth/Core (1.7.6) + - AppAuth/ExternalUserAgent (1.7.6): + - AppAuth/Core + - BoringSSL-GRPC (0.0.36): + - BoringSSL-GRPC/Implementation (= 0.0.36) + - BoringSSL-GRPC/Interface (= 0.0.36) + - BoringSSL-GRPC/Implementation (0.0.36): + - BoringSSL-GRPC/Interface (= 0.0.36) + - BoringSSL-GRPC/Interface (0.0.36) + - cloud_firestore (5.6.4): + - Firebase/Firestore (= 11.8.0) + - firebase_core + - Flutter + - Firebase/Analytics (11.8.0): + - Firebase/Core + - Firebase/Auth (11.8.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 11.8.0) + - Firebase/Core (11.8.0): + - Firebase/CoreOnly + - FirebaseAnalytics (~> 11.8.0) + - Firebase/CoreOnly (11.8.0): + - FirebaseCore (~> 11.8.0) + - Firebase/Firestore (11.8.0): + - Firebase/CoreOnly + - FirebaseFirestore (~> 11.8.0) + - firebase_auth (5.5.0): + - Firebase/Auth (= 11.8.0) + - firebase_core + - Flutter + - firebase_core (3.12.0): + - Firebase/CoreOnly (= 11.8.0) + - Flutter + - FirebaseAnalytics (11.8.0): + - FirebaseAnalytics/AdIdSupport (= 11.8.0) + - FirebaseCore (~> 11.8.0) + - FirebaseInstallations (~> 11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - FirebaseAnalytics/AdIdSupport (11.8.0): + - FirebaseCore (~> 11.8.0) + - FirebaseInstallations (~> 11.0) + - GoogleAppMeasurement (= 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - FirebaseAppCheckInterop (11.9.0) + - FirebaseAuth (11.8.1): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.8.0) + - FirebaseCoreExtension (~> 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GTMSessionFetcher/Core (< 5.0, >= 3.4) + - RecaptchaInterop (~> 100.0) + - FirebaseAuthInterop (11.9.0) + - FirebaseCore (11.8.1): + - FirebaseCoreInternal (~> 11.8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.8.0): + - FirebaseCore (~> 11.8.0) + - FirebaseCoreInternal (11.8.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseFirestore (11.8.0): + - FirebaseCore (~> 11.8.0) + - FirebaseCoreExtension (~> 11.8.0) + - FirebaseFirestoreInternal (= 11.8.0) + - FirebaseSharedSwift (~> 11.0) + - FirebaseFirestoreInternal (11.8.0): + - abseil/algorithm (~> 1.20240116.1) + - abseil/base (~> 1.20240116.1) + - abseil/container/flat_hash_map (~> 1.20240116.1) + - abseil/memory (~> 1.20240116.1) + - abseil/meta (~> 1.20240116.1) + - abseil/strings/strings (~> 1.20240116.1) + - abseil/time (~> 1.20240116.1) + - abseil/types (~> 1.20240116.1) + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseCore (~> 11.8.0) + - "gRPC-C++ (~> 1.65.0)" + - gRPC-Core (~> 1.65.0) + - leveldb-library (~> 1.22) + - nanopb (~> 3.30910.0) + - FirebaseInstallations (11.8.0): + - FirebaseCore (~> 11.8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - FirebaseSharedSwift (11.9.0) + - Flutter (1.0.0) + - google_sign_in_ios (0.0.1): + - AppAuth (>= 1.7.4) + - Flutter + - FlutterMacOS + - GoogleSignIn (~> 7.1) + - GTMSessionFetcher (>= 3.4.0) + - GoogleAppMeasurement (11.8.0): + - GoogleAppMeasurement/AdIdSupport (= 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/AdIdSupport (11.8.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 11.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/WithoutAdIdSupport (11.8.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleSignIn (7.1.0): + - AppAuth (< 2.0, >= 1.7.3) + - GTMAppAuth (< 5.0, >= 4.1.1) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - "gRPC-C++ (1.65.5)": + - "gRPC-C++/Implementation (= 1.65.5)" + - "gRPC-C++/Interface (= 1.65.5)" + - "gRPC-C++/Implementation (1.65.5)": + - abseil/algorithm/container (~> 1.20240116.2) + - abseil/base/base (~> 1.20240116.2) + - abseil/base/config (~> 1.20240116.2) + - abseil/base/core_headers (~> 1.20240116.2) + - abseil/base/log_severity (~> 1.20240116.2) + - abseil/base/no_destructor (~> 1.20240116.2) + - abseil/cleanup/cleanup (~> 1.20240116.2) + - abseil/container/flat_hash_map (~> 1.20240116.2) + - abseil/container/flat_hash_set (~> 1.20240116.2) + - abseil/container/inlined_vector (~> 1.20240116.2) + - abseil/flags/flag (~> 1.20240116.2) + - abseil/flags/marshalling (~> 1.20240116.2) + - abseil/functional/any_invocable (~> 1.20240116.2) + - abseil/functional/bind_front (~> 1.20240116.2) + - abseil/functional/function_ref (~> 1.20240116.2) + - abseil/hash/hash (~> 1.20240116.2) + - abseil/log/absl_check (~> 1.20240116.2) + - abseil/log/absl_log (~> 1.20240116.2) + - abseil/log/check (~> 1.20240116.2) + - abseil/log/globals (~> 1.20240116.2) + - abseil/log/log (~> 1.20240116.2) + - abseil/memory/memory (~> 1.20240116.2) + - abseil/meta/type_traits (~> 1.20240116.2) + - abseil/random/bit_gen_ref (~> 1.20240116.2) + - abseil/random/distributions (~> 1.20240116.2) + - abseil/random/random (~> 1.20240116.2) + - abseil/status/status (~> 1.20240116.2) + - abseil/status/statusor (~> 1.20240116.2) + - abseil/strings/cord (~> 1.20240116.2) + - abseil/strings/str_format (~> 1.20240116.2) + - abseil/strings/strings (~> 1.20240116.2) + - abseil/synchronization/synchronization (~> 1.20240116.2) + - abseil/time/time (~> 1.20240116.2) + - abseil/types/optional (~> 1.20240116.2) + - abseil/types/span (~> 1.20240116.2) + - abseil/types/variant (~> 1.20240116.2) + - abseil/utility/utility (~> 1.20240116.2) + - "gRPC-C++/Interface (= 1.65.5)" + - "gRPC-C++/Privacy (= 1.65.5)" + - gRPC-Core (= 1.65.5) + - "gRPC-C++/Interface (1.65.5)" + - "gRPC-C++/Privacy (1.65.5)" + - gRPC-Core (1.65.5): + - gRPC-Core/Implementation (= 1.65.5) + - gRPC-Core/Interface (= 1.65.5) + - gRPC-Core/Implementation (1.65.5): + - abseil/algorithm/container (~> 1.20240116.2) + - abseil/base/base (~> 1.20240116.2) + - abseil/base/config (~> 1.20240116.2) + - abseil/base/core_headers (~> 1.20240116.2) + - abseil/base/log_severity (~> 1.20240116.2) + - abseil/base/no_destructor (~> 1.20240116.2) + - abseil/cleanup/cleanup (~> 1.20240116.2) + - abseil/container/flat_hash_map (~> 1.20240116.2) + - abseil/container/flat_hash_set (~> 1.20240116.2) + - abseil/container/inlined_vector (~> 1.20240116.2) + - abseil/flags/flag (~> 1.20240116.2) + - abseil/flags/marshalling (~> 1.20240116.2) + - abseil/functional/any_invocable (~> 1.20240116.2) + - abseil/functional/bind_front (~> 1.20240116.2) + - abseil/functional/function_ref (~> 1.20240116.2) + - abseil/hash/hash (~> 1.20240116.2) + - abseil/log/check (~> 1.20240116.2) + - abseil/log/globals (~> 1.20240116.2) + - abseil/log/log (~> 1.20240116.2) + - abseil/memory/memory (~> 1.20240116.2) + - abseil/meta/type_traits (~> 1.20240116.2) + - abseil/random/bit_gen_ref (~> 1.20240116.2) + - abseil/random/distributions (~> 1.20240116.2) + - abseil/random/random (~> 1.20240116.2) + - abseil/status/status (~> 1.20240116.2) + - abseil/status/statusor (~> 1.20240116.2) + - abseil/strings/cord (~> 1.20240116.2) + - abseil/strings/str_format (~> 1.20240116.2) + - abseil/strings/strings (~> 1.20240116.2) + - abseil/synchronization/synchronization (~> 1.20240116.2) + - abseil/time/time (~> 1.20240116.2) + - abseil/types/optional (~> 1.20240116.2) + - abseil/types/span (~> 1.20240116.2) + - abseil/types/variant (~> 1.20240116.2) + - abseil/utility/utility (~> 1.20240116.2) + - BoringSSL-GRPC (= 0.0.36) + - gRPC-Core/Interface (= 1.65.5) + - gRPC-Core/Privacy (= 1.65.5) + - gRPC-Core/Interface (1.65.5) + - gRPC-Core/Privacy (1.65.5) + - GTMAppAuth (4.1.1): + - AppAuth/Core (~> 1.7) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher (3.5.0): + - GTMSessionFetcher/Full (= 3.5.0) + - GTMSessionFetcher/Core (3.5.0) + - GTMSessionFetcher/Full (3.5.0): + - GTMSessionFetcher/Core + - image_gallery_saver_plus (0.0.1): + - Flutter + - leveldb-library (1.22.6) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - PromisesObjC (2.4.0) + - RecaptchaInterop (100.0.0) + - share_plus (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) + - Firebase/Analytics + - Firebase/Auth + - Firebase/Core + - Firebase/Firestore + - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - Flutter (from `Flutter`) + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) + - image_gallery_saver_plus (from `.symlinks/plugins/image_gallery_saver_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - abseil + - AppAuth + - BoringSSL-GRPC + - Firebase + - FirebaseAnalytics + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseFirestore + - FirebaseFirestoreInternal + - FirebaseInstallations + - FirebaseSharedSwift + - GoogleAppMeasurement + - GoogleSignIn + - GoogleUtilities + - "gRPC-C++" + - gRPC-Core + - GTMAppAuth + - GTMSessionFetcher + - leveldb-library + - nanopb + - PromisesObjC + - RecaptchaInterop + +EXTERNAL SOURCES: + cloud_firestore: + :path: ".symlinks/plugins/cloud_firestore/ios" + firebase_auth: + :path: ".symlinks/plugins/firebase_auth/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + Flutter: + :path: Flutter + google_sign_in_ios: + :path: ".symlinks/plugins/google_sign_in_ios/darwin" + image_gallery_saver_plus: + :path: ".symlinks/plugins/image_gallery_saver_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + abseil: d121da9ef7e2ff4cab7666e76c5a3e0915ae08c3 + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 + BoringSSL-GRPC: ca6a8e5d04812fce8ffd6437810c2d46f925eaeb + cloud_firestore: 7a6d7b8111fe44d13a0e49783daad5219ce798ff + Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf + firebase_auth: c028e89b64d87f04708ed01f0e7691d997b3fa0d + firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f + FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b + FirebaseAppCheckInterop: 9226f7217b43e99dfa0bc9f674ad8108cef89feb + FirebaseAuth: ad59a1a7b161e75f74c39f70179d2482d40e2737 + FirebaseAuthInterop: 2a26ee1bea6d47df8048683cfa071e7da657798f + FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d + FirebaseCoreExtension: 3d3f2017a00d06e09ab4ebe065391b0bb642565e + FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 + FirebaseFirestore: 563a4ab1a65e2858f05e150bb4c31b0f8f79248b + FirebaseFirestoreInternal: 8c5921c360a70e447bfeefb245f450e8b50e750b + FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 + FirebaseSharedSwift: 574e6a5602afe4397a55c8d4f767382d620285de + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8 + GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896 + GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + "gRPC-C++": 2fa52b3141e7789a28a737f251e0c45b4cb20a87 + gRPC-Core: a27c294d6149e1c39a7d173527119cfbc3375ce4 + GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + image_gallery_saver_plus: e597bf65a7846979417a3eae0763b71b6dfec6c3 + leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + +PODFILE CHECKSUM: 55e20c0dc3b0fcb9699439564c45d4bfb91e7f9d + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..80f29a2 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,753 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0B08F4E3191C1718CE721DEE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44309CC712138A6C295BDD13 /* Pods_Runner.framework */; }; + 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 */; }; + 41BE7AE37D217557707F77F4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A61884E6070EB92C9DA7565 /* Pods_RunnerTests.framework */; }; + 619C01BB6DEEA093B9F64580 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 71E56F6CBCC9735A933C8324 /* GoogleService-Info.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 */ + 090C37EAACC6628C1878F26D /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 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 = ""; }; + 3001B99FAF58D3847964C671 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; 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; }; + 3A61884E6070EB92C9DA7565 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 40E1C4F703AEAFB3F4E96C40 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 44309CC712138A6C295BDD13 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6623077AAAEF559F09EAAEC6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 71E56F6CBCC9735A933C8324 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.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 = ""; }; + A8A46054CD2BDDAEAD8EBD4C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E4D15238BF2B8882453091C1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 822140EDA9A65D30FEC797F5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 41BE7AE37D217557707F77F4 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0B08F4E3191C1718CE721DEE /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 767DA86CBE420F5563DA86EC /* Pods */ = { + isa = PBXGroup; + children = ( + A8A46054CD2BDDAEAD8EBD4C /* Pods-Runner.debug.xcconfig */, + 3001B99FAF58D3847964C671 /* Pods-Runner.release.xcconfig */, + 40E1C4F703AEAFB3F4E96C40 /* Pods-Runner.profile.xcconfig */, + E4D15238BF2B8882453091C1 /* Pods-RunnerTests.debug.xcconfig */, + 6623077AAAEF559F09EAAEC6 /* Pods-RunnerTests.release.xcconfig */, + 090C37EAACC6628C1878F26D /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + 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 */, + 767DA86CBE420F5563DA86EC /* Pods */, + EBB65BF4410896E755CE9345 /* Frameworks */, + 71E56F6CBCC9735A933C8324 /* GoogleService-Info.plist */, + ); + 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 = ""; + }; + EBB65BF4410896E755CE9345 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 44309CC712138A6C295BDD13 /* Pods_Runner.framework */, + 3A61884E6070EB92C9DA7565 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + EE15A8A241E794CA6B46D3BF /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 822140EDA9A65D30FEC797F5 /* Frameworks */, + ); + 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 = ( + FB0BF3469EFA785B86C304D7 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 978F70D62289D467CBC98B93 /* [CP] Embed Pods Frameworks */, + 09CC5E7467F1BA4502F52B26 /* [CP] Copy Pods Resources */, + ); + 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 */, + 619C01BB6DEEA093B9F64580 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 09CC5E7467F1BA4502F52B26 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 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"; + }; + 978F70D62289D467CBC98B93 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EE15A8A241E794CA6B46D3BF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FB0BF3469EFA785B86C304D7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* 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 = 12.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)"; + DEVELOPMENT_TEAM = L6P4GD56Q6; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.forwardChainingManApp; + 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; + baseConfigurationReference = E4D15238BF2B8882453091C1 /* Pods-RunnerTests.debug.xcconfig */; + 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.forwardChainingManApp.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; + baseConfigurationReference = 6623077AAAEF559F09EAAEC6 /* Pods-RunnerTests.release.xcconfig */; + 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.forwardChainingManApp.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; + baseConfigurationReference = 090C37EAACC6628C1878F26D /* Pods-RunnerTests.profile.xcconfig */; + 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.forwardChainingManApp.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 = 12.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 = 12.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)"; + DEVELOPMENT_TEAM = L6P4GD56Q6; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.forwardChainingManApp; + 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)"; + DEVELOPMENT_TEAM = L6P4GD56Q6; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.forwardChainingManApp; + 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/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..8e3ca5d --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/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/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/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/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..2c107a7 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 1072369607324-0e0k7pn3nqk6suq97jnegljp3k0161o5.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.1072369607324-0e0k7pn3nqk6suq97jnegljp3k0161o5 + ANDROID_CLIENT_ID + 1072369607324-hp2vujt95ipkft4n348a96ndbiocqrdo.apps.googleusercontent.com + API_KEY + AIzaSyDI7KTuPhqGiIkX5pznUDYkToUmIhznqf8 + GCM_SENDER_ID + 1072369607324 + PLIST_VERSION + 1 + BUNDLE_ID + com.example.forwardChainingManApp + PROJECT_ID + sirekomendasi-dc7de + STORAGE_BUCKET + sirekomendasi-dc7de.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1072369607324:ios:1d8cb1dbadb289ff6f4359 + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..0af068d --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,69 @@ + + + + + NSPhotoLibraryAddUsageDescription + We need permission to save photos and videos to your library for your convenience. + + NSPhotoLibraryUsageDescription + We need permission to access your photo library to view and select your photos and videos. + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Forward Chaining Man App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + forward_chaining_man_app + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSAllowsLocalNetworking + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + + + com.googleusercontent.apps.1072369607324-0e0k7pn3nqk6suq97jnegljp3k0161o5 + + + + UIApplicationSupportsIndirectInputEvents + + + \ No newline at end of file diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/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/lib/admin_mode.dart b/lib/admin_mode.dart new file mode 100644 index 0000000..7ca6533 --- /dev/null +++ b/lib/admin_mode.dart @@ -0,0 +1,139 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; + +//////////////////////////////////////////////////////////// +/// Bagian Model & Fungsi Pendukung +//////////////////////////////////////////////////////////// + +/// Representasi data ProgramStudi (bawaan dari JSON) +class ProgramStudi { + final String name; + final String description; + final List categories; + final Map minat; + + ProgramStudi({ + required this.name, + required this.description, + required this.categories, + required this.minat, + }); + + factory ProgramStudi.fromJson(Map json) { + final minatMap = {}; + if (json['minat'] != null) { + (json['minat'] as Map).forEach((key, value) { + minatMap[key] = Minat.fromJson(value); + }); + } + + return ProgramStudi( + name: json['name'] ?? '', + description: json['description'] ?? '', + categories: json['categories'] == null + ? [] + : List.from(json['categories']), + minat: minatMap, + ); + } +} + +/// Representasi data Minat (pertanyaan, karir, dsb.) +class Minat { + final List pertanyaan; + final List karir; + final List jurusanTerkait; + + Minat({ + required this.pertanyaan, + required this.karir, + required this.jurusanTerkait, + }); + + factory Minat.fromJson(Map json) { + return Minat( + pertanyaan: json['pertanyaan'] == null + ? [] + : List.from(json['pertanyaan']), + karir: json['karir'] == null ? [] : List.from(json['karir']), + jurusanTerkait: json['jurusan_terkait'] == null + ? [] + : List.from(json['jurusan_terkait']), + ); + } +} + +/// Model flattened satu pertanyaan +class QuestionItem { + final String id; // unique ID, misal "Q1", "Q2" + final String programName; // ex: "IPA (Sains Murni) - Kerja" + final String minatKey; // ex: "Kedokteran" + final String questionText; // Cleaned question text + final String rawQuestionText; // Original question text with code + final int bobot; // ex: 5 + bool? userAnswer; // Jawaban user: true, false, atau null + + // Add this field to store the question code + final String questionCode; // ex: "KUL04" + + // Add any other existing fields... + + QuestionItem({ + required this.id, + required this.programName, + required this.minatKey, + required this.questionText, + required this.rawQuestionText, + required this.bobot, + this.userAnswer, + required this.questionCode, // Add to constructor + // Add any other existing parameters... + }); + + // If needed, you can keep a factory method to extract code from raw text + factory QuestionItem.fromRawQuestion({ + required String id, + required String programName, + required String minatKey, + required String questionText, + required String rawQuestionText, + required int bobot, + bool? userAnswer, + // Any other existing parameters... + }) { + // Extract question code + final regex = RegExp(r'([A-Z]+\d+):'); + final match = regex.firstMatch(rawQuestionText); + final questionCode = match != null && match.groupCount >= 1 + ? match.group(1)! + : id; // Fallback to ID if no code found + + return QuestionItem( + id: id, + programName: programName, + minatKey: minatKey, + questionText: questionText, + rawQuestionText: rawQuestionText, + bobot: bobot, + userAnswer: userAnswer, + questionCode: questionCode, + // Pass any other existing parameters... + ); + } +} + +/// Fungsi untuk mengambil bobot [n] dari string pertanyaan +int extractBobot(String pertanyaan) { + final regex = RegExp(r"\[(\d+)\]"); + final match = regex.firstMatch(pertanyaan); + if (match != null) { + return int.parse(match.group(1)!); + } + return 0; +} + +/// Fungsi untuk menghapus teks [n] di akhir pertanyaan +String cleanPertanyaan(String pertanyaan) { + return pertanyaan.replaceAll(RegExp(r"\[\d+\]"), "").trim(); +} diff --git a/lib/app/controllers/developer_controller.dart b/lib/app/controllers/developer_controller.dart new file mode 100644 index 0000000..4b66d89 --- /dev/null +++ b/lib/app/controllers/developer_controller.dart @@ -0,0 +1,13 @@ +import 'package:get/get.dart'; + +bool developerMode = false; + +/// Controller untuk DeveloperModePage +class DeveloperModeController extends GetxController { + final RxBool isDeveloperMode = developerMode.obs; + + void toggleDeveloperMode(bool value) { + isDeveloperMode.value = value; + developerMode = value; + } +} diff --git a/lib/app/controllers/home_controller.dart b/lib/app/controllers/home_controller.dart new file mode 100644 index 0000000..f3e77a4 --- /dev/null +++ b/lib/app/controllers/home_controller.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; + +/// Controller untuk HomePage +class HomeController extends GetxController { + final Rx pilihan = + Rx(null); // null=belum pilih; true=Kerja; false=Kuliah + + void setPilihan(bool? val) { + pilihan.value = val; + } +} diff --git a/lib/app/views/about/page_about.dart b/lib/app/views/about/page_about.dart new file mode 100644 index 0000000..7d4acc8 --- /dev/null +++ b/lib/app/views/about/page_about.dart @@ -0,0 +1,976 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:forward_chaining_man_app/app/views/about/widget/diagram_painter.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; + +import 'package:url_launcher/url_launcher.dart'; + +class AboutPage extends StatefulWidget { + const AboutPage({Key? key}) : super(key: key); + + @override + State createState() => _AboutPageState(); +} + +class ForwardChainingLogoPainter extends CustomPainter { + final double animationValue; + + ForwardChainingLogoPainter({required this.animationValue}); + + @override + void paint(Canvas canvas, Size size) { + final centerX = size.width / 2; + final centerY = size.height / 2; + final radius = size.width * 0.32; + + // Create a layered neural network structure with input, hidden, and output nodes + + // Define the layers (3 layers: input, hidden, output) + final int inputNodes = 4; + final int hiddenNodes = 6; + final int outputNodes = 3; + + // Node positions for each layer + final List inputLayer = []; + final List hiddenLayer = []; + final List outputLayer = []; + + // Create a gradient for the background glow + final Rect rect = + Rect.fromCircle(center: Offset(centerX, centerY), radius: radius * 1.2); + final gradient = RadialGradient( + colors: [ + Colors.indigo.shade400.withOpacity(0.2), + Colors.transparent, + ], + stops: const [0.5, 1.0], + ); + + final backgroundPaint = Paint() + ..shader = gradient.createShader(rect) + ..style = PaintingStyle.fill; + + canvas.drawCircle(Offset(centerX, centerY), radius * 1.2, backgroundPaint); + + // Paints for nodes + final inputNodePaint = Paint() + ..color = Colors.blue.shade400 + ..style = PaintingStyle.fill; + + final hiddenNodePaint = Paint() + ..color = Colors.indigo.shade400 + ..style = PaintingStyle.fill; + + final outputNodePaint = Paint() + ..color = Colors.purple.shade400 + ..style = PaintingStyle.fill; + + // Paint for node glows + final glowPaint = Paint() + ..color = Colors.indigo.shade200.withOpacity(0.4) + ..style = PaintingStyle.fill + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8); + + // Paint for node borders + final borderPaint = Paint() + ..color = Colors.white.withOpacity(0.8) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + // Paint for connections with animation + final connectionPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + // Create input layer nodes (left side) + final inputSpacing = size.height / (inputNodes + 1); + for (int i = 0; i < inputNodes; i++) { + final y = (i + 1) * inputSpacing; + final pulseFactor = math + .sin((animationValue * 2 * math.pi) + (i * 0.5)) + .clamp(-0.5, 0.5) * + 0.05; + final offsetX = (math.sin(animationValue * math.pi + i) * 4).clamp(-4, 4); + + // Position slightly to the left of center + inputLayer.add(Offset( + centerX - (radius * 0.7) + offsetX, y + (pulseFactor * size.height))); + } + + // Create hidden layer nodes (center) + final hiddenSpacing = size.height / (hiddenNodes + 1); + for (int i = 0; i < hiddenNodes; i++) { + final y = (i + 1) * hiddenSpacing; + final pulseFactor = math + .sin((animationValue * 2 * math.pi) + (i * 0.7)) + .clamp(-0.5, 0.5) * + 0.03; + final offsetY = + (math.sin(animationValue * math.pi * 2 + i * 0.5) * 5).clamp(-5, 5); + + // Position at center + hiddenLayer + .add(Offset(centerX + (pulseFactor * size.width), y + offsetY)); + } + + // Create output layer nodes (right side) + final outputSpacing = size.height / (outputNodes + 1); + for (int i = 0; i < outputNodes; i++) { + final y = (i + 1) * outputSpacing; + final pulseFactor = math + .sin((animationValue * 2 * math.pi) + (i * 0.9)) + .clamp(-0.5, 0.5) * + 0.05; + final offsetX = + (math.sin(animationValue * math.pi + i * 1.2) * 4).clamp(-4, 4); + + // Position to the right of center + outputLayer.add(Offset( + centerX + (radius * 0.7) + offsetX, y + (pulseFactor * size.height))); + } + + // Draw connections with animated data flow + void drawConnections( + List fromLayer, List toLayer, Color baseColor) { + for (int i = 0; i < fromLayer.length; i++) { + for (int j = 0; j < toLayer.length; j++) { + // Create a flow effect along the connection + final path = Path(); + path.moveTo(fromLayer[i].dx, fromLayer[i].dy); + path.lineTo(toLayer[j].dx, toLayer[j].dy); + + // Create gradient shader for data flow effect + final pathMetrics = path.computeMetrics().first; + final length = pathMetrics.length; + + // Animate a dot along the path + final flowPosition = + (animationValue * 2 + (i * 0.1) + (j * 0.05)) % 1.0; + final flowPoint = + pathMetrics.getTangentForOffset(length * flowPosition)?.position; + + // Basic line + connectionPaint.color = baseColor.withOpacity(0.3 + + (0.2 * math.sin(animationValue * math.pi * 2 + i + j)) + .clamp(0.0, 0.5)); + canvas.drawPath(path, connectionPaint); + + // Draw data flow point + if (flowPoint != null && (i + j) % 2 == 0) { + // Only draw on some connections to avoid clutter + final flowDotPaint = Paint() + ..color = Colors.white.withOpacity(0.7) + ..style = PaintingStyle.fill; + + canvas.drawCircle(flowPoint, 1.5, flowDotPaint); + } + } + } + } + + // Draw connections from input to hidden layer + drawConnections(inputLayer, hiddenLayer, Colors.blue.shade500); + + // Draw connections from hidden to output layer + drawConnections(hiddenLayer, outputLayer, Colors.purple.shade500); + + // Draw the nodes with glow effect + void drawNodesWithEffects( + List nodes, Paint nodePaint, double size) { + for (int i = 0; i < nodes.length; i++) { + final node = nodes[i]; + // Size pulsation + final pulse = + 1.0 + 0.15 * math.sin((animationValue * 2 * math.pi) + (i)); + final nodeSize = size * pulse.clamp(0.9, 1.15); + + // Draw glow + canvas.drawCircle(node, nodeSize * 1.5, glowPaint); + + // Draw node + canvas.drawCircle(node, nodeSize, nodePaint); + + // Draw border + canvas.drawCircle(node, nodeSize, borderPaint); + } + } + + // Draw all nodes by layer + drawNodesWithEffects(inputLayer, inputNodePaint, 5); + drawNodesWithEffects(hiddenLayer, hiddenNodePaint, 6); + drawNodesWithEffects(outputLayer, outputNodePaint, 5); + + // Draw central circle highlight + final centerGlowPaint = Paint() + ..color = Colors.indigo.withOpacity( + (0.1 + 0.05 * math.sin(animationValue * math.pi * 2)) + .clamp(0.05, 0.15)) + ..style = PaintingStyle.fill + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 15); + + canvas.drawCircle(Offset(centerX, centerY), radius * 0.4, centerGlowPaint); + } + + @override + bool shouldRepaint(ForwardChainingLogoPainter oldDelegate) { + return oldDelegate.animationValue != animationValue; + } +} + +class _AboutPageState extends State with TickerProviderStateMixin { + late AnimationController _logoAnimationController; + late AnimationController _cardAnimationController; + late Animation _logoRotationAnimation; + late Animation _logoScaleAnimation; + late Animation _fadeInAnimation; + late List> _cardSlideAnimations; + + // For step animation + int _currentStep = 0; + final int _totalSteps = 4; + + @override + void initState() { + super.initState(); + + // Logo animation controller + _logoAnimationController = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..repeat(reverse: true); + +// Card animation controller + _cardAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ); + +// Logo animations +// Fix: Ensure the end value is <= 1.0 to prevent the assertion error + _logoRotationAnimation = Tween(begin: 0, end: 0.05).animate( + CurvedAnimation( + parent: _logoAnimationController, + curve: Curves.easeInOut, + ), + ); + +// Fix: Ensure the end value doesn't cause any curves to go beyond 1.0 + _logoScaleAnimation = Tween(begin: 1.0, end: 1.08).animate( + CurvedAnimation( + parent: _logoAnimationController, + curve: Curves.easeInOut, + ), + ); + + _fadeInAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation( + parent: _cardAnimationController, + // Fix: Ensure the end of the interval is <= 1.0 + curve: const Interval(0, 0.6, curve: Curves.easeOut), + ), + ); + + // Card slide animations (staggered) + _cardSlideAnimations = [ + for (int i = 0; i < 5; i++) + Tween( + begin: const Offset(0, 0.5), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _cardAnimationController, + curve: Interval( + 0.2 + (i * 0.12), + (0.7 + (i * 0.08)).clamp(0.0, 1.0), // ✅ Perbaikan utama! + curve: Curves.easeOutCubic, + ), + ), + ), + ]; + + // Start animations + _cardAnimationController.forward(); + + // Start step animation + Future.delayed(const Duration(seconds: 2), () { + _startStepAnimation(); + }); + } + + void _startStepAnimation() { + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + setState(() { + _currentStep = (_currentStep + 1) % _totalSteps; + }); + _startStepAnimation(); + } + }); + } + + @override + void dispose() { + _logoAnimationController.dispose(); + _cardAnimationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.indigo.shade900, + Colors.blue.shade800, + Colors.blue.shade700, + ], + ), + ), + child: SafeArea( + bottom: false, + child: Column( + children: [ + // App Bar + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: GestureDetector( + onTap: () => Get.back(), + child: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + ), + ), + const Spacer(), + const Text( + 'Tentang Aplikasi', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const Spacer(), + const SizedBox(width: 40), // Balance the layout + ], + ), + ), + + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.all(24), + child: Column( + children: [ + // Animated Logo + + const SizedBox(height: 24), + + // Title and Tagline + FadeTransition( + opacity: _fadeInAnimation, + child: Column( + children: [ + const Text( + 'EduGuide', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(50), + ), + child: const Text( + 'Sistem Rekomendasi Karir & Jurusan', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 40), + + // About App Card + SlideTransition( + position: _cardSlideAnimations[0], + child: _buildInfoCard( + title: 'Tentang Aplikasi', + icon: Icons.info_outline, + color: Colors.blue.shade300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Aplikasi EduGuide adalah sistem pakar berbasis aturan (rule-based expert system) yang menggunakan metode inferensi forward chaining untuk memberikan rekomendasi jurusan dan karir yang sesuai dengan minat pengguna.', + style: TextStyle(fontSize: 14, height: 1.5), + ), + const SizedBox(height: 16), + const Text( + 'Aplikasi ini dikembangkan sebagai bagian dari tugas akhir/skripsi untuk menunjukkan implementasi praktis dari metode forward chaining dalam sistem pendukung keputusan.', + style: TextStyle(fontSize: 14, height: 1.5), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + + // How It Works Card + SlideTransition( + position: _cardSlideAnimations[1], + child: _buildInfoCard( + title: 'Cara Kerja Forward Chaining', + icon: Icons.lightbulb_outline, + color: Colors.orange.shade300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Forward Chaining adalah metode penalaran dari fakta-fakta yang diketahui menuju kesimpulan. Dalam aplikasi ini:', + style: TextStyle(fontSize: 14, height: 1.5), + ), + const SizedBox(height: 16), + + // Animated steps + _buildAnimatedStepExplanation(), + + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.amber.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.amber.shade200, + ), + ), + child: Row( + children: [ + Icon( + Icons.tips_and_updates, + color: Colors.amber.shade700, + size: 24, + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Dengan metode ini, sistem dapat memberikan rekomendasi yang paling sesuai berdasarkan minat kamu!', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + + // Forward Chaining Visual Explanation + SlideTransition( + position: _cardSlideAnimations[2], + child: _buildInfoCard( + title: 'Visualisasi Proses', + icon: Icons.bar_chart, + color: Colors.green.shade300, + child: Column( + children: [ + Image.asset( + 'assets/forward_chaining_diagram.png', + fit: BoxFit.contain, + height: 180, + errorBuilder: (context, error, stackTrace) { + // Fallback if image not available + return _buildForwardChainingDiagram(); + }, + ), + const SizedBox(height: 16), + const Text( + 'Forward Chaining bekerja dengan mengevaluasi jawaban kamu dan mencocokkannya dengan aturan (rules) untuk menemukan rekomendasi terbaik. Ini seperti menyelesaikan teka-teki dengan petunjuk yang kamu berikan.', + style: TextStyle(fontSize: 14, height: 1.5), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + + // Tech Stack Card + SlideTransition( + position: _cardSlideAnimations[3], + child: _buildInfoCard( + title: 'Teknologi', + icon: Icons.code, + color: Colors.purple.shade300, + child: Wrap( + spacing: 10, + runSpacing: 10, + children: [ + _buildTechChip( + label: 'Flutter', icon: Icons.flutter_dash), + _buildTechChip( + label: 'Dart', icon: Icons.extension), + _buildTechChip( + label: 'Forward Chaining', + icon: Icons.account_tree), + _buildTechChip( + label: 'GetX', icon: Icons.auto_awesome), + _buildTechChip( + label: 'Rule-Based System', icon: Icons.rule), + _buildTechChip( + label: 'Expert System', + icon: Icons.psychology), + ], + ), + ), + ), + const SizedBox(height: 16), + + // Developer Card + SlideTransition( + position: _cardSlideAnimations[4], + child: _buildInfoCard( + title: 'Pengembang', + icon: Icons.person, + color: Colors.amber.shade300, + child: Row( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.grey.shade200, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + image: const DecorationImage( + image: AssetImage('assets/profile_dev.png'), + fit: BoxFit.cover, + // Use a placeholder if no image is available + onError: null, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Yanuar Tri Laksono', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + const Text( + 'Mahasiswa Informatika', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + SocialButton( + icon: FontAwesomeIcons + .envelope, // Email icon + onTap: () { + launchUrl(Uri.parse( + 'mailto:yanuartrilaksono23@gmail.com')); + }, + ), + const SizedBox(width: 12), + SocialButton( + icon: FontAwesomeIcons + .linkedin, // Portfolio link icon + onTap: () { + launchUrl(Uri.parse( + 'https://www.linkedin.com/in/yanuar-tri-laksono/')); + }, + ), + const SizedBox(width: 12), + SocialButton( + icon: FontAwesomeIcons + .github, // GitHub icon + onTap: () { + launchUrl(Uri.parse( + 'https://github.com/Greek-Cp')); + }, + ), + ], + ) + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 30), + + // Footer + FadeTransition( + opacity: _fadeInAnimation, + child: Column( + children: [ + Text( + '© ${DateTime.now().year} EduGuide App', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Text( + 'Versi 1.0.0', + style: TextStyle( + color: Colors.white.withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ), + ), + const SizedBox(height: 40), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + // Build the animated step explanation + Widget _buildAnimatedStepExplanation() { + final List> steps = [ + { + 'icon': Icons.question_answer, + 'title': 'Langkah 1: Mengumpulkan Fakta', + 'content': + 'Sistem mengumpulkan jawaban "Ya" atau "Tidak" dari semua pertanyaanmu dan menyimpannya sebagai fakta.', + 'color': Colors.blue.shade700, + }, + { + 'icon': Icons.rule, + 'title': 'Langkah 2: Mencocokkan Aturan', + 'content': + 'Sistem mencocokkan jawabanmu dengan aturan-aturan minat dan karir. Setiap jawaban "Ya" akan menambah skor pada minat tertentu.', + 'color': Colors.green.shade700, + }, + { + 'icon': Icons.calculate, + 'title': 'Langkah 3: Menghitung Skor', + 'content': + 'Skor untuk setiap minat dan karir dihitung berdasarkan pertanyaan yang kamu jawab "Ya".', + 'color': Colors.orange.shade700, + }, + { + 'icon': Icons.star, + 'title': 'Langkah 4: Memberikan Rekomendasi', + 'content': + 'Sistem mengurutkan hasil dan menampilkan 3 minat dengan skor tertinggi sebagai rekomendasi terbaikmu.', + 'color': Colors.purple.shade700, + }, + ]; + + return Column( + children: steps.asMap().entries.map((entry) { + final index = entry.key; + final step = entry.value; + final isActive = index == _currentStep; + + return AnimatedContainer( + duration: const Duration(milliseconds: 500), + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: + isActive ? step['color'].withOpacity(0.1) : Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isActive ? step['color'] : Colors.grey.shade200, + width: isActive ? 2 : 1, + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 500), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isActive ? step['color'] : Colors.grey.shade200, + shape: BoxShape.circle, + ), + child: Icon( + step['icon'], + color: isActive ? Colors.white : Colors.grey.shade600, + size: 18, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + step['title'], + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: isActive ? step['color'] : Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + step['content'], + style: TextStyle( + fontSize: 13, + height: 1.4, + color: isActive ? Colors.black87 : Colors.grey.shade700, + ), + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ); + } + + // Fallback Forward Chaining diagram if image is not available + Widget _buildForwardChainingDiagram() { + return Container( + height: 180, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: CustomPaint( + painter: ForwardChainingDiagramPainter(), + size: const Size(double.infinity, 180), + ), + ); + } + + // Animated logo content with nodes and connections + Widget _buildAnimatedLogoContent() { + return CustomPaint( + painter: ForwardChainingLogoPainter( + animationValue: _logoAnimationController.value, + ), + child: const Icon( + Icons.psychology, + size: 70, + color: Colors.indigo, + ), + ); + } + + // Card widget with consistent styling + Widget _buildInfoCard({ + required String title, + required IconData icon, + required Color color, + required Widget child, + }) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Card Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withOpacity(0.2), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + + // Card Content + Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ], + ), + ); + } + + // Tech stack chip + Widget _buildTechChip({required String label, required IconData icon}) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(50), + border: Border.all( + color: Colors.grey.shade300, + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 16, + color: Colors.indigo, + ), + const SizedBox(width: 6), + Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + ), + ], + ), + ); + } + + // Social media/contact button + Widget _buildSocialButton({ + required IconData icon, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + size: 18, + color: Colors.indigo, + ), + ), + ); + } +} + +class SocialButton extends StatelessWidget { + final IconData icon; + final VoidCallback onTap; + + const SocialButton({ + required this.icon, + required this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + ), + child: FaIcon( + icon, + size: 18, + color: Colors.indigo, + ), + ), + ); + } +} diff --git a/lib/app/views/about/widget/diagram_painter.dart b/lib/app/views/about/widget/diagram_painter.dart new file mode 100644 index 0000000..0933178 --- /dev/null +++ b/lib/app/views/about/widget/diagram_painter.dart @@ -0,0 +1,219 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; + +import 'package:url_launcher/url_launcher.dart'; + +class ForwardChainingDiagramPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final width = size.width; + final height = size.height; + + // Define colors + final workingMemoryColor = Colors.blue.shade100; + final rulesColor = Colors.orange.shade100; + final resultsColor = Colors.green.shade100; + final arrowColor = Colors.grey.shade700; + + // Define paint objects + final boxPaint = Paint()..style = PaintingStyle.fill; + final borderPaint = Paint() + ..style = PaintingStyle.stroke + ..color = Colors.grey.shade600 + ..strokeWidth = 1.5; + + final textStyle = TextStyle( + color: Colors.black87, + fontSize: 10, + fontWeight: FontWeight.w500, + ); + + final titleStyle = TextStyle( + color: Colors.black, + fontSize: 11, + fontWeight: FontWeight.bold, + ); + + // Helper function to draw boxes with text + void drawBox(String title, String content, Rect rect, Color color) { + // Draw box + boxPaint.color = color; + canvas.drawRRect( + RRect.fromRectAndRadius(rect, Radius.circular(8)), + boxPaint, + ); + canvas.drawRRect( + RRect.fromRectAndRadius(rect, Radius.circular(8)), + borderPaint, + ); + + // Draw title + final titleSpan = TextSpan(text: title, style: titleStyle); + final titlePainter = TextPainter( + text: titleSpan, + textDirection: TextDirection.ltr, + ); + titlePainter.layout(maxWidth: rect.width - 10); + titlePainter.paint( + canvas, + Offset(rect.left + 5, rect.top + 5), + ); + + // Draw content + final contentSpan = TextSpan(text: content, style: textStyle); + final contentPainter = TextPainter( + text: contentSpan, + textDirection: TextDirection.ltr, + ); + contentPainter.layout(maxWidth: rect.width - 10); + contentPainter.paint( + canvas, + Offset(rect.left + 5, rect.top + 25), + ); + } + + // Helper to draw arrows + void drawArrow(Offset start, Offset end) { + final paint = Paint() + ..color = arrowColor + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke; + + // Draw line + canvas.drawLine(start, end, paint); + + // Draw arrowhead + final delta = end - start; + final angle = delta.direction; + final arrowSize = 8.0; + + final arrowPath = Path() + ..moveTo(end.dx, end.dy) + ..lineTo( + end.dx - arrowSize * math.cos(angle - math.pi / 6), + end.dy - arrowSize * math.sin(angle - math.pi / 6), + ) + ..lineTo( + end.dx - arrowSize * math.cos(angle + math.pi / 6), + end.dy - arrowSize * math.sin(angle + math.pi / 6), + ) + ..close(); + + canvas.drawPath(arrowPath, Paint()..color = arrowColor); + } + + // Calculate box positions + final workingMemoryRect = Rect.fromLTWH(20, 20, width * 0.25, height - 40); + final rulesRect = + Rect.fromLTWH(width * 0.33, 20, width * 0.25, height - 40); + final resultsRect = + Rect.fromLTWH(width * 0.66, 20, width * 0.25, height - 40); + + // Draw the boxes + drawBox( + 'Working Memory', + 'Facts:\n• Q1=Yes\n• Q2=No\n• Q3=Yes\n...', + workingMemoryRect, + workingMemoryColor, + ); + + drawBox( + 'Rules', + 'IF Q1=Yes THEN Skor+3\nIF Q2=Yes THEN Skor+2\nIF Q3=Yes AND Q4=Yes\n THEN Skor+4\n...', + rulesRect, + rulesColor, + ); + + drawBox( + 'Results (Top 3)', + '1. IPA | Kedokteran (24)\n2. IPS | Ekonomi (19)\n3. IPA | Teknik (16)', + resultsRect, + resultsColor, + ); + + // Draw arrows + drawArrow( + Offset(workingMemoryRect.right, workingMemoryRect.center.dy - 20), + Offset(rulesRect.left, rulesRect.center.dy - 20), + ); + + drawArrow( + Offset(rulesRect.right, rulesRect.center.dy), + Offset(resultsRect.left, resultsRect.center.dy), + ); + + // Draw loop arrow for rule evaluation + final loopStart = Offset(rulesRect.right - 20, rulesRect.bottom - 25); + final loopControl1 = Offset(rulesRect.right + 20, rulesRect.bottom + 15); + final loopControl2 = Offset(rulesRect.left - 20, rulesRect.bottom + 15); + final loopEnd = Offset(rulesRect.left, rulesRect.bottom - 25); + + final loopPath = Path() + ..moveTo(loopStart.dx, loopStart.dy) + ..cubicTo( + loopControl1.dx, + loopControl1.dy, + loopControl2.dx, + loopControl2.dy, + loopEnd.dx, + loopEnd.dy, + ); + + canvas.drawPath( + loopPath, + Paint() + ..color = arrowColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1.2, + ); + + // Draw arrowhead for loop + final loopDelta = Offset(0, -5); + final loopAngle = loopDelta.direction; + final arrowSize = 7.0; + + final loopArrowPath = Path() + ..moveTo(loopEnd.dx, loopEnd.dy) + ..lineTo( + loopEnd.dx - arrowSize * math.cos(loopAngle - math.pi / 6), + loopEnd.dy - arrowSize * math.sin(loopAngle - math.pi / 6), + ) + ..lineTo( + loopEnd.dx - arrowSize * math.cos(loopAngle + math.pi / 6), + loopEnd.dy - arrowSize * math.sin(loopAngle + math.pi / 6), + ) + ..close(); + + canvas.drawPath(loopArrowPath, Paint()..color = arrowColor); + + // Add "Rule Evaluation Loop" text + final loopTextSpan = TextSpan( + text: "Rule Evaluation Loop", + style: TextStyle( + color: Colors.grey.shade800, + fontSize: 9, + fontStyle: FontStyle.italic, + ), + ); + + final loopTextPainter = TextPainter( + text: loopTextSpan, + textDirection: TextDirection.ltr, + ); + loopTextPainter.layout(); + loopTextPainter.paint( + canvas, Offset(rulesRect.center.dx - 35, rulesRect.bottom + 5)); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/app/views/dashboard_teacher.dart b/lib/app/views/dashboard_teacher.dart new file mode 100644 index 0000000..6ba1964 --- /dev/null +++ b/lib/app/views/dashboard_teacher.dart @@ -0,0 +1,3653 @@ +import 'package:flutter/material.dart'; +import 'package:forward_chaining_man_app/app/controllers/developer_controller.dart'; +import 'package:forward_chaining_man_app/app/views/developer/page/page_developer_viewer.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:fl_chart/fl_chart.dart'; + +class TeacherDashboardController extends GetxController { + // ========== INSTANCE & DEPENDENCIES ========== + final FirebaseAuth _auth = FirebaseAuth.instance; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + // ========== STATE VARIABLES ========== + // Loading state + var isLoading = true.obs; + + // Developer mode + final RxBool isDeveloperMode = developerMode.obs; + + // User data + var teacherName = ''.obs; + + // School data + var schoolId = ''.obs; + + // Results data + var studentResults = [].obs; + var filteredResults = [].obs; + + // Filter states + var selectedCategoryFilter = 'all'.obs; + var selectedDateFilter = 'all_time'.obs; + var selectedFilter = 'all'.obs; + var selectedClass = 'Semua Kelas'.obs; + var availableClasses = ['Semua Kelas'].obs; + + // Custom date filter + var selectedStartDate = DateTime.now().obs; + var selectedEndDate = DateTime.now().obs; + var isCustomDateSelected = false.obs; + + // Statistics + var stats = { + 'totalStudents': 0, + 'careerRecommendations': 0, + 'studyRecommendations': 0, + }.obs; + + // Variabel untuk pagination + var currentPage = 0.obs; + var itemsPerPage = 10.obs; // Jumlah item per halaman + var totalPages = 0.obs; + var paginatedResults = [].obs; + + // Fungsi untuk mengatur jumlah item per halaman + void setItemsPerPage(int count) { + itemsPerPage.value = count; + updatePagination(); + } + + // Fungsi untuk pindah ke halaman tertentu + void goToPage(int page) { + if (page >= 0 && page < totalPages.value) { + currentPage.value = page; + updatePagination(); + } + } + + // Fungsi untuk halaman selanjutnya + void nextPage() { + if (currentPage.value < totalPages.value - 1) { + currentPage.value++; + updatePagination(); + } + } + + // Fungsi untuk halaman sebelumnya + void previousPage() { + if (currentPage.value > 0) { + currentPage.value--; + updatePagination(); + } + } + + void updatePagination() { + if (filteredResults.isEmpty) { + paginatedResults.clear(); + totalPages.value = 0; + return; + } + + totalPages.value = (filteredResults.length / itemsPerPage.value).ceil(); + + // Pastikan halaman saat ini valid + if (currentPage.value >= totalPages.value) { + currentPage.value = totalPages.value - 1; + } + + int startIndex = currentPage.value * itemsPerPage.value; + int endIndex = startIndex + itemsPerPage.value; + + // Pastikan endIndex tidak melebihi panjang array + if (endIndex > filteredResults.length) { + endIndex = filteredResults.length; + } + + // Update data yang akan ditampilkan + paginatedResults.value = filteredResults.sublist(startIndex, endIndex); + } + + // Cache untuk student class mapping - user ID to class mapping + final Map _studentClassCache = {}; + + // Cache untuk student school mapping - user ID to school ID mapping + final Map _studentSchoolCache = {}; + + // ========== LIFECYCLE METHODS ========== + @override + void onInit() { + super.onInit(); + loadTeacherData(); + loadStudentResults(); + loadAvailableClasses(); + } + + // ========== DATA LOADING METHODS ========== + Future loadTeacherData() async { + try { + final User? currentUser = _auth.currentUser; + if (currentUser != null) { + // Tetap menggunakan koleksi teachers seperti yang diminta + final teacherDoc = + await _firestore.collection('teachers').doc(currentUser.uid).get(); + if (teacherDoc.exists) { + teacherName.value = teacherDoc.data()?['name'] ?? 'Guru'; + // Ambil schoolId dari data guru jika ada + schoolId.value = teacherDoc.data()?['schoolId'] ?? ''; + } + } + } catch (e) { + print('Error loading teacher data: $e'); + } + } + + Future loadStudentResults() async { + try { + isLoading.value = true; + + // Get the teacher's school ID + final prefs = await SharedPreferences.getInstance(); + final teacherSchoolId = prefs.getString('school_id') ?? schoolId.value; + + if (teacherSchoolId.isEmpty) { + // If no school ID, handle the error + Get.snackbar( + 'Error', + 'Tidak dapat menentukan sekolah Anda', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + isLoading.value = false; + return; + } + + // Preload student class cache + await _preloadStudentClassCache(); + + // Jika filter kelas sudah dipilih, gunakan query where + QuerySnapshot querySnapshot; + if (selectedClass.value != 'Semua Kelas') { + // Dapatkan semua ID siswa dengan kelas yang dipilih + List studentIds = + await _getStudentIdsByClass(selectedClass.value); + + if (studentIds.isEmpty) { + // Jika tidak ada siswa dengan kelas ini + studentResults.value = []; + filteredResults.value = []; + stats['totalStudents'] = 0; + stats['careerRecommendations'] = 0; + stats['studyRecommendations'] = 0; + isLoading.value = false; + return; + } + + // Batasi ukuran chunk karena Firestore hanya mendukung hingga 10 where-in dalam satu query + List allResults = []; + for (int i = 0; i < studentIds.length; i += 10) { + int end = (i + 10 < studentIds.length) ? i + 10 : studentIds.length; + List chunk = studentIds.sublist(i, end); + + // Query from the school-specific recommendation_history subcollection + QuerySnapshot chunkResult = await _firestore + .collection('schools') + .doc(teacherSchoolId) + .collection('recommendation_history') + .where('userId', whereIn: chunk) + .orderBy('timestamp', descending: true) + .get(); + + allResults.addAll(chunkResult.docs); + } + + // Sort the combined results by timestamp + allResults.sort((a, b) { + Timestamp aTimestamp = + (a.data() as Map)['timestamp'] as Timestamp; + Timestamp bTimestamp = + (b.data() as Map)['timestamp'] as Timestamp; + return bTimestamp.compareTo(aTimestamp); + }); + + studentResults.value = allResults; + } else { + // Terapkan pagination jika perlu - query from school-specific collection + querySnapshot = await _firestore + .collection('schools') + .doc(teacherSchoolId) + .collection('recommendation_history') + .orderBy('timestamp', descending: true) + .limit(100) // Batasi ke 100 hasil terakhir untuk mencegah lag + .get(); + + studentResults.value = querySnapshot.docs; + } + + // Apply category and date filters + _applyFiltersWithoutClass(); + + // Update stats + stats['totalStudents'] = studentResults.length; + stats['careerRecommendations'] = studentResults.where((doc) { + final data = doc.data() as Map; + return data['isKerja'] == true; + }).length; + stats['studyRecommendations'] = studentResults.where((doc) { + final data = doc.data() as Map; + return data['isKerja'] == false; + }).length; + } catch (e) { + print('Error loading student results: $e'); + Get.snackbar( + 'Error', + 'Gagal memuat data siswa: ${e.toString()}', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } finally { + isLoading.value = false; + } + } + + // Metode baru untuk memuat class cache - diperbarui untuk struktur firestore baru + Future _preloadStudentClassCache() async { + try { + if (_studentClassCache.isEmpty) { + // Dapatkan shared preferences untuk school ID + final prefs = await SharedPreferences.getInstance(); + final teacherSchoolId = prefs.getString('school_id') ?? schoolId.value; + + if (teacherSchoolId.isEmpty) { + // Jika tidak ada schoolId, coba ambil dari semua sekolah + final QuerySnapshot schoolsSnapshot = + await _firestore.collection('schools').get(); + + for (var schoolDoc in schoolsSnapshot.docs) { + // Ambil semua siswa dari setiap sekolah + final QuerySnapshot studentsSnapshot = + await schoolDoc.reference.collection('students').get(); + + for (var doc in studentsSnapshot.docs) { + final data = doc.data() as Map; + if (data.containsKey('class') && data['class'] != null) { + _studentClassCache[doc.id] = data['class'].toString(); + _studentSchoolCache[doc.id] = schoolDoc.id; + } + } + } + } else { + // Jika ada schoolId, ambil hanya dari sekolah tersebut + final QuerySnapshot studentsSnapshot = await _firestore + .collection('schools') + .doc(teacherSchoolId) + .collection('students') + .get(); + + for (var doc in studentsSnapshot.docs) { + final data = doc.data() as Map; + if (data.containsKey('class') && data['class'] != null) { + _studentClassCache[doc.id] = data['class'].toString(); + _studentSchoolCache[doc.id] = teacherSchoolId; + } + } + } + } + } catch (e) { + print('Error preloading student class cache: $e'); + } + } + + // Metode untuk mendapatkan student IDs berdasarkan kelas - diperbarui untuk struktur firestore baru + Future> _getStudentIdsByClass(String className) async { + List studentIds = []; + + try { + // Gunakan cache yang sudah kita buat + if (_studentClassCache.isNotEmpty) { + _studentClassCache.forEach((userId, userClass) { + if (userClass == className) { + studentIds.add(userId); + } + }); + } else { + // Jika cache kosong, kita perlu query database + // Dapatkan shared preferences untuk school ID + final prefs = await SharedPreferences.getInstance(); + final teacherSchoolId = prefs.getString('school_id') ?? schoolId.value; + + if (teacherSchoolId.isEmpty) { + // Jika tidak ada schoolId, cari di semua sekolah + final QuerySnapshot schoolsSnapshot = + await _firestore.collection('schools').get(); + + for (var schoolDoc in schoolsSnapshot.docs) { + final QuerySnapshot studentsSnapshot = await schoolDoc.reference + .collection('students') + .where('class', isEqualTo: className) + .get(); + + for (var doc in studentsSnapshot.docs) { + studentIds.add(doc.id); + // Update cache + _studentClassCache[doc.id] = className; + _studentSchoolCache[doc.id] = schoolDoc.id; + } + } + } else { + // Jika ada schoolId, ambil hanya dari sekolah tersebut + final QuerySnapshot studentsSnapshot = await _firestore + .collection('schools') + .doc(teacherSchoolId) + .collection('students') + .where('class', isEqualTo: className) + .get(); + + studentIds = studentsSnapshot.docs.map((doc) => doc.id).toList(); + + // Update cache + for (var doc in studentsSnapshot.docs) { + _studentClassCache[doc.id] = className; + _studentSchoolCache[doc.id] = teacherSchoolId; + } + } + } + } catch (e) { + print('Error getting student IDs by class: $e'); + } + + return studentIds; + } + + Future loadAvailableClasses() async { + try { + if (_studentClassCache.isNotEmpty) { + // Gunakan data dari cache yang sudah ada + final Set classes = {'Semua Kelas'}; + classes.addAll(_studentClassCache.values); + availableClasses.value = classes.toList()..sort(); + } else { + // Dapatkan shared preferences untuk school ID + final prefs = await SharedPreferences.getInstance(); + final teacherSchoolId = prefs.getString('school_id') ?? schoolId.value; + + // Extract unique class values + final Set classes = {'Semua Kelas'}; + + if (teacherSchoolId.isEmpty) { + // Jika tidak ada schoolId, ambil dari semua sekolah + final QuerySnapshot schoolsSnapshot = + await _firestore.collection('schools').get(); + + for (var schoolDoc in schoolsSnapshot.docs) { + final QuerySnapshot studentsSnapshot = + await schoolDoc.reference.collection('students').get(); + + for (var doc in studentsSnapshot.docs) { + final data = doc.data() as Map; + if (data.containsKey('class') && data['class'] != null) { + classes.add(data['class'].toString()); + // Update cache sambil kita ambil data + _studentClassCache[doc.id] = data['class'].toString(); + _studentSchoolCache[doc.id] = schoolDoc.id; + } + } + } + } else { + // Jika ada schoolId, ambil hanya dari sekolah tersebut + final QuerySnapshot studentsSnapshot = await _firestore + .collection('schools') + .doc(teacherSchoolId) + .collection('students') + .get(); + + for (var doc in studentsSnapshot.docs) { + final data = doc.data() as Map; + if (data.containsKey('class') && data['class'] != null) { + classes.add(data['class'].toString()); + // Update cache sambil kita ambil data + _studentClassCache[doc.id] = data['class'].toString(); + _studentSchoolCache[doc.id] = teacherSchoolId; + } + } + } + + availableClasses.value = classes.toList()..sort(); + } + } catch (e) { + print('Error loading available classes: $e'); + } + } + + // ========== FILTERING METHODS ========== + // Metode dioptimalkan untuk filter tanpa perlu loading ulang data kelas + void _applyFiltersWithoutClass() { + List result = studentResults; + + // Apply category filter + if (selectedCategoryFilter.value != 'all') { + bool isKerja = selectedCategoryFilter.value == 'career'; + result = result.where((doc) { + final data = doc.data() as Map; + return data['isKerja'] == isKerja; + }).toList(); + } + + // Apply date filter + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final firstDayOfMonth = DateTime(now.year, now.month, 1); + + if (isCustomDateSelected.value) { + final start = DateTime(selectedStartDate.value.year, + selectedStartDate.value.month, selectedStartDate.value.day); + final end = DateTime(selectedEndDate.value.year, + selectedEndDate.value.month, selectedEndDate.value.day, 23, 59, 59); + + result = result.where((doc) { + final data = doc.data() as Map; + if (data['timestamp'] == null) return false; + + final timestamp = data['timestamp'] as Timestamp; + final date = timestamp.toDate(); + return date.isAfter(start.subtract(const Duration(seconds: 1))) && + date.isBefore(end.add(const Duration(seconds: 1))); + }).toList(); + } else if (selectedDateFilter.value == 'today') { + result = result.where((doc) { + final data = doc.data() as Map; + if (data['timestamp'] == null) return false; + + final timestamp = data['timestamp'] as Timestamp; + final date = timestamp.toDate(); + return date.isAfter(today.subtract(const Duration(seconds: 1))); + }).toList(); + } else if (selectedDateFilter.value == 'this_month') { + result = result.where((doc) { + final data = doc.data() as Map; + if (data['timestamp'] == null) return false; + + final timestamp = data['timestamp'] as Timestamp; + final date = timestamp.toDate(); + return date + .isAfter(firstDayOfMonth.subtract(const Duration(seconds: 1))); + }).toList(); + } + + filteredResults.value = result; + updatePagination(); + } + + // Master filter application - sekarang hanya memicu reload jika filter kelas berubah + Future applyAllFilters() async { + // Jika filter kelas berubah, kita perlu memuat ulang data + if (selectedClass.value != 'Semua Kelas') { + await loadStudentResults(); + } else { + _applyFiltersWithoutClass(); + } + } + + // Filter triggers + void filterByCategory(String category) { + selectedCategoryFilter.value = category; + _applyFiltersWithoutClass(); + } + + // Filter by date (all_time, today, this_month) + void filterByDate(String dateFilter) { + selectedDateFilter.value = dateFilter; + _applyFiltersWithoutClass(); + } + + // Filter by class - ini memerlukan reload karena mengubah query dasar + void filterByClass(String classFilter) { + selectedClass.value = classFilter; + loadStudentResults(); // Reload data dengan query baru + } + + // Custom date filter methods + void setCustomDateRange(DateTime start, DateTime end) { + selectedStartDate.value = start; + selectedEndDate.value = end; + isCustomDateSelected.value = true; + selectedDateFilter.value = 'custom'; + _applyFiltersWithoutClass(); + } + + void resetCustomDateFilter() { + isCustomDateSelected.value = false; + } + + // Legacy filter - tetap dipertahankan untuk kompatibilitas + void filterResults(String filter) { + selectedFilter.value = filter; + + switch (filter) { + case 'career': + filteredResults.value = + studentResults.where((doc) => doc['isKerja'] == true).toList(); + break; + case 'study': + filteredResults.value = + studentResults.where((doc) => doc['isKerja'] == false).toList(); + break; + case 'all': + default: + filteredResults.value = studentResults; + break; + } + updatePagination(); + } + + // ========== LEGACY FILTER METHODS - DIPERTAHANKAN UNTUK KOMPATIBILITAS ========== + List _applyCategoryFilter( + List docs, String filter) { + switch (filter) { + case 'career': + return docs.where((doc) { + final data = doc.data() as Map; + return data['isKerja'] == true; + }).toList(); + case 'study': + return docs.where((doc) { + final data = doc.data() as Map; + return data['isKerja'] == false; + }).toList(); + case 'all': + default: + return docs; + } + } + + List _applyDateFilter( + List docs, String filter) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final firstDayOfMonth = DateTime(now.year, now.month, 1); + + switch (filter) { + case 'today': + return docs.where((doc) { + final data = doc.data() as Map; + if (data['timestamp'] == null) return false; + + final timestamp = data['timestamp'] as Timestamp; + final date = timestamp.toDate(); + return date.isAfter(today.subtract(const Duration(seconds: 1))); + }).toList(); + case 'this_month': + return docs.where((doc) { + final data = doc.data() as Map; + if (data['timestamp'] == null) return false; + + final timestamp = data['timestamp'] as Timestamp; + final date = timestamp.toDate(); + return date + .isAfter(firstDayOfMonth.subtract(const Duration(seconds: 1))); + }).toList(); + case 'all_time': + default: + return docs; + } + } + + List _applyCustomDateFilter( + List docs, DateTime startDate, DateTime endDate) { + final start = DateTime(startDate.year, startDate.month, startDate.day); + final end = DateTime(endDate.year, endDate.month, endDate.day, 23, 59, 59); + + return docs.where((doc) { + final data = doc.data() as Map; + if (data['timestamp'] == null) return false; + + final timestamp = data['timestamp'] as Timestamp; + final date = timestamp.toDate(); + return date.isAfter(start.subtract(const Duration(seconds: 1))) && + date.isBefore(end.add(const Duration(seconds: 1))); + }).toList(); + } + + // Optimasi: pakai cache untuk mencegah query ke database untuk setiap dokumen - diperbarui untuk struktur baru + Future> _applyClassFilter( + List docs, String filter) async { + if (filter == 'Semua Kelas') { + return docs; + } + + List filteredDocs = []; + + for (var doc in docs) { + final data = doc.data() as Map; + final userId = data['userId']; + + if (userId == null) continue; + + // Gunakan cache terlebih dahulu + if (_studentClassCache.containsKey(userId)) { + if (_studentClassCache[userId] == filter) { + filteredDocs.add(doc); + } + continue; + } + + // Jika tidak ada di cache, coba query database + try { + // Dapatkan schoolId dari cache atau shared preferences + String? studentSchoolId = _studentSchoolCache[userId]; + + if (studentSchoolId == null) { + // Jika tidak ada di cache, coba cari di semua sekolah + bool found = false; + final schoolsSnapshot = await _firestore.collection('schools').get(); + + for (var schoolDoc in schoolsSnapshot.docs) { + final studentDoc = await schoolDoc.reference + .collection('students') + .doc(userId) + .get(); + + if (studentDoc.exists) { + final studentData = studentDoc.data() as Map; + final studentClass = studentData['class']; + + // Update cache + _studentClassCache[userId] = studentClass ?? ''; + _studentSchoolCache[userId] = schoolDoc.id; + + if (studentClass == filter) { + filteredDocs.add(doc); + } + + found = true; + break; + } + } + + if (!found) { + // Siswa tidak ditemukan di database + continue; + } + } else { + // Jika ada schoolId di cache, gunakan untuk query langsung + final studentDoc = await _firestore + .collection('schools') + .doc(studentSchoolId) + .collection('students') + .doc(userId) + .get(); + + if (!studentDoc.exists) continue; + + final studentData = studentDoc.data() as Map; + final studentClass = studentData['class']; + + // Update cache + _studentClassCache[userId] = studentClass ?? ''; + + if (studentClass == filter) { + filteredDocs.add(doc); + } + } + } catch (e) { + print('Error fetching student class: $e'); + } + } + + return filteredDocs; + } + + // ========== UTILITY METHODS ========== + String formatTimestamp(dynamic timestamp) { + if (timestamp == null) return 'Tanggal tidak tersedia'; + + try { + DateTime dateTime = (timestamp as Timestamp).toDate(); + return '${dateTime.day}/${dateTime.month}/${dateTime.year} ${dateTime.hour}:${dateTime.minute.toString().padLeft(2, '0')}'; + } catch (e) { + return 'Format tanggal error'; + } + } + + void toggleDeveloperMode(bool value) { + isDeveloperMode.value = value; + developerMode = value; + } + + // ========== CHART DATA METHODS ========== + List getPieChartData() { + return [ + PieChartSectionData( + value: stats['careerRecommendations']!.toDouble(), + title: 'Karir', + color: Colors.deepPurple.shade700, + radius: 60, + titleStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + PieChartSectionData( + value: stats['studyRecommendations']!.toDouble(), + title: 'Kuliah', + color: Colors.indigo.shade900, + radius: 60, + titleStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ]; + } + + // ========== NAVIGATION METHODS ========== + void viewStudentDetail(DocumentSnapshot doc) { + Get.to(() => StudentResultDetailPage(document: doc)); + } + + Future signOut() async { + try { + await _auth.signOut(); + + // Clear shared preferences + final prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + + // Navigate to login selection page + Get.off(IntroPage()); + } catch (e) { + print('Error signing out: $e'); + Get.snackbar( + 'Error', + 'Gagal keluar: ${e.toString()}', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } + } +} + +class TeacherDashboardPage extends StatelessWidget { + const TeacherDashboardPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(TeacherDashboardController()); + final theme = Theme.of(context); + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: SafeArea( + child: Column( + children: [ + _buildAppBar(controller), + // Developer Mode Card with improved visual cues + + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + child: Obx(() => controller.isLoading.value + ? _buildLoadingView() + : _buildDashboardContent(controller, theme)), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildAppBar(TeacherDashboardController controller) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + padding: const EdgeInsets.all(8), + child: Icon( + Icons.psychology, + size: 30, + color: Colors.blue.shade700, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'EduGuide', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Obx(() => Text( + 'Selamat datang, ${controller.teacherName.value}', + style: const TextStyle( + fontSize: 14, + color: Colors.white70, + ), + )), + ], + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + onPressed: () { + // Refresh data + controller.loadStudentResults(); + }, + icon: const Icon(Icons.refresh, color: Colors.white), + tooltip: 'Refresh Data', + ), + ), + const SizedBox(width: 8), + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + onPressed: () { + // Show sign out confirmation + Get.dialog( + AlertDialog( + title: const Text('Konfirmasi'), + content: const Text('Apakah Anda yakin ingin keluar?'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'Batal', + style: TextStyle(color: Colors.grey.shade700), + ), + ), + ElevatedButton( + onPressed: () { + Get.back(); + controller.signOut(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + ), + child: const Text('Keluar', + style: TextStyle(color: Colors.white)), + ), + ], + ), + ); + }, + icon: const Icon(Icons.logout, color: Colors.white), + tooltip: 'Keluar', + ), + ), + ], + ), + ); + } + + Widget _buildLoadingView() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.blue.shade700), + ), + const SizedBox(height: 16), + Text( + 'Memuat data...', + style: TextStyle( + fontSize: 16, + color: Colors.blue.shade700, + ), + ), + ], + ), + ); + } + + Widget _buildDashboardContent( + TeacherDashboardController controller, ThemeData theme) { + return RefreshIndicator( + onRefresh: controller.loadStudentResults, + color: Colors.blue.shade700, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Row( + children: [ + Icon(Icons.analytics, + size: 18, color: Colors.blue.shade800), + const SizedBox(width: 8), + Text( + 'Akses Data Aplikasi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.grey.shade300, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.indigo.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.code, + size: 20, + color: Colors.orange.shade800, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Mode Developer', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + const SizedBox(height: 2), + const Text( + 'Akses data & model AI', + style: TextStyle( + fontSize: 12, + color: Colors.black54, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + const Text( + 'Aktifkan mode pengembang untuk melihat data dan validasi model forward chaining yang digunakan dalam sistem rekomendasi.', + style: TextStyle( + fontSize: 13, + color: Colors.black54, + height: 1.4, + ), + ), + const SizedBox(height: 12), + + Obx(() => Container( + decoration: BoxDecoration( + color: controller.isDeveloperMode.value + ? Colors.indigo.shade50 + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: SwitchListTile( + title: Text( + controller.isDeveloperMode.value + ? 'Mode Pengembang Aktif' + : 'Mode Pengembang Nonaktif', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: controller.isDeveloperMode.value + ? Colors.indigo.shade800 + : Colors.black54, + ), + ), + value: controller.isDeveloperMode.value, + onChanged: (value) => + controller.toggleDeveloperMode(value), + activeColor: Colors.indigo, + activeTrackColor: Colors.indigo.shade300, + inactiveThumbColor: Colors.grey.shade400, + inactiveTrackColor: Colors.grey.shade300, + secondary: Icon( + controller.isDeveloperMode.value + ? Icons.visibility + : Icons.visibility_off, + color: controller.isDeveloperMode.value + ? Colors.indigo + : Colors.grey.shade500, + size: 20, + ), + dense: true, + ), + )), + const SizedBox(height: 12), + + // Developer mode button with attention-grabbing styling + Obx(() => controller.isDeveloperMode.value + ? Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.orange.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ElevatedButton( + onPressed: () => + Get.to(() => const DevDataViewerPage()), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade600, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 16), + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.data_array, size: 22), + SizedBox(width: 12), + Text( + 'Data Viewer', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ) + : const SizedBox.shrink()), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 12), + _buildStatisticsCards(controller), + const SizedBox(height: 24), + _buildFilterButtons(controller), + const SizedBox(height: 24), + _buildStudentList(controller), + ], + ), + ); + } + + Widget _buildStatisticsCards(TeacherDashboardController controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan padding yang konsisten + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Row( + children: [ + Icon(Icons.analytics, size: 18, color: Colors.blue.shade800), + const SizedBox(width: 8), + Text( + 'Statistik Hasil Kuisioner', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + + // Row untuk Chart dan Stat Cards + LayoutBuilder( + builder: (context, constraints) { + // Tentukan ukuran chart optimal berdasarkan lebar yang tersedia + final isWideScreen = constraints.maxWidth > 600; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Chart Container - flex yang lebih besar pada layar lebar + Expanded( + flex: isWideScreen ? 3 : 2, + child: Container( + height: 210, // Sedikit ditambah untuk menampung legend + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Chart title + Row( + children: [ + Icon(Icons.pie_chart, + size: 14, color: Colors.blue.shade800), + const SizedBox(width: 6), + Expanded( + child: Text( + 'Distribusi Jenis Rekomendasi', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + + // Chart area + Expanded( + child: Obx(() { + final hasData = + controller.stats['totalStudents']! > 0; + + return hasData + ? PieChartWidget(controller: controller) + : const EmptyChartMessage(); + }), + ), + + // Legend - hanya ditampilkan jika ada data + Obx(() { + final hasData = + controller.stats['totalStudents']! > 0; + + return hasData + ? Padding( + padding: + const EdgeInsets.only(top: 8, bottom: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLegendItem( + 'Karir', Colors.blue.shade700), + const SizedBox(width: 16), + _buildLegendItem( + 'Kuliah', Colors.indigo.shade800), + ], + ), + ) + : const SizedBox.shrink(); + }), + ], + ), + ), + ), + + const SizedBox(width: 12), + + // Stat Cards Container + Expanded( + flex: + isWideScreen ? 2 : 3, // Sesuaikan flex berdasarkan layar + child: Container( + height: + 210, // Tinggi yang sama dengan chart untuk konsistensi + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Stat card untuk Total Siswa + Expanded( + child: _buildStatCard( + title: 'Total Siswa', + value: controller.stats['totalStudents'].toString(), + icon: Icons.people, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 8), + + // Stat card untuk Rekomendasi Karir + Expanded( + child: _buildStatCard( + title: 'Rekomendasi Karir', + value: controller.stats['careerRecommendations'] + .toString(), + icon: Icons.work, + color: Colors.blue.shade700, + ), + ), + const SizedBox(height: 8), + + // Stat card untuk Rekomendasi Kuliah + Expanded( + child: _buildStatCard( + title: 'Rekomendasi Kuliah', + value: controller.stats['studyRecommendations'] + .toString(), + icon: Icons.school, + color: Colors.indigo.shade800, + ), + ), + ], + ), + ), + ), + ], + ); + }, + ), + ], + ); + } + + Widget _buildLegendItem(String label, Color color) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade700, + ), + ), + ], + ); + } + + Widget _buildStatCard({ + required String title, + required String value, + required IconData icon, + required Color color, + }) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: color, + size: 16, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: color, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildFilterButtons(TeacherDashboardController controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.filter_list, size: 20, color: Colors.blue.shade800), + const SizedBox(width: 8), + Text( + 'Daftar Hasil Kuisioner Siswa', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Category Filters + Text( + 'Filter Kategori:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + _buildFilterButton( + label: 'Semua', + value: 'all', + controller: controller, + icon: Icons.list_alt, + filterType: 'category', + ), + const SizedBox(width: 12), + _buildFilterButton( + label: 'Karir', + value: 'career', + controller: controller, + icon: Icons.work, + filterType: 'category', + ), + const SizedBox(width: 12), + _buildFilterButton( + label: 'Kuliah', + value: 'study', + controller: controller, + icon: Icons.school, + filterType: 'category', + ), + ], + ), + + const SizedBox(height: 20), + + // Date Filters + Text( + 'Filter Waktu:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + _buildFilterButton( + label: 'Semua Waktu', + value: 'all_time', + controller: controller, + icon: Icons.av_timer, + filterType: 'date', + ), + const SizedBox(width: 12), + _buildFilterButton( + label: 'Hari Ini', + value: 'today', + controller: controller, + icon: Icons.today, + filterType: 'date', + ), + const SizedBox(width: 12), + _buildFilterButton( + label: 'Bulan Ini', + value: 'this_month', + controller: controller, + icon: Icons.date_range, + filterType: 'date', + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _buildDatePickerButton(controller), + ), + ], + ), + Obx(() => controller.isCustomDateSelected.value + ? Padding( + padding: const EdgeInsets.only(top: 8), + child: Container( + padding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.blue.shade200), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.date_range, + size: 16, color: Colors.blue), + const SizedBox(width: 8), + Text( + '${controller.selectedStartDate.value.day}/${controller.selectedStartDate.value.month}/${controller.selectedStartDate.value.year} - ' + '${controller.selectedEndDate.value.day}/${controller.selectedEndDate.value.month}/${controller.selectedEndDate.value.year}', + style: TextStyle( + fontSize: 12, + color: Colors.blue.shade800, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + InkWell( + onTap: () { + controller.resetCustomDateFilter(); + controller.filterByDate('all_time'); + }, + child: Icon(Icons.close, + size: 16, color: Colors.red.shade700), + ), + ], + ), + ), + ) + : const SizedBox.shrink()), + + const SizedBox(height: 20), + + // Class Filter DropDown + Text( + 'Filter Kelas:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.blue.shade200, width: 1), + ), + child: Obx(() => DropdownButtonHideUnderline( + child: DropdownButton( + value: controller.selectedClass.value, + isExpanded: true, + icon: Icon(Icons.keyboard_arrow_down, + color: Colors.blue.shade700), + elevation: 16, + style: TextStyle(color: Colors.blue.shade700), + onChanged: (String? newValue) { + controller.filterByClass(newValue!); + }, + items: controller.availableClasses + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ); + }).toList(), + ), + )), + ), + ], + ); + } + + Widget _buildDatePickerButton(TeacherDashboardController controller) { + return ElevatedButton( + onPressed: () async { + // Tampilkan date range picker + final DateTimeRange? picked = await showDateRangePicker( + context: Get.context!, + firstDate: DateTime(2020), + lastDate: DateTime.now(), + initialDateRange: DateTimeRange( + start: controller.selectedStartDate.value, + end: controller.selectedEndDate.value, + ), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Colors.blue.shade700, + onPrimary: Colors.white, + onSurface: Colors.black, + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: Colors.blue.shade700, + ), + ), + ), + child: child!, + ); + }, + ); + + if (picked != null) { + controller.setCustomDateRange(picked.start, picked.end); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: controller.isCustomDateSelected.value + ? Colors.blue.shade700 + : Colors.white, + foregroundColor: controller.isCustomDateSelected.value + ? Colors.white + : Colors.blue.shade700, + elevation: controller.isCustomDateSelected.value ? 4 : 1, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + side: BorderSide( + color: controller.isCustomDateSelected.value + ? Colors.transparent + : Colors.blue.shade200, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.calendar_month, size: 16), + const SizedBox(width: 8), + const Text( + 'Pilih Tanggal', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + + Widget _buildFilterButton({ + required String label, + required String value, + required TeacherDashboardController controller, + required IconData icon, + required String filterType, + }) { + return Obx(() { + final isSelected = filterType == 'category' + ? controller.selectedCategoryFilter.value == value + : controller.selectedDateFilter.value == value; + + // Jika filter tanggal kustom aktif, nonaktifkan tombol filter tanggal lainnya + final isDateDisabled = filterType == 'date' && + controller.isCustomDateSelected.value && + value != 'custom'; + + return Expanded( + child: ElevatedButton( + onPressed: isDateDisabled + ? null + : () { + if (filterType == 'category') { + controller.filterByCategory(value); + } else { + controller.resetCustomDateFilter(); + controller.filterByDate(value); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: isSelected ? Colors.blue.shade700 : Colors.white, + foregroundColor: isSelected ? Colors.white : Colors.blue.shade700, + disabledBackgroundColor: Colors.grey.shade200, + disabledForegroundColor: Colors.grey.shade600, + elevation: isSelected ? 4 : 1, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + side: BorderSide( + color: isSelected || isDateDisabled + ? Colors.transparent + : Colors.blue.shade200, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + }); + } + + Widget _buildStudentList(TeacherDashboardController controller) { + return Obx(() { + if (controller.filteredResults.isEmpty) { + return Container( + height: 200, + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: 48, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Belum ada data hasil kuisioner', + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade600, + ), + ), + ], + ), + ); + } + + // Panggil updatePagination jika belum dipanggil + if (controller.paginatedResults.isEmpty) { + controller.updatePagination(); + } + + return Column( + children: [ + // Informasi pagination + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Menampilkan ${controller.paginatedResults.length} dari ${controller.filteredResults.length} hasil', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + // Dropdown untuk mengatur jumlah item per halaman + Row( + children: [ + Text( + 'Tampilkan: ', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(4), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: controller.itemsPerPage.value, + items: [5, 10, 20, 50].map((int value) { + return DropdownMenuItem( + value: value, + child: Text('$value'), + ); + }).toList(), + onChanged: (int? newValue) { + if (newValue != null) { + controller.setItemsPerPage(newValue); + } + }, + ), + ), + ), + ], + ), + ], + ), + ), + + // Daftar siswa + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.paginatedResults.length, + itemBuilder: (context, index) { + final doc = controller.paginatedResults[index]; + final data = doc.data() as Map; + final isKerja = data['isKerja'] ?? false; + final recommendations = List>.from( + data['recommendations'] ?? []); + + // Get top recommendation + String topRecommendation = 'Tidak ada'; + if (recommendations.isNotEmpty) { + topRecommendation = recommendations[0]['title'] ?? 'Tidak ada'; + } + + return Card( + margin: const EdgeInsets.only(bottom: 16), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: InkWell( + onTap: () => controller.viewStudentDetail(doc), + borderRadius: BorderRadius.circular(15), + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: isKerja + ? Colors.blue.shade50 + : Colors.indigo.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + isKerja ? Icons.work : Icons.school, + color: isKerja + ? Colors.blue.shade400 + : Colors.indigo.shade400, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data['userName'] ?? + data['userEmail'] ?? + 'Siswa', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + controller + .formatTimestamp(data['timestamp']), + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: isKerja + ? Colors.blue.shade100 + : Colors.indigo.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + isKerja ? 'Karir' : 'Kuliah', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isKerja + ? Colors.blue.shade700 + : Colors.indigo.shade700, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Rekomendasi Utama', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + Text( + topRecommendation, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 16), + Container( + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: IconButton( + onPressed: () => + controller.viewStudentDetail(doc), + icon: const Icon(Icons.visibility), + color: Colors.blue.shade700, + tooltip: 'Lihat Detail', + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ), + + // Kontrol pagination yang diperbaiki + if (controller.totalPages.value > 1) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Tombol Previous + IconButton( + onPressed: controller.currentPage.value > 0 + ? () => controller.previousPage() + : null, + icon: const Icon(Icons.chevron_left), + color: Colors.blue.shade700, + disabledColor: Colors.grey.shade400, + ), + + // Indikator halaman dengan smart pagination + Expanded( + child: Center( + child: _buildSmartPagination(controller), + ), + ), + + // Tombol Next + IconButton( + onPressed: controller.currentPage.value < + controller.totalPages.value - 1 + ? () => controller.nextPage() + : null, + icon: const Icon(Icons.chevron_right), + color: Colors.blue.shade700, + disabledColor: Colors.grey.shade400, + ), + ], + ), + ), + ], + ); + }); + } + +// Fungsi untuk membuat tombol halaman dengan pendekatan smart pagination + Widget _buildSmartPagination(TeacherDashboardController controller) { + // Variabel yang dibutuhkan + final int currentPage = controller.currentPage.value; + final int totalPages = controller.totalPages.value; + + // Fungsi untuk membuat tombol halaman + Widget pageButton(int index) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 2), + child: ElevatedButton( + onPressed: () => controller.goToPage(index), + style: ElevatedButton.styleFrom( + backgroundColor: + currentPage == index ? Colors.blue.shade700 : Colors.white, + foregroundColor: + currentPage == index ? Colors.white : Colors.blue.shade700, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: currentPage == index + ? Colors.transparent + : Colors.blue.shade200, + ), + ), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 0, + ), + minimumSize: const Size(32, 32), + ), + child: Text('${index + 1}'), + ), + ); + } + + // Widget untuk ellipsis + Widget ellipsis() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + '...', + style: TextStyle( + color: Colors.grey.shade600, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + // Buat list untuk tombol-tombol yang akan ditampilkan + List paginationItems = []; + + // Jika total halaman <= 7, tampilkan semua halaman + if (totalPages <= 7) { + for (int i = 0; i < totalPages; i++) { + paginationItems.add(pageButton(i)); + } + } + // Jika halaman lebih dari 7, gunakan smart pagination + else { + // Selalu tampilkan halaman pertama + paginationItems.add(pageButton(0)); + + // Logika untuk menentukan halaman mana yang ditampilkan + if (currentPage < 3) { + // Dekat dengan awal: tampilkan halaman 0-4 kemudian ellipsis dan halaman terakhir + for (int i = 1; i <= 3; i++) { + paginationItems.add(pageButton(i)); + } + paginationItems.add(ellipsis()); + paginationItems.add(pageButton(totalPages - 1)); + } else if (currentPage >= totalPages - 3) { + // Dekat dengan akhir: tampilkan halaman pertama, ellipsis, dan 4 halaman terakhir + paginationItems.add(ellipsis()); + for (int i = totalPages - 4; i < totalPages; i++) { + paginationItems.add(pageButton(i)); + } + } else { + // Di tengah: tampilkan halaman pertama, ellipsis, halaman saat ini dan tetangganya, ellipsis, halaman terakhir + paginationItems.add(ellipsis()); + for (int i = currentPage - 1; i <= currentPage + 1; i++) { + paginationItems.add(pageButton(i)); + } + paginationItems.add(ellipsis()); + paginationItems.add(pageButton(totalPages - 1)); + } + } + + // Tampilkan dalam SingleChildScrollView untuk mencegah overflow + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: paginationItems, + ), + ); + } +} + +class PieChartWidget extends StatelessWidget { + final TeacherDashboardController controller; + + const PieChartWidget({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + // Hitung ukuran chart berdasarkan constraints + final size = constraints.maxWidth < constraints.maxHeight + ? constraints.maxWidth + : constraints.maxHeight; + + return Center( + child: SizedBox( + width: size * 0.9, // Sedikit lebih kecil dari ruang yang tersedia + height: size * 0.9, + child: PieChart( + PieChartData( + centerSpaceRadius: size * 0.15, + sectionsSpace: 1, + borderData: FlBorderData(show: false), + centerSpaceColor: Colors.transparent, + sections: controller.getPieChartData().map((section) { + return section.copyWith( + radius: size * 0.35, + showTitle: false, + titlePositionPercentageOffset: 0, + ); + }).toList(), + ), + swapAnimationDuration: const Duration(milliseconds: 800), + ), + ), + ); + }, + ); + } +} + +// Widget untuk menampilkan pesan saat tidak ada data +class EmptyChartMessage extends StatelessWidget { + const EmptyChartMessage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.bar_chart_outlined, + size: 36, + color: Colors.grey.shade300, + ), + const SizedBox(height: 4), + Text( + 'Belum ada data', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + ), + ), + ], + ), + ); + } +} + +// Widget untuk item legenda yang disederhanakan dan konsisten +Widget _buildLegendItem(String label, Color color) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + ), + ], + ); +} + +class StudentResultDetailPage extends StatefulWidget { + final DocumentSnapshot document; + + const StudentResultDetailPage({ + Key? key, + required this.document, + }) : super(key: key); + + @override + State createState() => + _StudentResultDetailPageState(); +} + +class _StudentResultDetailPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final GlobalKey _headerKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 4, vsync: this); + + // Delay to allow build to complete before animations start + Future.delayed(Duration.zero, () { + _startInitialAnimations(); + }); + } + + void _startInitialAnimations() { + // Animation logic for initial page load + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final data = widget.document.data() as Map; + final isKerja = data['isKerja'] ?? false; + final userName = data['userName'] ?? data['userEmail'] ?? 'Siswa'; + final recommendations = + List>.from(data['recommendations'] ?? []); + final userAnswers = + List>.from(data['userAnswers'] ?? []); + Map workingMemory = {}; + if (data['workingMemory'] != null) { + if (data['workingMemory'] is Map) { + workingMemory = Map.from(data['workingMemory']); + } else if (data['workingMemory'] is List) { + // Convert list to map with indices as keys + final list = List.from(data['workingMemory']); + for (int i = 0; i < list.length; i++) { + workingMemory['item_$i'] = list[i]; + } + } + } + + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverAppBar( + pinned: true, + expandedHeight: 180.0, + backgroundColor: Colors.blue.shade800, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Get.back(), + ), + actions: [ + IconButton( + icon: const Icon(Icons.share, color: Colors.white), + onPressed: () { + Get.snackbar( + 'Info', + 'Fitur berbagi hasil sedang dalam pengembangan', + backgroundColor: Colors.blue.shade50, + colorText: Colors.blue.shade700, + snackPosition: SnackPosition.BOTTOM, + animationDuration: const Duration(milliseconds: 800), + duration: const Duration(seconds: 3), + ); + }, + ), + ], + flexibleSpace: FlexibleSpaceBar( + title: Text( + 'Detail Hasil $userName', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.white), + ), + background: Hero( + tag: 'student_header_${widget.document.id}', + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: Stack( + children: [ + Positioned( + right: -50, + bottom: -50, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 1500), + curve: Curves.elasticOut, + builder: (context, value, child) { + return Opacity( + opacity: 0.2 * value, + child: Transform.scale( + scale: value, + child: Icon( + isKerja ? Icons.work : Icons.school, + size: 200, + color: Colors.white, + ), + ), + ); + }, + ), + ), + Positioned( + top: 70, + left: 20, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 800), + curve: Curves.easeOutCubic, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(-20 * (1 - value), 0), + child: Row( + children: [ + CircleAvatar( + backgroundColor: + Colors.white.withOpacity(0.9), + radius: 30, + child: Icon( + Icons.person, + color: Colors.blue.shade700, + size: 30, + ), + ), + const SizedBox(width: 15), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + data['userName'] ?? 'Siswa', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + Text( + data['userEmail'] ?? '', + style: TextStyle( + color: + Colors.white.withOpacity(0.8), + fontSize: 14, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ), + SliverPersistentHeader( + delegate: _SliverAppBarDelegate( + TabBar( + controller: _tabController, + tabs: [ + Tab(icon: Icon(Icons.person), text: 'Profil'), + Tab(icon: Icon(Icons.recommend), text: 'Rekomendasi'), + Tab(icon: Icon(Icons.question_answer), text: 'Jawaban'), + Tab(icon: Icon(Icons.memory), text: 'Working Memory'), + ], + labelColor: Colors.blue.shade700, + unselectedLabelColor: Colors.grey.shade600, + indicatorColor: Colors.blue.shade700, + indicatorWeight: 3, + ), + ), + pinned: true, + ), + ]; + }, + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.grey.shade50, + Colors.blue.shade50, + ], + stops: const [0.7, 1.0], + ), + ), + child: TabBarView( + controller: _tabController, + children: [ + _buildProfileTab(data), + _buildRecommendationsTab(recommendations, isKerja), + _buildAnswersTab(userAnswers), + _buildWorkingMemoryTab(workingMemory), + ], + ), + ), + ), + ); + } + + Widget _buildProfileTab(Map data) { + final timestamp = data['timestamp'] is Timestamp + ? (data['timestamp'] as Timestamp).toDate() + : DateTime.now(); + final formattedDate = DateFormat('dd MMMM yyyy, HH:mm').format(timestamp); + final isKerja = data['isKerja'] ?? false; + final answeredQuestions = data['answeredQuestions'] ?? 0; + final totalQuestions = data['totalQuestions'] ?? 0; + final completionPercentage = totalQuestions > 0 + ? (answeredQuestions / totalQuestions * 100).round() + : 0; + + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('Informasi Siswa', Icons.info), + const SizedBox(height: 12), + _buildStudentInfoCard(data), + const SizedBox(height: 24), + _buildSectionHeader('Progres Pengerjaan', Icons.insights), + const SizedBox(height: 12), + _buildProgressCard( + completionPercentage, answeredQuestions, totalQuestions), + const SizedBox(height: 24), + _buildSectionHeader('Statistik', Icons.bar_chart), + const SizedBox(height: 12), + _buildStatisticsCard(data), + ], + ), + ); + } + + Widget _buildProgressCard(int percentage, int answered, int total) { + return Card( + elevation: 4, + shadowColor: Colors.blue.shade100, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: percentage / 100), + duration: const Duration(milliseconds: 1500), + curve: Curves.easeOutCubic, + builder: (context, value, child) { + return Column( + children: [ + Stack( + alignment: Alignment.center, + children: [ + SizedBox( + height: 120, + width: 120, + child: CircularProgressIndicator( + value: value, + strokeWidth: 12, + backgroundColor: Colors.grey.shade200, + valueColor: AlwaysStoppedAnimation( + Colors.blue.shade500, + ), + ), + ), + Column( + children: [ + Text( + '${(value * 100).toInt()}%', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + Text( + 'Selesai', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildProgressStat( + 'Terjawab', + answered.toString(), + Icons.check_circle, + Colors.green, + ), + _buildProgressStat( + 'Total', + total.toString(), + Icons.assignment, + Colors.blue, + ), + ], + ), + ], + ); + }, + ), + ], + ), + ), + ); + } + + Widget _buildProgressStat( + String label, String value, IconData icon, MaterialColor color) { + return Column( + children: [ + Icon( + icon, + color: color.shade400, + size: 28, + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: color.shade700, + ), + ), + Text( + label, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ); + } + + Widget _buildStatisticsCard(Map data) { + // Sample data for demonstration + final recommendations = + List>.from(data['recommendations'] ?? []); + final scores = + recommendations.map((r) => (r['score'] ?? 0) as num).toList(); + + return Card( + elevation: 4, + shadowColor: Colors.blue.shade100, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Distribusi Skor Rekomendasi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 16), + SizedBox( + height: 200, + child: scores.isEmpty + ? Center( + child: Text( + 'Tidak ada data rekomendasi', + style: TextStyle( + color: Colors.grey.shade600, + ), + ), + ) + : BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: 100, + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData(), + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + final index = value.toInt(); + if (index >= 0 && + index < recommendations.length) { + return Text( + 'R${index + 1}', + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ); + } + return const SizedBox(); + }, + reservedSize: 22, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + if (value == 0 || value == 50 || value == 100) { + return Text( + '${value.toInt()}%', + style: const TextStyle( + color: Colors.black, + fontSize: 10, + ), + ); + } + return const SizedBox(); + }, + interval: 25, + reservedSize: 30, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData( + show: false, + ), + barGroups: scores.asMap().entries.map((entry) { + final index = entry.key; + final value = entry.value; + return BarChartGroupData( + x: index, + barRods: [ + BarChartRodData( + toY: value.toDouble(), + gradient: LinearGradient( + colors: [ + Colors.blue.shade300, + Colors.blue.shade700 + ], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ), + width: 20, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ), + ], + ); + }).toList(), + gridData: FlGridData( + show: true, + horizontalInterval: 25, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey.shade200, + strokeWidth: 1, + ); + }, + ), + ), + swapAnimationDuration: const Duration(milliseconds: 800), + ), + ), + ], + ), + ), + ); + } + + Widget _buildStudentInfoCard(Map data) { + final timestamp = data['timestamp'] is Timestamp + ? (data['timestamp'] as Timestamp).toDate() + : DateTime.now(); + final formattedDate = DateFormat('dd MMMM yyyy, HH:mm').format(timestamp); + final isKerja = data['isKerja'] ?? false; + + return Card( + elevation: 4, + shadowColor: Colors.blue.shade100, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + backgroundColor: Colors.blue.shade50, + radius: 24, + child: Icon( + Icons.person, + color: Colors.blue.shade700, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data['userName'] ?? 'Siswa', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + data['userEmail'] ?? '', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + Row( + children: [ + _buildInfoItem( + icon: Icons.calendar_today, + label: 'Tanggal', + value: formattedDate, + ), + _buildInfoItem( + icon: isKerja ? Icons.work : Icons.school, + label: 'Jenis Rekomendasi', + value: isKerja ? 'Karir' : 'Kuliah', + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + _buildInfoItem( + icon: Icons.question_answer, + label: 'Total Pertanyaan', + value: data['totalQuestions']?.toString() ?? '0', + ), + _buildInfoItem( + icon: Icons.check_circle, + label: 'Pertanyaan Dijawab', + value: data['answeredQuestions']?.toString() ?? '0', + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildRecommendationsTab( + List> recommendations, bool isKerja) { + return recommendations.isEmpty + ? _buildEmptyCenteredMessage( + 'Tidak ada rekomendasi yang tersedia', + Icons.search_off, + ) + : ListView.builder( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: recommendations.length + 1, // +1 for header + itemBuilder: (context, index) { + if (index == 0) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader( + 'Hasil Rekomendasi', + isKerja ? Icons.work : Icons.school, + ), + const SizedBox(height: 16), + ], + ); + } + + final recommendationIndex = index - 1; + final recommendation = recommendations[recommendationIndex]; + return _buildRecommendationCard( + recommendation, recommendationIndex, isKerja); + }, + ); + } + + Widget _buildRecommendationCard( + Map recommendation, int index, bool isKerja) { + final score = recommendation['score']?.toString() ?? '0'; + final careers = List.from(recommendation['careers'] ?? []); + final majors = List.from(recommendation['majors'] ?? []); + final rules = List.from(recommendation['rules'] ?? []); + final title = recommendation['title'] ?? 'Rekomendasi ${index + 1}'; + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300 + (index * 150)), + curve: Curves.easeOutCubic, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(0, 20 * (1 - value)), + child: Card( + margin: const EdgeInsets.only(bottom: 16), + elevation: 4, + shadowColor: Colors.blue.shade100, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: ExpansionTile( + initiallyExpanded: index == 0, // First one expanded by default + tilePadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + leading: Hero( + tag: 'recommendation_$index', + child: CircleAvatar( + backgroundColor: Colors.blue.shade50, + radius: 20, + child: Text( + '${index + 1}', + style: TextStyle( + color: Colors.blue.shade700, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + title: Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + TweenAnimationBuilder( + tween: Tween( + begin: 0.0, end: double.parse(score) / 100), + duration: const Duration(milliseconds: 1000), + builder: (context, value, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Skor: ${(value * 100).toInt()}%', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + const SizedBox(height: 4), + LinearProgressIndicator( + value: value, + backgroundColor: Colors.grey.shade200, + valueColor: AlwaysStoppedAnimation( + value < 0.3 + ? Colors.red.shade400 + : value < 0.7 + ? Colors.orange.shade400 + : Colors.green.shade400, + ), + minHeight: 5, + borderRadius: BorderRadius.circular(10), + ), + ], + ); + }, + ), + ], + ), + children: [ + const Divider(), + const SizedBox(height: 16), + if (isKerja && careers.isNotEmpty) ...[ + _buildSectionTitle('Karir yang Direkomendasikan'), + const SizedBox(height: 8), + _buildChipList(careers), + const SizedBox(height: 16), + ], + if (!isKerja && majors.isNotEmpty) ...[ + _buildSectionTitle('Jurusan yang Direkomendasikan'), + const SizedBox(height: 8), + _buildChipList(majors), + const SizedBox(height: 16), + ], + if (rules.isNotEmpty) ...[ + _buildSectionTitle('Aturan yang Terpenuhi'), + const SizedBox(height: 8), + _buildRulesList(rules), + ], + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildAnswersTab(List> userAnswers) { + return userAnswers.isEmpty + ? _buildEmptyCenteredMessage( + 'Tidak ada jawaban yang tersedia', + Icons.question_mark, + ) + : ListView.builder( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: userAnswers.length + 1, // +1 for header + itemBuilder: (context, index) { + if (index == 0) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader('Jawaban Siswa', Icons.question_answer), + const SizedBox(height: 16), + ], + ); + } + + final answerIndex = index - 1; + return _buildAnswerCard(userAnswers[answerIndex], answerIndex); + }, + ); + } + + Widget _buildAnswerCard(Map answer, int index) { + final questionText = answer['question'] ?? 'Pertanyaan tidak tersedia'; + final userAnswer = answer['answer'] ?? 'Tidak dijawab'; + final programName = answer['programName'] ?? ''; + final bobot = answer['bobot']?.toString() ?? '0'; + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300 + (index * 100)), + curve: Curves.easeOutCubic, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(30 * (1 - value), 0), + child: Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 3, + shadowColor: Colors.blue.shade100, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 28, + height: 28, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.blue.shade100.withOpacity(0.3), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + '${index + 1}', + style: TextStyle( + color: Colors.blue.shade700, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + questionText, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: userAnswer == 'Ya' + ? Colors.green.shade50 + : userAnswer == 'Tidak' + ? Colors.red.shade50 + : Colors.grey.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: userAnswer == 'Ya' + ? Colors.green.shade200 + : userAnswer == 'Tidak' + ? Colors.red.shade200 + : Colors.grey.shade300, + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + 'Jawaban: $userAnswer', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: userAnswer == 'Ya' + ? Colors.green.shade700 + : userAnswer == 'Tidak' + ? Colors.red.shade700 + : Colors.grey.shade700, + ), + ), + ), + ), + ], + ), + if (programName.isNotEmpty || bobot != '0') ...[ + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + if (programName.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + programName, + style: TextStyle( + fontSize: 12, + color: Colors.blue.shade700, + ), + ), + ), + if (bobot != '0') + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.amber.shade50, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + 'Bobot: $bobot', + style: TextStyle( + fontSize: 12, + color: Colors.amber.shade800, + ), + ), + ), + ], + ), + ], + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildWorkingMemoryTab(Map workingMemory) { + return workingMemory.isEmpty + ? _buildEmptyCenteredMessage( + 'Working memory tidak tersedia', + Icons.memory_outlined, + ) + : ListView.builder( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: workingMemory.length + 1, // +1 for header + itemBuilder: (context, index) { + if (index == 0) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader( + 'Working Memory', + Icons.memory, + tooltip: + 'Data memori kerja yang digunakan saat proses inferensi', + ), + const SizedBox(height: 16), + Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + flex: 3, + child: Text( + 'Variabel', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + 'Nilai', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + ], + ); + } + + final memoryIndex = index - 1; + final entry = workingMemory.entries.elementAt(memoryIndex); + return _buildWorkingMemoryItem( + entry.key, entry.value, memoryIndex); + }, + ); + } + + Widget _buildWorkingMemoryItem(String key, dynamic value, int index) { + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300 + (index * 50)), + curve: Curves.easeOutCubic, + builder: (context, animValue, child) { + return Opacity( + opacity: animValue, + child: Transform.translate( + offset: Offset(0, 20 * (1 - animValue)), + child: Card( + margin: const EdgeInsets.only(bottom: 8), + elevation: 2, + shadowColor: Colors.blue.shade100, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + flex: 3, + child: Text( + key, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + Expanded( + flex: 2, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: value is bool && value == true + ? Colors.green.shade50 + : value is bool && value == false + ? Colors.red.shade50 + : Colors.blue.shade50, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + value.toString(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: value is bool && value == true + ? Colors.green.shade700 + : value is bool && value == false + ? Colors.red.shade700 + : Colors.blue.shade700, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildEmptyCenteredMessage(String message, IconData icon) { + return Center( + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 800), + curve: Curves.elasticOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.scale( + scale: 0.8 + (0.2 * value), + child: Container( + width: 250, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.blue.shade100.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 60, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + message, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } + + Widget _buildInfoItem({ + required IconData icon, + required String label, + required String value, + }) { + return Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + icon, + size: 16, + color: Colors.blue.shade400, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSectionHeader(String title, IconData icon, {String? tooltip}) { + return Row( + children: [ + Icon( + icon, + color: Colors.blue.shade700, + size: 20, + ), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + if (tooltip != null) ...[ + const SizedBox(width: 8), + Tooltip( + message: tooltip, + child: Icon( + Icons.info_outline, + size: 16, + color: Colors.grey.shade600, + ), + ), + ], + ], + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.blue.shade700, + ), + ); + } + + Widget _buildChipList(List items) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: items.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 600 + (index * 50)), + curve: Curves.easeOutCubic, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.scale( + scale: 0.8 + (0.2 * value), + child: Chip( + label: Text( + item, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + backgroundColor: Colors.blue.shade50, + labelStyle: TextStyle(color: Colors.blue.shade700), + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + elevation: 1, + shadowColor: Colors.blue.shade100, + ), + ), + ); + }, + ); + }).toList(), + ); + } + + Widget _buildRulesList(List rules) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: rules.asMap().entries.map((entry) { + final index = entry.key; + final rule = entry.value; + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300 + (index * 100)), + curve: Curves.easeOutCubic, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(20 * (1 - value), 0), + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.check_circle, + size: 16, + color: Colors.green.shade600, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + rule, + style: const TextStyle(fontSize: 14), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }).toList(), + ); + } +} + +class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { + final TabBar tabBar; + + _SliverAppBarDelegate(this.tabBar); + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide( + color: Colors.grey.shade200, + width: 1, + ), + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.05), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: tabBar, + ); + } + + @override + double get maxExtent => tabBar.preferredSize.height; + + @override + double get minExtent => tabBar.preferredSize.height; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { + return false; + } +} diff --git a/lib/app/views/developer/page/page_developer_viewer.dart b/lib/app/views/developer/page/page_developer_viewer.dart new file mode 100644 index 0000000..a51ee64 --- /dev/null +++ b/lib/app/views/developer/page/page_developer_viewer.dart @@ -0,0 +1,4733 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/app/views/about/page_about.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:forward_chaining_man_app/app/views/student/model/data_student.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:firebase_core/firebase_core.dart'; +import 'package:intl/intl.dart' as intl; + +import 'package:url_launcher/url_launcher.dart'; + +class DevDataViewerController extends GetxController { + final RxList programStudiKerja = [].obs; + final RxList programStudiKuliah = [].obs; + final RxString currentView = + 'overview'.obs; // overview, kerja, kuliah, rules, ugm + final RxBool isLoading = true.obs; + final RxString loadingError = ''.obs; + final RxList> rulesData = >[].obs; + + // Data for UGM tuition fees + final RxList> biayaKuliahD4UGM = + >[].obs; + final RxList> biayaKuliahS1UGM = + >[].obs; + + @override + void onInit() { + super.onInit(); + loadAllData(); + } + + /// Load all data for analysis + void loadAllData() async { + isLoading.value = true; + loadingError.value = ''; + + try { + // Load Kerja data + await loadProgramData(true, programStudiKerja); + + // Load Kuliah data + await loadProgramData(false, programStudiKuliah); + + // Load UGM tuition fee data + await loadUGMTuitionData(); + + // Generate sample rules for analysis + generateSampleRules(); + + isLoading.value = false; + } catch (e) { + loadingError.value = e.toString(); + isLoading.value = false; + } + } + + void setCurrentView(String view) { + currentView.value = view; + } + + /// Memuat data ProgramStudi dari file JSON (Sains + Teknik) tergantung Kerja/Kuliah + Future loadProgramData( + bool isKerja, RxList target) async { + // Tentukan file sains + final sainsFile = isKerja + ? 'assets/ipa_sains_kerja.json' + : 'assets/ipa_sains_kuliah.json'; + + // File teknik + final teknikFile = isKerja + ? 'assets/ipa_teknik_kerja.json' + : 'assets/ipa_teknik_kuliah.json'; + + // Baca JSON sains + final sainsString = await rootBundle.rootBundle.loadString(sainsFile); + final sainsMap = json.decode(sainsString) as Map; + + // Baca JSON teknik + final teknikString = await rootBundle.rootBundle.loadString(teknikFile); + final teknikMap = json.decode(teknikString) as Map; + + // Ubah ke list ProgramStudi + final programs = []; + // Parsing sains + for (var entry in sainsMap.entries) { + programs.add(ProgramStudi.fromJson(entry.value)); + } + // Parsing teknik + for (var entry in teknikMap.entries) { + programs.add(ProgramStudi.fromJson(entry.value)); + } + + target.value = programs; + } + + /// Load UGM tuition fee data + Future loadUGMTuitionData() async { + try { + // Load D4 data + final d4String = await rootBundle.rootBundle + .loadString('assets/biaya_kuliah_d4_ugm.json'); + final d4List = json.decode(d4String) as List; + biayaKuliahD4UGM.value = d4List.cast>(); + + // Load S1 data + final s1String = await rootBundle.rootBundle + .loadString('assets/biaya_kuliah_s1_ugm.json'); + final s1List = json.decode(s1String) as List; + biayaKuliahS1UGM.value = s1List.cast>(); + } catch (e) { + print('Error loading UGM data: $e'); + // Continue even if UGM data fails to load + } + } + + /// Generate sample rules untuk analisis + void generateSampleRules() { + final rules = >[]; + + // Flatten pertanyaan dari programStudiKerja untuk contoh rules + int counter = 1; + for (var prog in programStudiKerja) { + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + final minatVal = minatEntry.value; + + for (var p in minatVal.pertanyaan) { + final bobot = extractBobot(p); + final cleaned = cleanPertanyaan(p); + final qId = 'Q$counter'; + counter++; + + rules.add({ + 'id': 'R$counter', + 'type': 'Forward Chaining Rule', + 'condition': 'IF $qId = Yes', + 'action': 'THEN Score("${prog.name}|$minatKey") += $bobot', + 'question': cleaned, + 'weight': bobot, + 'programName': prog.name, + 'minatKey': minatKey, + }); + } + } + } + + rulesData.value = rules; + } + + /// Get total question count + int getTotalQuestions() { + int count = 0; + + // Count questions from kerja + for (var prog in programStudiKerja) { + for (var minat in prog.minat.values) { + count += minat.pertanyaan.length; + } + } + + // Count questions from kuliah + for (var prog in programStudiKuliah) { + for (var minat in prog.minat.values) { + count += minat.pertanyaan.length; + } + } + + return count; + } + + /// Count total minat + int getTotalMinat() { + int kerjaMinat = + programStudiKerja.fold(0, (sum, prog) => sum + prog.minat.length); + int kuliahMinat = + programStudiKuliah.fold(0, (sum, prog) => sum + prog.minat.length); + return kerjaMinat + kuliahMinat; + } + + /// Count total jurusan + int getTotalJurusan() { + Set allJurusan = {}; + + // Collect unique jurusan from Kerja + for (var prog in programStudiKerja) { + for (var minat in prog.minat.values) { + allJurusan.addAll(minat.jurusanTerkait); + } + } + + // Collect unique jurusan from Kuliah + for (var prog in programStudiKuliah) { + for (var minat in prog.minat.values) { + allJurusan.addAll(minat.jurusanTerkait); + } + } + + return allJurusan.length; + } + + /// Count total karir + int getTotalKarir() { + Set allKarir = {}; + + // Collect unique karir from Kerja + for (var prog in programStudiKerja) { + for (var minat in prog.minat.values) { + allKarir.addAll(minat.karir); + } + } + + // Collect unique karir from Kuliah + for (var prog in programStudiKuliah) { + for (var minat in prog.minat.values) { + allKarir.addAll(minat.karir); + } + } + + return allKarir.length; + } +} + +/// Halaman untuk melihat data dan analisis forward chaining (developer mode) + +class DevDataViewerPage extends StatelessWidget { + const DevDataViewerPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(DevDataViewerController()); + + // Define theme colors + final primaryColor = Colors.blue.shade800; + final secondaryColor = Colors.indigo.shade900; + final bgGradient = LinearGradient( + colors: [primaryColor, secondaryColor], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); + + return Scaffold( + appBar: AppBar( + title: const Text('Statistik Data Aplikasi', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black, + letterSpacing: 0.5)), + backgroundColor: Colors.white, + elevation: 0, + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.grey.shade100, Colors.white], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Obx(() { + if (controller.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(primaryColor), + ), + const SizedBox(height: 16), + Text('Loading data...', + style: TextStyle( + color: primaryColor, fontWeight: FontWeight.w500)) + ], + )); + } + + if (controller.loadingError.value.isNotEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, + size: 48, color: Colors.red.shade700), + const SizedBox(height: 16), + Text('Error: ${controller.loadingError.value}', + style: const TextStyle(fontSize: 16)), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => controller.loadAllData(), + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text('Retry'), + ) + ], + ), + ); + } + + return Column( + children: [ + // Tab navigation with improved styling + Container( + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildTab( + controller, 'overview', 'Overview', primaryColor), + _buildTab( + controller, 'kerja', 'Kerja Data', primaryColor), + _buildTab( + controller, 'kuliah', 'Kuliah Data', primaryColor), + _buildTab(controller, 'rules', 'Rules', primaryColor), + _buildTab(controller, 'analysis', 'Model Analysis', + primaryColor), + _buildTab(controller, 'ugm', 'UGM Data', primaryColor), + ], + ), + ), + ), + + // Content based on selected tab + Expanded( + child: buildContent(controller, primaryColor, secondaryColor), + ), + ], + ); + }), + ), + ); + } + + Widget _buildTab(DevDataViewerController controller, String viewName, + String label, Color primaryColor) { + final isSelected = controller.currentView.value == viewName; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.setCurrentView(viewName), + borderRadius: BorderRadius.circular(24), + splashColor: primaryColor.withOpacity(0.1), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + decoration: BoxDecoration( + color: isSelected ? primaryColor : Colors.transparent, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: isSelected ? primaryColor : Colors.grey.shade300, + width: 1, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: primaryColor.withOpacity(0.3), + spreadRadius: 1, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : Colors.grey.shade800, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + fontSize: 14, + ), + ), + ), + ), + ), + ); + } + + Widget buildContent(DevDataViewerController controller, Color primaryColor, + Color secondaryColor) { + switch (controller.currentView.value) { + case 'overview': + return buildOverviewTab(controller, primaryColor, secondaryColor); + case 'kerja': + return buildDataTab(controller, controller.programStudiKerja, 'Kerja', + primaryColor, secondaryColor); + case 'kuliah': + return buildDataTab(controller, controller.programStudiKuliah, 'Kuliah', + primaryColor, secondaryColor); + case 'rules': + return buildRulesTab(controller, primaryColor, secondaryColor); + case 'analysis': + return buildAnalysisTab(controller, primaryColor, secondaryColor); + case 'ugm': + return buildUGMDataTab(controller, primaryColor, secondaryColor); + default: + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 48, color: Colors.red.shade300), + const SizedBox(height: 16), + const Text('Unknown view', style: TextStyle(fontSize: 18)), + ], + ), + ); + } + } + + /// Tab Overview - statistik umum with improved UI + Widget buildOverviewTab(DevDataViewerController controller, + Color primaryColor, Color secondaryColor) { + // Prepare data for charts + final List minatDistribution = []; + final Map minatCounts = {}; + + // Count minat by category + for (var prog in [ + ...controller.programStudiKerja, + ...controller.programStudiKuliah + ]) { + for (var minatKey in prog.minat.keys) { + if (minatCounts.containsKey(minatKey)) { + minatCounts[minatKey] = minatCounts[minatKey]! + 1; + } else { + minatCounts[minatKey] = 1; + } + } + } + + // Convert to pie chart data + List colorList = [ + Colors.blue.shade400, + Colors.indigo.shade400, + Colors.purple.shade400, + Colors.teal.shade400, + Colors.cyan.shade400, + Colors.green.shade400, + Colors.amber.shade400, + Colors.orange.shade400, + ]; + + int colorIndex = 0; + minatCounts.forEach((key, value) { + if (colorIndex >= colorList.length) colorIndex = 0; + + if (minatDistribution.length < 6) { + // Limit to top 6 for visibility + minatDistribution.add(PieChartSectionData( + value: value.toDouble(), + title: key, + radius: 100, + titleStyle: const TextStyle( + color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), + color: colorList[colorIndex++], + )); + } + }); + + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + secondaryColor.withOpacity(0.9), + primaryColor.withOpacity(0.9), + ], + ), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Section + const Text( + 'Data Aplikasi', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 6), + Text( + 'Visualisasi data yang digunakan dalam analisis forward chaining', + style: TextStyle( + fontSize: 14, + color: Colors.white.withOpacity(0.8), + ), + ), + const Divider(color: Colors.white30, thickness: 1, height: 32), + + // Stats Cards + GridView.count( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + _buildStatCard( + 'Total Questions', + controller.getTotalQuestions().toString(), + Icons.question_answer, + onTap: () => _showQuestionsDialog(controller, primaryColor), + primaryColor: primaryColor, + ), + _buildStatCard( + 'Total Rules', + controller.rulesData.length.toString(), + Icons.rule, + onTap: () => _showRulesDialog(controller, primaryColor), + primaryColor: primaryColor, + ), + _buildStatCard( + 'Total Minat', + controller.getTotalMinat().toString(), + Icons.category, + onTap: () => _showMinatDialog(controller, primaryColor), + primaryColor: primaryColor, + ), + _buildStatCard( + 'Total Jurusan', + controller.getTotalJurusan().toString(), + Icons.school, + onTap: () => _showJurusanDialog(controller, primaryColor), + primaryColor: primaryColor, + ), + _buildStatCard( + 'Total Karir', + controller.getTotalKarir().toString(), + Icons.work, + onTap: () => _showKarirDialog(controller, primaryColor), + primaryColor: primaryColor, + ), + _buildStatCard( + 'Data Sources', + '6 JSON Files', + Icons.data_array, + primaryColor: primaryColor, + ), + ], + ), + + // Charts Section + const SizedBox(height: 30), + Card( + elevation: 8, + shadowColor: Colors.black26, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Distribusi Minat', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + SizedBox( + height: 250, + child: PieChart( + PieChartData( + sections: minatDistribution, + centerSpaceRadius: 40, + sectionsSpace: 2, + pieTouchData: PieTouchData( + touchCallback: + (FlTouchEvent event, pieTouchResponse) { + // You can implement touch interaction if needed + }, + ), + ), + ), + ), + const SizedBox(height: 16), + const Text( + 'Visualisasi distribusi minat berdasarkan frekuensi kemunculan', + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + color: Colors.grey, + ), + ), + ], + ), + ), + ), + + // Question Distribution per Program Study + const SizedBox(height: 30), + Card( + elevation: 8, + shadowColor: Colors.black26, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Distribusi Pertanyaan per Program Studi', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + SizedBox( + height: 300, + child: buildQuestionDistributionChart( + controller, primaryColor, secondaryColor), + ), + ], + ), + ), + ), + + const SizedBox(height: 30), + + // Forward Chaining Explanation with improved styling + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.psychology, color: primaryColor, size: 28), + const SizedBox(width: 12), + const Text( + 'Implementasi Forward Chaining', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + const Text( + 'Model ini mengimplementasikan pendekatan Forward Chaining berbasis aturan dengan:', + style: TextStyle(fontSize: 15), + ), + const SizedBox(height: 12), + _buildFeatureItem( + icon: Icons.memory, + title: 'Memori Kerja', + description: 'Menyimpan fakta seperti "Q1=Ya" / "Q1=Tidak"', + color: primaryColor, + ), + _buildFeatureItem( + icon: Icons.rule_folder, + title: 'Basis Aturan', + description: 'Aturan dalam bentuk "JIKA kondisi MAKA aksi"', + color: primaryColor, + ), + _buildFeatureItem( + icon: Icons.sync, + title: 'Mesin Inferensi', + description: + 'Menerapkan aturan pada memori kerja untuk mendapatkan skor', + color: primaryColor, + ), + _buildFeatureItem( + icon: Icons.balance, + title: 'Pembobotan Skor', + description: + 'Setiap pertanyaan memiliki bobot yang berkontribusi pada skor akhir', + color: primaryColor, + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade100), + ), + child: const Text( + 'Implementasi ini adalah sistem produksi klasik dengan siklus cocok-selesaikan-bertindak yang berlanjut hingga tidak ada lagi aturan yang dapat dijalankan.', + style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 14, + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 30), + + // Visualisasi Flow Forward Chaining with improved styling + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.account_tree, color: primaryColor, size: 28), + const SizedBox(width: 12), + const Text( + 'Alur Forward Chaining', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 20), + buildForwardChainingFlowchart(primaryColor, secondaryColor), + ], + ), + ), + + const SizedBox(height: 40), + ], + ), + ), + ); + } + + Widget buildForwardChainingFlowchart( + Color primaryColor, Color secondaryColor) { + return Container( + height: 420, + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + _buildFlowStep( + number: "1", + text: + "Pengguna menjawab pertanyaan (Ya/Tidak) untuk menetapkan fakta awal", + primaryColor: primaryColor), + _buildFlowArrow(), + _buildFlowStep( + number: "2", + text: + "Fakta ditambahkan ke dalam memori kerja (misalnya, \"Q1=Ya\")", + primaryColor: primaryColor), + _buildFlowArrow(), + _buildFlowStep( + number: "3", + text: + "Aturan yang sesuai dengan fakta dalam memori kerja dijalankan", + primaryColor: primaryColor), + _buildFlowArrow(), + _buildFlowStep( + number: "4", + text: + "Setiap aturan yang dijalankan menambahkan skor ke minat yang sesuai", + primaryColor: primaryColor), + _buildFlowArrow(), + _buildFlowStep( + number: "5", + text: + "Setelah semua aturan dievaluasi, minat diurutkan berdasarkan skor", + primaryColor: primaryColor), + _buildFlowArrow(), + _buildFlowStep( + number: "6", + text: "Tiga minat teratas disajikan sebagai rekomendasi", + primaryColor: primaryColor), + ], + ), + ), + ); + } + + Widget _buildFlowStep( + {required String number, + required String text, + required Color primaryColor}) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + margin: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + number, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + text, + style: const TextStyle(fontSize: 14), + ), + ), + ], + ), + ); + } + + Widget _buildFlowArrow() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Icon( + Icons.arrow_downward, + color: Colors.grey.shade400, + size: 20, + ), + ); + } + + Widget _buildFeatureItem({ + required IconData icon, + required String title, + required String description, + required Color color, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + Text( + description, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget buildQuestionDistributionChart(DevDataViewerController controller, + Color primaryColor, Color secondaryColor) { + // Prepare the data + final Map questionCounts = {}; + + // Count questions per program + for (var prog in controller.programStudiKerja) { + int count = 0; + for (var minat in prog.minat.values) { + count += minat.pertanyaan.length; + } + questionCounts["${prog.name} (Kerja)"] = count; + } + + for (var prog in controller.programStudiKuliah) { + int count = 0; + for (var minat in prog.minat.values) { + count += minat.pertanyaan.length; + } + questionCounts["${prog.name} (Kuliah)"] = count; + } + + // Sort the data + final sortedEntries = questionCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + // Take top 8 for readability + final limitedEntries = sortedEntries.take(8).toList(); + + return BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: limitedEntries.isNotEmpty + ? (limitedEntries.first.value * 1.2).toDouble() + : 10, + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + getTooltipItem: (group, groupIndex, rod, rodIndex) { + return BarTooltipItem( + '${limitedEntries[groupIndex].key}\n', + const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: '${rod.toY.round()} questions', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.normal, + ), + ), + ], + ); + }, + ), + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + if (value >= limitedEntries.length || value < 0) + return const Text(''); + // Abbreviate long program names + String title = limitedEntries[value.toInt()].key; + if (title.length > 15) { + title = '${title.substring(0, 13)}...'; + } + + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Transform.rotate( + angle: 0.4, + child: Text( + title, + style: const TextStyle( + color: Colors.grey, + fontSize: 10, + ), + ), + ), + ); + }, + reservedSize: 44, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 36, + getTitlesWidget: (value, meta) { + if (value == 0) return const Text(''); + return Text( + value.toInt().toString(), + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ); + }, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: false), + gridData: FlGridData( + show: true, + horizontalInterval: 10, + getDrawingHorizontalLine: (value) => FlLine( + color: Colors.grey.shade200, + strokeWidth: 1, + dashArray: [5, 5], + ), + ), + barGroups: List.generate( + limitedEntries.length, + (index) => BarChartGroupData( + x: index, + barRods: [ + BarChartRodData( + toY: limitedEntries[index].value.toDouble(), + color: primaryColor, + width: 16, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + backDrawRodData: BackgroundBarChartRodData( + show: true, + toY: limitedEntries.first.value * 1.2, + color: Colors.grey.shade200, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatCard(String title, String value, IconData icon, + {VoidCallback? onTap, required Color primaryColor}) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(12.0), // Reduced padding slightly + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(10), // Reduced padding + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: + Icon(icon, size: 24, color: primaryColor), // Smaller icon + ), + const SizedBox(height: 12), // Less space + Container( + width: double.infinity, // Forces wrapping + child: Text( + title, + style: TextStyle( + fontSize: 13, // Smaller font + color: Colors.grey.shade700, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + maxLines: 2, // Allow up to 2 lines + overflow: + TextOverflow.ellipsis, // Show ellipsis if it overflows + ), + ), + const SizedBox(height: 6), // Less space + FittedBox( + // This will scale text to fit + fit: BoxFit.scaleDown, + child: Text( + value, + style: TextStyle( + fontSize: 22, // Smaller font + fontWeight: FontWeight.bold, + color: primaryColor, + ), + maxLines: 1, + ), + ), + if (onTap != null) const SizedBox(height: 6), + if (onTap != null) + Row( + mainAxisSize: MainAxisSize.min, // Take only needed space + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + // Allow text to shrink if needed + child: Text( + 'Lihat Detail', + style: TextStyle( + fontSize: 11, // Smaller font + color: primaryColor, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.arrow_forward, + size: 10, // Smaller icon + color: primaryColor, + ), + ], + ), + ], + ), + ), + ), + ); + } + +// Add the dialog methods with improved styling + void _showQuestionsDialog( + DevDataViewerController controller, Color primaryColor) { + final allQuestions = >[]; + + // Collect questions from Kerja + for (var prog in controller.programStudiKerja) { + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + final minatVal = minatEntry.value; + + for (var p in minatVal.pertanyaan) { + final bobot = extractBobot(p); + final cleaned = cleanPertanyaan(p); + + allQuestions.add({ + 'pertanyaan': cleaned, + 'bobot': bobot, + 'program': prog.name, + 'minat': minatKey, + 'type': 'Kerja', + }); + } + } + } + + // Collect questions from Kuliah + for (var prog in controller.programStudiKuliah) { + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + final minatVal = minatEntry.value; + + for (var p in minatVal.pertanyaan) { + final bobot = extractBobot(p); + final cleaned = cleanPertanyaan(p); + + allQuestions.add({ + 'pertanyaan': cleaned, + 'bobot': bobot, + 'program': prog.name, + 'minat': minatKey, + 'type': 'Kuliah', + }); + } + } + } + + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Container( + width: Get.width * 0.9, + height: Get.height * 0.8, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'All Questions', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.question_answer, + color: primaryColor, + size: 20, + ), + ), + const SizedBox(width: 8), + Text( + 'Total: ${allQuestions.length} questions', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + // Add search functionality here if needed + ], + ), + ), + const Divider(), + Expanded( + child: ListView.builder( + itemCount: allQuestions.length, + itemBuilder: (context, index) { + final q = allQuestions[index]; + final isKerja = q['type'] == 'Kerja'; + + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 4), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with number and question + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Number avatar + CircleAvatar( + radius: 14, + backgroundColor: isKerja + ? Colors.blue.shade700 + : Colors.green.shade700, + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + const SizedBox(width: 12), + + // Question text + Expanded( + child: Text( + q['pertanyaan'], + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15, + ), + ), + ), + ], + ), + + const SizedBox(height: 12), + const Divider(height: 1), + const SizedBox(height: 12), + + // Tags as Wrap to handle overflow + Wrap( + spacing: 8, // horizontal space between tags + runSpacing: 8, // vertical space between lines + children: [ + _buildQuestionTag( + isKerja ? 'Kerja' : 'Kuliah', + isKerja ? Colors.blue : Colors.green, + ), + _buildQuestionTag( + q['program'], + Colors.indigo, + ), + _buildQuestionTag( + 'Bobot: ${q['bobot']}', + Colors.amber.shade800, + ), + ], + ), + + const SizedBox(height: 8), + + // Minat info + Row( + children: [ + Icon(Icons.interests, + size: 12, color: Colors.grey.shade600), + const SizedBox(width: 4), + Expanded( + child: Text( + 'Minat: ${q['minat']}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ), + ), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + top: BorderSide(color: Colors.grey.shade200), + ), + ), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Get.back(), + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Close', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + // Improved question tag to prevent overflow + Widget _buildQuestionTag(String text, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Text( + text, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: color, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ); + } + + void _showRulesDialog( + DevDataViewerController controller, Color primaryColor) { + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Container( + width: Get.width * 0.9, + height: Get.height * 0.8, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'All Rules', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.rule, + color: primaryColor, + size: 20, + ), + ), + const SizedBox(width: 8), + Text( + 'Total: ${controller.rulesData.length} rules', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const Divider(), + Expanded( + child: ListView.builder( + itemCount: controller.rulesData.length, + itemBuilder: (context, index) { + final rule = controller.rulesData[index]; + + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 4), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + title: Text( + '${rule['condition']} ${rule['action']}', + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + Text( + 'Question: ${rule['question']}', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + _buildQuestionTag( + 'Weight: ${rule['weight']}', + Colors.amber.shade800, + ), + const SizedBox(width: 8), + _buildQuestionTag( + rule['programName'], + Colors.indigo, + ), + const SizedBox(width: 8), + Expanded( + child: _buildQuestionTag( + 'Minat: ${rule['minatKey']}', + Colors.teal, + ), + ), + ], + ), + ], + ), + leading: CircleAvatar( + backgroundColor: Colors.amber.shade700, + child: Text( + rule['id'].toString().replaceAll('R', ''), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + isThreeLine: true, + ), + ); + }, + ), + ), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + top: BorderSide(color: Colors.grey.shade200), + ), + ), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Get.back(), + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Close', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + void _showMinatDialog( + DevDataViewerController controller, Color primaryColor) { + final allMinat = >[]; + + // Collect minat from Kerja with additional details + for (var prog in controller.programStudiKerja) { + for (var minatEntry in prog.minat.entries) { + allMinat.add({ + 'name': minatEntry.key, + 'program': prog.name, + 'type': 'Kerja', + 'jumlahPertanyaan': minatEntry.value.pertanyaan.length, + 'jumlahKarir': minatEntry.value.karir.length, + 'jumlahJurusan': minatEntry.value.jurusanTerkait.length, + // Store the actual data for detailed view + 'pertanyaan': minatEntry.value.pertanyaan, + 'karir': minatEntry.value.karir, + 'jurusanTerkait': minatEntry.value.jurusanTerkait, + }); + } + } + + // Collect minat from Kuliah with additional details + for (var prog in controller.programStudiKuliah) { + for (var minatEntry in prog.minat.entries) { + allMinat.add({ + 'name': minatEntry.key, + 'program': prog.name, + 'type': 'Kuliah', + 'jumlahPertanyaan': minatEntry.value.pertanyaan.length, + 'jumlahKarir': minatEntry.value.karir.length, + 'jumlahJurusan': minatEntry.value.jurusanTerkait.length, + // Store the actual data for detailed view + 'pertanyaan': minatEntry.value.pertanyaan, + 'karir': minatEntry.value.karir, + 'jurusanTerkait': minatEntry.value.jurusanTerkait, + }); + } + } + + // Sort by number of questions + allMinat + .sort((a, b) => b['jumlahPertanyaan'].compareTo(a['jumlahPertanyaan'])); + + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Container( + width: Get.width * 0.9, + height: Get.height * 0.8, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'All Minat', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.interests, + color: primaryColor, + size: 20, + ), + ), + const SizedBox(width: 8), + Text( + 'Total: ${allMinat.length} minat', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const Divider(), + Expanded( + child: ListView.builder( + itemCount: allMinat.length, + itemBuilder: (context, index) { + final minat = allMinat[index]; + final isKerja = minat['type'] == 'Kerja'; + + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 4), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Theme( + data: Theme.of(context).copyWith( + dividerColor: + Colors.transparent, // Remove the default divider + ), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + title: Text( + minat['name'], + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + subtitle: Wrap( + // Use Wrap to prevent overflow + spacing: 8, + runSpacing: 8, + children: [ + _buildQuestionTag( + isKerja ? 'Kerja' : 'Kuliah', + isKerja ? Colors.blue : Colors.green, + ), + _buildQuestionTag( + minat['program'], + Colors.indigo, + ), + ], + ), + leading: CircleAvatar( + backgroundColor: isKerja + ? Colors.blue.shade700 + : Colors.green.shade700, + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(), + + // Overall stats + Container( + margin: const EdgeInsets.only( + top: 8, bottom: 16), + child: Row( + children: [ + Expanded( + child: _buildMinatInfoCounter( + icon: Icons.question_answer, + title: 'Pertanyaan', + count: minat['jumlahPertanyaan'], + color: Colors.blue, + ), + ), + Expanded( + child: _buildMinatInfoCounter( + icon: Icons.work, + title: 'Karir', + count: minat['jumlahKarir'], + color: Colors.orange, + ), + ), + Expanded( + child: _buildMinatInfoCounter( + icon: Icons.school, + title: 'Jurusan', + count: minat['jumlahJurusan'], + color: Colors.green, + ), + ), + ], + ), + ), + + // Detailed sections using DefaultTabController for better organization + Container( + height: + 300, // Fixed height for the tab view + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade200), + ), + child: DefaultTabController( + length: 3, + child: Column( + children: [ + TabBar( + tabs: const [ + Tab(text: 'Pertanyaan'), + Tab(text: 'Karir'), + Tab(text: 'Jurusan'), + ], + indicatorColor: primaryColor, + labelColor: primaryColor, + unselectedLabelColor: + Colors.grey.shade600, + labelStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + Expanded( + child: TabBarView( + children: [ + // Pertanyaan tab + _buildPertanyaanTabContent( + minat), + + // Karir tab + _buildKarirTabContent(minat), + + // Jurusan tab + _buildJurusanTabContent(minat), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + top: BorderSide(color: Colors.grey.shade200), + ), + ), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Get.back(), + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Close', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + // Helper widget for the stats counter + Widget _buildMinatInfoCounter({ + required IconData icon, + required String title, + required int count, + required Color color, + }) { + return Column( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(height: 6), + Text( + title, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + count.toString(), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ); + } + + // Tab content for Pertanyaan + Widget _buildPertanyaanTabContent(Map minat) { + final List pertanyaanList = minat['pertanyaan']; + + if (pertanyaanList.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(20), + child: Text('Tidak ada pertanyaan'), + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: pertanyaanList.length, + itemBuilder: (context, index) { + final String question = pertanyaanList[index]; + final bobot = extractBobot(question); + final cleaned = cleanPertanyaan(question); + + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${index + 1}.', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + cleaned, + style: const TextStyle(fontSize: 13), + ), + ), + ], + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.amber.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Bobot: $bobot', + style: TextStyle( + fontSize: 11, + color: Colors.amber.shade800, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + }, + ); + } + + // Tab content for Karir + Widget _buildKarirTabContent(Map minat) { + final List karirList = minat['karir']; + + if (karirList.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(20), + child: Text('Tidak ada karir terkait'), + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(12), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: karirList.map((karir) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.orange.shade200), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.work, + size: 14, + color: Colors.orange.shade700, + ), + const SizedBox(width: 6), + Flexible( + child: Text( + karir, + style: TextStyle( + fontSize: 13, + color: Colors.orange.shade800, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + }).toList(), + ), + ); + } + + // Tab content for Jurusan + Widget _buildJurusanTabContent(Map minat) { + final List jurusanList = minat['jurusanTerkait']; + + if (jurusanList.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(20), + child: Text('Tidak ada jurusan terkait'), + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: jurusanList.length, + itemBuilder: (context, index) { + final String jurusan = jurusanList[index]; + + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Row( + children: [ + Icon( + Icons.school, + size: 18, + color: Colors.green.shade700, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + jurusan, + style: TextStyle( + fontSize: 13, + color: Colors.green.shade800, + ), + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildMinatInfoItem({ + required IconData icon, + required String title, + required String value, + required Color color, + }) { + return Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(width: 12), + Text( + title, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Text( + value, + style: TextStyle( + fontWeight: FontWeight.bold, + color: color, + ), + ), + ), + ], + ); + } + + void _showJurusanDialog( + DevDataViewerController controller, Color primaryColor) { + // Collect all unique jurusan with their associated minat and program + final Map>> jurusanMap = {}; + + // Helper function to add jurusan data + void addJurusanData( + String jurusan, String program, String minat, String type) { + if (!jurusanMap.containsKey(jurusan)) { + jurusanMap[jurusan] = []; + } + + jurusanMap[jurusan]!.add({ + 'program': program, + 'minat': minat, + 'type': type, + }); + } + + // Collect from Kerja + for (var prog in controller.programStudiKerja) { + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + + for (var jurusan in minatEntry.value.jurusanTerkait) { + addJurusanData(jurusan, prog.name, minatKey, 'Kerja'); + } + } + } + + // Collect from Kuliah + for (var prog in controller.programStudiKuliah) { + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + + for (var jurusan in minatEntry.value.jurusanTerkait) { + addJurusanData(jurusan, prog.name, minatKey, 'Kuliah'); + } + } + } + + // Convert to list for display + final jurusanList = jurusanMap.entries.map((entry) { + return { + 'name': entry.key, + 'count': entry.value.length, + 'references': entry.value, + }; + }).toList(); + + // Sort by frequency + jurusanList + .sort((a, b) => (b['count'] as int).compareTo(a['count'] as int)); + + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Container( + width: Get.width * 0.9, + height: Get.height * 0.8, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'All Jurusan', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.school, + color: primaryColor, + size: 20, + ), + ), + const SizedBox(width: 8), + Text( + 'Total: ${jurusanList.length} jurusan', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const Divider(), + Expanded( + child: ListView.builder( + itemCount: jurusanList.length, + itemBuilder: (context, index) { + final jurusan = jurusanList[index]; + final references = + jurusan['references'] as List>; + + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 4), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + title: Text( + jurusan['name'] as String, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + subtitle: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.indigo.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.indigo.withOpacity(0.3), + ), + ), + child: Text( + 'Referenced ${jurusan['count']} times', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.indigo, + ), + ), + ), + ], + ), + leading: CircleAvatar( + backgroundColor: Colors.indigo, + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + children: [ + const Divider(indent: 16, endIndent: 16), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + child: Text( + 'Referenced in:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: references.length, + itemBuilder: (context, i) { + final ref = references[i]; + final isKerja = ref['type'] == 'Kerja'; + + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 4, + ), + title: Row( + children: [ + _buildQuestionTag( + isKerja ? 'Kerja' : 'Kuliah', + isKerja ? Colors.blue : Colors.green, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + ref['program'], + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + 'Minat: ${ref['minat']}', + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + color: Colors.grey.shade600, + ), + ), + ), + leading: Icon( + isKerja ? Icons.work : Icons.school, + color: isKerja ? Colors.blue : Colors.green, + size: 20, + ), + ); + }, + ), + const SizedBox(height: 8), + ], + ), + ); + }, + ), + ), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + top: BorderSide(color: Colors.grey.shade200), + ), + ), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Get.back(), + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Close', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + void _showKarirDialog( + DevDataViewerController controller, Color primaryColor) { + // Collect all unique karir with their associated minat and program + final Map>> karirMap = {}; + + // Helper function to add karir data + void addKarirData(String karir, String program, String minat, String type) { + if (!karirMap.containsKey(karir)) { + karirMap[karir] = []; + } + + karirMap[karir]!.add({ + 'program': program, + 'minat': minat, + 'type': type, + }); + } + + // Collect from Kerja + for (var prog in controller.programStudiKerja) { + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + + for (var karir in minatEntry.value.karir) { + addKarirData(karir, prog.name, minatKey, 'Kerja'); + } + } + } + + // Collect from Kuliah + for (var prog in controller.programStudiKuliah) { + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + + for (var karir in minatEntry.value.karir) { + addKarirData(karir, prog.name, minatKey, 'Kuliah'); + } + } + } + + // Convert to list for display + final karirList = karirMap.entries.map((entry) { + return { + 'name': entry.key, + 'count': entry.value.length, + 'references': entry.value, + }; + }).toList(); + + // Sort by frequency + karirList.sort((a, b) => (b['count'] as int).compareTo(a['count'] as int)); + + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Container( + width: Get.width * 0.9, + height: Get.height * 0.8, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'All Karir', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.work, + color: primaryColor, + size: 20, + ), + ), + const SizedBox(width: 8), + Text( + 'Total: ${karirList.length} karir', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const Divider(), + Expanded( + child: ListView.builder( + itemCount: karirList.length, + itemBuilder: (context, index) { + final karir = karirList[index]; + final references = + karir['references'] as List>; + + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 16, vertical: 4), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + title: Text( + karir['name'] as String, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + subtitle: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.teal.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.teal.withOpacity(0.3), + ), + ), + child: Text( + 'Referenced ${karir['count']} times', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.teal, + ), + ), + ), + ], + ), + leading: CircleAvatar( + backgroundColor: Colors.teal, + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + children: [ + const Divider(indent: 16, endIndent: 16), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + child: Text( + 'Referenced in:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: references.length, + itemBuilder: (context, i) { + final ref = references[i]; + final isKerja = ref['type'] == 'Kerja'; + + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 4, + ), + title: Row( + children: [ + _buildQuestionTag( + isKerja ? 'Kerja' : 'Kuliah', + isKerja ? Colors.blue : Colors.green, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + ref['program'], + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + 'Minat: ${ref['minat']}', + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + color: Colors.grey.shade600, + ), + ), + ), + leading: Icon( + isKerja ? Icons.work : Icons.school, + color: isKerja ? Colors.blue : Colors.green, + size: 20, + ), + ); + }, + ), + const SizedBox(height: 8), + ], + ), + ); + }, + ), + ), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + top: BorderSide(color: Colors.grey.shade200), + ), + ), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Get.back(), + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Close', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + /// Tab untuk menampilkan data ProgramStudi (Kerja atau Kuliah) dengan UI yang ditingkatkan + Widget buildDataTab( + DevDataViewerController controller, + List data, + String type, + Color primaryColor, + Color secondaryColor) { + return Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: (type == 'Kerja' ? Colors.blue : Colors.green) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + type == 'Kerja' ? Icons.work : Icons.school, + color: type == 'Kerja' ? Colors.blue : Colors.green, + size: 20, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$type Dataset', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${data.length} Program Studi', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + ), + Expanded( + child: ListView.builder( + itemCount: data.length, + itemBuilder: (context, index) { + final program = data[index]; + final minatCount = program.minat.length; + int pertanyaanCount = 0; + + // Count pertanyaan + for (var minat in program.minat.values) { + pertanyaanCount += minat.pertanyaan.length; + } + + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + childrenPadding: EdgeInsets.zero, + title: Text( + program.name, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 6), + Row( + children: [ + _buildCompactTag( + '$minatCount minat', + type == 'Kerja' ? Colors.blue : Colors.green, + ), + const SizedBox(width: 8), + _buildCompactTag( + '$pertanyaanCount pertanyaan', + Colors.amber.shade800, + ), + ], + ), + if (program.categories.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + 'Categories: ${program.categories.join(", ")}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + ), + ], + ], + ), + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Program description + if (program.description.isNotEmpty) + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.grey.shade200, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Description', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Text( + program.description, + style: const TextStyle( + fontSize: 13, + ), + ), + ], + ), + ), + + // Minat details + ...program.minat.entries.map((minatEntry) { + final minatKey = minatEntry.key; + final minatValue = minatEntry.value; + + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + childrenPadding: const EdgeInsets.only( + bottom: 16, + left: 16, + right: 16, + ), + title: Text( + 'Minat: $minatKey', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + subtitle: Text( + '${minatValue.pertanyaan.length} pertanyaan', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ), + leading: CircleAvatar( + backgroundColor: + (type == 'Kerja' ? Colors.blue : Colors.green) + .withOpacity(0.2), + child: Icon( + Icons.interests, + color: type == 'Kerja' + ? Colors.blue + : Colors.green, + size: 20, + ), + ), + children: [ + // Pertanyaan + if (minatValue.pertanyaan.isNotEmpty) ...[ + const SizedBox(height: 4), + _buildMinatSectionHeader( + 'Pertanyaan', Icons.question_answer), + const SizedBox(height: 8), + ...minatValue.pertanyaan.map((p) { + final bobot = extractBobot(p); + final cleaned = cleanPertanyaan(p); + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text('• ', + style: TextStyle(fontSize: 15)), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + cleaned, + style: const TextStyle( + fontSize: 13), + ), + const SizedBox(height: 2), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.amber.shade100, + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + 'Bobot: $bobot', + style: TextStyle( + fontSize: 11, + color: + Colors.amber.shade800, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ], + + // Karir + if (minatValue.karir.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildMinatSectionHeader('Karir', Icons.work), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: minatValue.karir + .map((k) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.teal.shade50, + borderRadius: + BorderRadius.circular(16), + border: Border.all( + color: Colors.teal.shade200, + ), + ), + child: Text( + k, + style: TextStyle( + fontSize: 12, + color: Colors.teal.shade700, + ), + ), + )) + .toList(), + ), + ], + + // Jurusan Terkait + if (minatValue.jurusanTerkait.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildMinatSectionHeader( + 'Jurusan Terkait', Icons.school), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: minatValue.jurusanTerkait + .map((j) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.indigo.shade50, + borderRadius: + BorderRadius.circular(16), + border: Border.all( + color: Colors.indigo.shade200, + ), + ), + child: Text( + j, + style: TextStyle( + fontSize: 12, + color: Colors.indigo.shade700, + ), + ), + )) + .toList(), + ), + ], + ], + ), + ), + ); + }).toList(), + ], + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildCompactTag(String text, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Text( + text, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: color, + ), + ), + ); + } + + Widget _buildMinatSectionHeader(String title, IconData icon) { + return Row( + children: [ + Icon( + icon, + size: 16, + color: Colors.grey.shade700, + ), + const SizedBox(width: 6), + Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ); + } + + /// Tab untuk menampilkan rules dengan UI yang ditingkatkan + Widget buildRulesTab(DevDataViewerController controller, Color primaryColor, + Color secondaryColor) { + return Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.amber.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.rule, + color: Colors.amber.shade800, + size: 20, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Forward Chaining Rules', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${controller.rulesData.length} Rules', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + const Spacer(), + _buildRuleFilterButton(controller, primaryColor), + ], + ), + ), + + // Rule visualization using fl_chart + Container( + height: 180, + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Rule Distribution by Weight', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Visualization of rule weights across the system', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + Expanded( + child: buildRuleWeightChart( + controller, primaryColor, secondaryColor), + ), + ], + ), + ), + + // Rules list + Expanded( + child: ListView.builder( + itemCount: controller.rulesData.length, + itemBuilder: (context, index) { + final rule = controller.rulesData[index]; + final weight = rule['weight'] as int; + + // Determine color based on weight + final Color weightColor = weight >= 4 + ? Colors.red.shade700 + : weight >= 3 + ? Colors.orange.shade700 + : weight >= 2 + ? Colors.amber.shade700 + : Colors.green.shade700; + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Rule ID avatar + CircleAvatar( + radius: 16, + backgroundColor: Colors.amber.shade700, + child: Text( + rule['id'].toString().replaceAll('R', ''), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + const SizedBox(width: 12), + + // Rule condition and action + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + rule['condition'], + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + const SizedBox(height: 2), + Text( + rule['action'], + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: Colors.blue, + ), + ), + ], + ), + ), + + // Weight indicator + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: weightColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: weightColor.withOpacity(0.3), + ), + ), + child: Text( + 'Weight: ${rule['weight']}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: weightColor, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + const Divider(), + + // Question, program and minat details + Text( + 'Question: ${rule['question']}', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + _buildRuleTag( + rule['programName'], + Colors.indigo, + Icons.category, + ), + const SizedBox(width: 8), + _buildRuleTag( + 'Minat: ${rule['minatKey']}', + Colors.teal, + Icons.interests, + ), + ], + ), + ], + ), + ), + ); + }, + ), + ), + ], + ); + } + + Widget _buildRuleFilterButton( + DevDataViewerController controller, Color primaryColor) { + // You can implement filtering functionality here if needed + return InkWell( + onTap: () { + // Show filter options + }, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: primaryColor.withOpacity(0.3)), + ), + child: Row( + children: [ + Icon( + Icons.filter_list, + size: 16, + color: primaryColor, + ), + const SizedBox(width: 4), + Text( + 'Filter', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: primaryColor, + ), + ), + ], + ), + ), + ); + } + + Widget _buildRuleTag(String text, Color color, IconData icon) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 12, + color: color, + ), + const SizedBox(width: 4), + Text( + text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: color, + ), + ), + ], + ), + ); + } + + Widget buildRuleWeightChart(DevDataViewerController controller, + Color primaryColor, Color secondaryColor) { + // Count rules by weight + final Map weightCounts = {}; + + for (var rule in controller.rulesData) { + final weight = rule['weight'] as int; + if (weightCounts.containsKey(weight)) { + weightCounts[weight] = weightCounts[weight]! + 1; + } else { + weightCounts[weight] = 1; + } + } + + // Sort weights + final sortedWeights = weightCounts.keys.toList()..sort(); + + // Prepare bar chart data + final List barGroups = []; + + for (int i = 0; i < sortedWeights.length; i++) { + final weight = sortedWeights[i]; + final count = weightCounts[weight]!; + + // Different colors for different weights + final Color barColor = weight >= 4 + ? Colors.red.shade700 + : weight >= 3 + ? Colors.orange.shade700 + : weight >= 2 + ? Colors.amber.shade700 + : Colors.green.shade700; + + barGroups.add( + BarChartGroupData( + x: i, + barRods: [ + BarChartRodData( + toY: count.toDouble(), + color: barColor, + width: 16, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + ), + ], + ), + ); + } + + return BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final weight = sortedWeights[group.x.toInt()]; + return BarTooltipItem( + 'Weight $weight\n', + const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: '${rod.toY.round()} rules', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.normal, + ), + ), + ], + ); + }, + ), + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + if (value >= sortedWeights.length || value < 0) + return const Text(''); + return Text( + 'W${sortedWeights[value.toInt()]}', + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ); + }, + reservedSize: 28, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + getTitlesWidget: (value, meta) { + if (value == 0) return const Text(''); + if (value % 1 != 0) return const Text(''); + return Text( + value.toInt().toString(), + style: const TextStyle( + color: Colors.grey, + fontSize: 11, + ), + ); + }, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: false), + gridData: FlGridData( + show: true, + horizontalInterval: 1, + getDrawingHorizontalLine: (value) => FlLine( + color: Colors.grey.shade200, + strokeWidth: 1, + dashArray: [5, 5], + ), + ), + barGroups: barGroups, + ), + ); + } + + /// Tab untuk analisis kesesuaian dengan metode Forward Chaining dengan UI yang ditingkatkan + Widget buildAnalysisTab(DevDataViewerController controller, + Color primaryColor, Color secondaryColor) { + // Prepare data for radar chart + final List scores = [ + 90, + 75, + 80, + 95, + 85 + ]; // Implementation, Rule Structure, Inference, Explanation, Overall + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + primaryColor, + secondaryColor, + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.analytics, + color: Colors.white, + size: 28, + ), + ), + const SizedBox(width: 16), + const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Forward Chaining Model Analysis', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + SizedBox(height: 4), + Text( + 'Evaluation of implementation quality', + style: TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 24), + + // Radar Chart + Container( + height: 280, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: buildAnalysisRadarChart( + scores, primaryColor, secondaryColor), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Analysis sections + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAnalysisSection( + 'Implementation Accuracy', + [ + 'The implementation correctly follows forward chaining principles:', + '• Uses a working memory to store facts', + '• Has a rule base of IF-THEN rules', + '• Implements pattern matching to find applicable rules', + '• Rules fire when their conditions match working memory', + '• Actions of fired rules can update scores (modify state)', + ], + 90, + Icons.check_circle, + primaryColor, + ), + _buildAnalysisSection( + 'Rule Structure', + [ + 'Rules follow standard structure but with simplifications:', + '• Conditions only check for presence of "Qn=Yes"', + '• More complex conditions (e.g., AND/OR combinations) aren\'t implemented', + '• Rule actions only increase scores instead of adding new facts', + '• The system does not support rule chaining (where firing one rule enables another)', + ], + 75, + Icons.rule, + primaryColor, + ), + _buildAnalysisSection( + 'Inference Process', + [ + 'The inference process is partially implemented:', + '• Rules are checked against working memory', + '• Matched rules are fired and actions executed', + '• The process loops until no more rules can fire', + '• However, since rules don\'t add new facts, the process completes in one iteration', + '• There\'s no conflict resolution strategy as all applicable rules are fired', + ], + 80, + Icons.psychology, + primaryColor, + ), + _buildAnalysisSection( + 'Results Explanation', + [ + 'The system provides good explanation capabilities:', + '• Shows which rules contributed to each recommendation', + '• Displays the questions that influenced the result', + '• Shows the weights/scores that led to the final ranking', + '• This transparency is a strength of the implementation', + ], + 95, + Icons.info, + primaryColor, + ), + _buildAnalysisSection( + 'Overall Assessment', + [ + 'This is a simplified but valid forward chaining implementation:', + '• It follows the core principles of the forward chaining method', + '• The implementation is well-suited for its specific use case', + '• The scoring mechanism is an appropriate adaptation for the recommendation context', + '• Areas for potential enhancement: more complex rule conditions, true fact generation, and multi-stage inference', + ], + 85, + Icons.assessment, + primaryColor, + ), + ], + ), + ), + + const SizedBox(height: 30), + + // Improvement suggestions + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.lightbulb, + color: Colors.amber.shade600, + size: 24, + ), + const SizedBox(width: 12), + const Text( + 'Improvement Suggestions', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 12), + _buildImprovementItem( + 'Implement full fact generation (not just scoring)', + 1, + Colors.blue, + ), + _buildImprovementItem( + 'Support complex conditions with AND/OR operators', + 2, + Colors.green, + ), + _buildImprovementItem( + 'Enable multi-stage inference with rule chaining', + 3, + Colors.purple, + ), + _buildImprovementItem( + 'Add conflict resolution strategies for rule prioritization', + 4, + Colors.orange, + ), + _buildImprovementItem( + 'Consider implementing backward chaining to complement forward chaining', + 5, + Colors.teal, + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 30), + ], + ), + ); + } + + Widget buildAnalysisRadarChart( + List scores, Color primaryColor, Color secondaryColor) { + return RadarChart( + RadarChartData( + radarShape: RadarShape.polygon, + radarBorderData: BorderSide( + color: Colors.grey.shade300, + width: 1, + ), + gridBorderData: BorderSide( + color: Colors.grey.shade200, + width: 1, + ), + titlePositionPercentageOffset: 0.2, + tickCount: 5, + dataSets: [ + RadarDataSet( + dataEntries: [ + RadarEntry(value: scores[0]), // Implementation + RadarEntry(value: scores[1]), // Rule Structure + RadarEntry(value: scores[2]), // Inference + RadarEntry(value: scores[3]), // Explanation + RadarEntry(value: scores[4]), // Overall + ], + fillColor: primaryColor.withOpacity(0.2), + borderColor: primaryColor, + borderWidth: 2, + entryRadius: 4, + ), + ], + ticksTextStyle: const TextStyle( + color: Colors.grey, + fontSize: 10, + ), + radarBackgroundColor: Colors.transparent, + borderData: FlBorderData(show: false), + getTitle: (index, angle) { + final titles = [ + 'Implementation', + 'Rule\nStructure', + 'Inference\nProcess', + 'Results\nExplanation', + 'Overall\nAssessment', + ]; + return RadarChartTitle( + text: titles[index], + angle: angle, + positionPercentageOffset: 0.1, + ); + }, + ), + ); + } + + Widget _buildAnalysisSection(String title, List points, + int scorePercent, IconData icon, Color primaryColor) { + final Color scoreColor = _getScoreColor(scorePercent); + + return Card( + margin: const EdgeInsets.only(bottom: 20), + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: primaryColor, + size: 22, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: scoreColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: scoreColor.withOpacity(0.3), + ), + ), + child: Text( + '$scorePercent%', + style: TextStyle( + color: scoreColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 12), + ...points.map((point) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + point, + style: TextStyle( + fontSize: 14, + height: 1.4, + color: point.startsWith('•') + ? Colors.grey.shade800 + : Colors.grey.shade900, + fontWeight: point.startsWith('•') + ? FontWeight.normal + : FontWeight.w500, + ), + ), + )), + ], + ), + ), + ); + } + + Widget _buildImprovementItem(String text, int number, Color color) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: color.withOpacity(0.1), + shape: BoxShape.circle, + border: Border.all( + color: color.withOpacity(0.3), + ), + ), + child: Center( + child: Text( + number.toString(), + style: TextStyle( + color: color, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + text, + style: const TextStyle( + fontSize: 15, + height: 1.4, + ), + ), + ), + ], + ), + ); + } + + Color _getScoreColor(int score) { + if (score >= 90) return Colors.green.shade700; + if (score >= 80) return Colors.teal.shade700; + if (score >= 70) return Colors.blue.shade700; + if (score >= 60) return Colors.amber.shade700; + if (score >= 50) return Colors.orange.shade700; + return Colors.red.shade700; + } + + Widget buildUGMDataTab(DevDataViewerController controller, Color primaryColor, + Color secondaryColor) { + return DefaultTabController( + length: 2, + child: Column( + children: [ + Container( + color: Colors.white, + child: TabBar( + indicatorColor: primaryColor, + indicatorWeight: 3, + labelColor: primaryColor, + unselectedLabelColor: Colors.grey.shade600, + labelStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + tabs: [ + Tab( + icon: Icon(Icons.school, color: primaryColor), + text: 'Program S1', + ), + Tab( + icon: Icon(Icons.school, color: secondaryColor), + text: 'Program D4', + ), + ], + ), + ), + Expanded( + child: TabBarView( + children: [ + // S1 Data Tab + buildUGMProgramTab(controller.biayaKuliahS1UGM, 'S1', + primaryColor, secondaryColor), + // D4 Data Tab + buildUGMProgramTab(controller.biayaKuliahD4UGM, 'D4', + primaryColor, secondaryColor), + ], + ), + ), + ], + ), + ); + } + + /// Helper widget to build UGM program data tab content with improved UI + Widget buildUGMProgramTab(List> data, String programType, + Color primaryColor, Color secondaryColor) { + if (data.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.info_outline, + size: 48, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'No data available for this program type', + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade600, + ), + ), + ], + ), + ); + } + + // Calculate average UKT for visualization + final List avgUktByCategory = []; + final List> filteredData = + data.where((row) => row['NO'] != '').toList(); + + for (int i = 1; i <= 6; i++) { + double sum = 0; + int count = 0; + for (var row in filteredData) { + final value = row['UKT_KELOMPOK_$i']; + if (value != null && value.toString().isNotEmpty) { + // Remove currency format and convert to double + final cleanValue = value + .toString() + .replaceAll('Rp', '') + .replaceAll('.', '') + .replaceAll(',', '') + .trim(); + if (cleanValue.isNotEmpty) { + sum += double.tryParse(cleanValue) ?? 0; + count++; + } + } + } + avgUktByCategory.add(count > 0 ? sum / count : 0); + } + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + primaryColor, + secondaryColor, + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.account_balance, + color: Colors.white, + size: 28, + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Biaya Kuliah UGM - Program $programType', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + Text( + '${filteredData.length} program studi', + style: const TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 24), + + // UKT Chart + Container( + height: 220, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Rata-rata UKT per Kelompok', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Visualisasi biaya rata-rata per kelompok UKT', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + Expanded( + child: buildUktChart( + avgUktByCategory, primaryColor, secondaryColor), + ), + ], + ), + ), + ], + ), + ), + + // Data Table + Padding( + padding: const EdgeInsets.all(16), + child: Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Data Lengkap UKT', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.grey.shade200, + ), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columnSpacing: 15, + horizontalMargin: 10, + headingRowColor: MaterialStateProperty.all( + primaryColor.withOpacity(0.1)), + headingTextStyle: TextStyle( + color: primaryColor, + fontWeight: FontWeight.bold, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + border: TableBorder( + borderRadius: BorderRadius.circular(8), + verticalInside: BorderSide( + color: Colors.grey.shade200, + ), + horizontalInside: BorderSide( + color: Colors.grey.shade200, + ), + ), + columns: [ + const DataColumn(label: Text('NO')), + const DataColumn(label: Text('PROGRAM')), + const DataColumn(label: Text('NAMA PROGRAM STUDI')), + const DataColumn(label: Text('BKT')), + const DataColumn(label: Text('UKT 1')), + const DataColumn(label: Text('UKT 2')), + const DataColumn(label: Text('UKT 3')), + const DataColumn(label: Text('UKT 4')), + const DataColumn(label: Text('UKT 5')), + const DataColumn(label: Text('UKT 6')), + ], + rows: filteredData.map((row) { + return DataRow( + cells: [ + DataCell(Text(row['NO'] ?? '')), + DataCell(Text(row['PROGRAM'] ?? '')), + DataCell(Text(row['NAMA PROGRAM STUDI'] ?? '')), + DataCell(Text(row['BKT PER SEMESTER'] ?? '')), + DataCell(_buildUktCell( + row['UKT_KELOMPOK_1'] ?? '', primaryColor)), + DataCell(_buildUktCell( + row['UKT_KELOMPOK_2'] ?? '', primaryColor)), + DataCell(_buildUktCell( + row['UKT_KELOMPOK_3'] ?? '', primaryColor)), + DataCell(_buildUktCell( + row['UKT_KELOMPOK_4'] ?? '', primaryColor)), + DataCell(_buildUktCell( + row['UKT_KELOMPOK_5'] ?? '', primaryColor)), + DataCell(_buildUktCell( + row['UKT_KELOMPOK_6'] ?? '', primaryColor)), + ], + ); + }).toList(), + ), + ), + ), + ], + ), + ), + ), + ), + + // UKT Explanation + Padding( + padding: const EdgeInsets.all(16), + child: Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + color: primaryColor, + size: 24, + ), + const SizedBox(width: 12), + const Text( + 'Penjelasan UKT', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 12), + _buildUktInfo( + 'UKT Kelompok 1', + 'Pendidikan Unggul Bersubsidi 100%', + 1, + primaryColor, + ), + _buildUktInfo( + 'UKT Kelompok 2', + 'Pendidikan Unggul Bersubsidi 100%', + 2, + primaryColor, + ), + _buildUktInfo( + 'UKT Kelompok 3', + 'Pendidikan Unggul Bersubsidi 75%', + 3, + primaryColor, + ), + _buildUktInfo( + 'UKT Kelompok 4', + 'Pendidikan Unggul Bersubsidi 50%', + 4, + primaryColor, + ), + _buildUktInfo( + 'UKT Kelompok 5', + 'Pendidikan Unggul Bersubsidi 25%', + 5, + primaryColor, + ), + _buildUktInfo( + 'UKT Kelompok 6', + 'Pendidikan Unggul (Biaya Penuh)', + 6, + primaryColor, + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + _buildUktInfoNote( + 'BKT: Biaya Kuliah Tunggal', + 'Biaya operasional per mahasiswa per semester', + Icons.account_balance_wallet, + Colors.amber.shade700, + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 20), + ], + ), + ); + } + + Widget _buildUktCell(String value, Color color) { + if (value.isEmpty) return const Text('-'); + + return Container( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), + decoration: BoxDecoration( + color: + value.contains('0') ? Colors.grey.shade100 : color.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + value, + style: TextStyle( + fontWeight: value.contains('0') ? FontWeight.normal : FontWeight.w500, + color: value.contains('0') ? Colors.grey.shade600 : Colors.black, + ), + ), + ); + } + + Widget _buildUktInfo( + String title, String description, int group, Color color) { + // Determine color based on group + final Color groupColor = group <= 2 + ? Colors.green.shade700 + : group <= 4 + ? Colors.amber.shade700 + : Colors.red.shade700; + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: groupColor.withOpacity(0.1), + shape: BoxShape.circle, + border: Border.all( + color: groupColor, + width: 1.5, + ), + ), + child: Center( + child: Text( + group.toString(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: groupColor, + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + const SizedBox(height: 2), + Text( + description, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildUktInfoNote( + String title, String description, IconData icon, Color color) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: color, + size: 20, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + const SizedBox(height: 2), + Text( + description, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ], + ); + } + + Widget buildUktChart( + List data, Color primaryColor, Color secondaryColor) { + final List barGroups = []; + + for (int i = 0; i < data.length; i++) { + // Determine color based on UKT group + final Color barColor = i <= 1 + ? Colors.green.shade700 + : i <= 3 + ? Colors.amber.shade700 + : Colors.red.shade700; + + barGroups.add( + BarChartGroupData( + x: i, + barRods: [ + BarChartRodData( + toY: data[i], + color: barColor, + width: 16, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + ), + ], + ), + ); + } + + return BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + getTooltipItem: (group, groupIndex, rod, rodIndex) { + // Format currency + final formattedValue = _formatCurrency(rod.toY); + + return BarTooltipItem( + 'UKT Kelompok ${groupIndex + 1}\n', + const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: formattedValue, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.normal, + ), + ), + ], + ); + }, + ), + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + return Text( + 'K${value.toInt() + 1}', + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ); + }, + reservedSize: 28, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 60, + getTitlesWidget: (value, meta) { + if (value == 0) return const Text(''); + // Show abbreviated values (e.g., 1M for 1,000,000) + String label = ''; + if (value >= 1000000) { + label = '${(value / 1000000).toStringAsFixed(1)}M'; + } else if (value >= 1000) { + label = '${(value / 1000).toStringAsFixed(0)}K'; + } else { + label = value.toStringAsFixed(0); + } + return Text( + label, + style: const TextStyle( + color: Colors.grey, + fontSize: 11, + ), + ); + }, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: false), + gridData: FlGridData( + show: true, + horizontalInterval: 1000000, // 1 million + getDrawingHorizontalLine: (value) => FlLine( + color: Colors.grey.shade200, + strokeWidth: 1, + dashArray: [5, 5], + ), + ), + barGroups: barGroups, + ), + ); + } + + String _formatCurrency(double value) { + if (value == 0) return 'Rp 0'; + + // Format as Indonesian Rupiah + final formatted = value.toStringAsFixed(0).replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (match) => '${match.group(1)}.'); + + return 'Rp $formatted'; + } +} diff --git a/lib/app/views/page_intro.dart b/lib/app/views/page_intro.dart new file mode 100644 index 0000000..6851120 --- /dev/null +++ b/lib/app/views/page_intro.dart @@ -0,0 +1,1604 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/splash_screen/page/page_splash_screen.dart'; +import 'package:forward_chaining_man_app/app/views/teacher_page.dart'; +import 'package:get/get.dart'; +import 'package:lottie/lottie.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:math' as math; + +class IntroPage extends StatefulWidget { + const IntroPage({Key? key}) : super(key: key); + + @override + State createState() => _IntroPageState(); +} + +class _IntroPageState extends State with TickerProviderStateMixin { + final PageController _pageController = PageController(); + late AnimationController _animationController; + late AnimationController _networkController; + int _currentPage = 0; + + // Network visualization data + final List _nodes = []; + final List _connections = []; + + final List> _onboardingPages = [ + { + 'title': 'Selamat Datang', + 'description': + 'Forward Chaining adalah aplikasi rekomendasi karir dan jurusan kuliah yang menggunakan sistem pakar berbasis aturan untuk memberikan hasil yang akurat.', + 'icon': Icons.psychology, + }, + { + 'title': 'Cara Kerja Aplikasi', + 'description': + 'Aplikasi ini menggunakan metode Forward Chaining, yaitu teknik inferensi sistem pakar yang mengevaluasi fakta awal (jawaban Anda) dan mencocokkannya dengan aturan secara berurutan untuk mencapai kesimpulan terbaik.', + 'icon': Icons.sync_alt, + }, + { + 'title': 'Alur Penggunaan', + 'description': + 'Anda akan menjawab pertanyaan seputar minat, bakat, dan kepribadian. Sistem kemudian menganalisis jawaban tersebut, menerapkan aturan Forward Chaining, dan menghasilkan rekomendasi karir atau jurusan kuliah yang paling sesuai.', + 'icon': Icons.question_answer, + }, + { + 'title': 'Siap Memulai', + 'description': + 'Silakan pilih peran Anda sebagai Siswa untuk mendapatkan rekomendasi karir/kuliah atau sebagai Guru untuk memantau hasil rekomendasi siswa Anda.', + 'icon': Icons.rocket_launch, + }, + ]; + + @override + void initState() { + super.initState(); + + // Create network nodes and connections + _setupNetworkGraph(); + + // Main controller for logo and elements animation + _animationController = AnimationController( + duration: const Duration(seconds: 10), + vsync: this, + )..repeat(); + + // Network animation controller + _networkController = AnimationController( + duration: const Duration(milliseconds: 15000), + vsync: this, + )..repeat(); + } + + // Setup network graph with nodes and connections + void _setupNetworkGraph() { + final random = math.Random(42); // Fixed seed for consistent layout + + // Create nodes - spreading them beyond screen boundaries + for (int i = 0; i < 25; i++) { + _nodes.add( + NetworkNode( + x: -0.3 + random.nextDouble() * 1.6, + y: -0.3 + random.nextDouble() * 1.6, + size: 3.0 + random.nextDouble() * 5.0, + ), + ); + } + + // Create connections between nodes + for (int i = 0; i < _nodes.length; i++) { + final connectionCount = 3 + random.nextInt(4); + final connectedIndices = {}; + + for (int j = 0; j < connectionCount; j++) { + int attempts = 0; + while (attempts < 10) { + final targetIndex = random.nextInt(_nodes.length); + + if (targetIndex != i && !connectedIndices.contains(targetIndex)) { + connectedIndices.add(targetIndex); + + _connections.add( + NetworkConnection( + sourceIndex: i, + targetIndex: targetIndex, + pulseOffset: random.nextDouble(), + pulseSpeed: 0.2 + random.nextDouble() * 0.3, + ), + ); + break; + } + attempts++; + } + } + } + } + + @override + void dispose() { + _pageController.dispose(); + _animationController.dispose(); + _networkController.dispose(); + super.dispose(); + } + + void _goToNextPage() { + if (_currentPage < _onboardingPages.length - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } + } + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + final screenWidth = MediaQuery.of(context).size.width; + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: Stack( + children: [ + // Grid background pattern + AnimatedBuilder( + animation: _networkController, + builder: (context, child) { + return CustomPaint( + painter: GridPatternPainter( + progress: _networkController.value, + ), + size: Size.infinite, + ); + }, + ), + + // + // Subtle wave pattern in background + AnimatedBuilder( + animation: _networkController, + builder: (context, child) { + return CustomPaint( + painter: WavePatternPainter( + progress: _networkController.value, + ), + size: Size.infinite, + ); + }, + ), + + // Main content + SafeArea( + child: Column( + children: [ + Expanded( + child: PageView.builder( + controller: _pageController, + itemCount: _onboardingPages.length, + onPageChanged: (index) { + setState(() { + _currentPage = index; + }); + }, + itemBuilder: (context, index) { + return _buildPageContent( + index, screenHeight, screenWidth); + }, + ), + ), + + // Bottom navigation and indicators + Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 20), + child: Column( + children: [ + // Page indicator dots + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: + List.generate(_onboardingPages.length, (index) { + return Container( + width: 8, + height: 8, + margin: const EdgeInsets.symmetric(horizontal: 5), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _currentPage == index + ? Colors.white + : Colors.white.withOpacity(0.3), + ), + ); + }), + ), + + const SizedBox(height: 20), + + // Next/Start button + _currentPage < _onboardingPages.length - 1 + ? _buildButton( + label: 'Lanjut', + icon: Icons.arrow_forward, + onTap: _goToNextPage, + ) + : _buildRoleSelectionSection(), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildPageContent(int index, double screenHeight, double screenWidth) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 20), + + // App Logo with Animation + Hero( + tag: 'app_logo', + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + // Animated logo with glow + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.rotate( + angle: _animationController.value * 2 * math.pi, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.blue.withOpacity(0.3), + width: 2, + ), + ), + ), + ); + }, + ), + + // Main icon with scale animation + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.scale( + scale: 0.9 + + 0.1 * + math.sin( + _animationController.value * 2 * math.pi), + child: Icon( + Icons.psychology, + size: 50, + color: Colors.indigo, + ), + ); + }, + ), + + // Glowing effect + AnimatedBuilder( + animation: _networkController, + builder: (context, child) { + return CustomPaint( + painter: GlowingCirclePainter( + progress: _networkController.value, + color: Colors.blue.shade500, + ), + size: const Size(100, 100), + ); + }, + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // App Title + const Text( + 'EduGuide', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + ), + ), + + const SizedBox(height: 8), + + // App Subtitle + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + 'Sistem Rekomendasi Karir & Kuliah', + style: TextStyle( + fontSize: 14, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + + const SizedBox(height: 40), + + const SizedBox(height: 24), + + // Page Title + Text( + _onboardingPages[index]['title'], + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + + const SizedBox(height: 16), + + // Page Description - Limited height and no scroll + Container( + constraints: BoxConstraints(maxHeight: screenHeight * 0.15), + child: Text( + _onboardingPages[index]['description'], + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 5, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + height: 1.5, + ), + ), + ), + + // Visual content based on page + SizedBox( + height: screenHeight * 0.15, + child: _buildPageVisualization(index), + ), + ], + ), + ); + } + + Widget _buildButton({ + required String label, + required IconData icon, + required VoidCallback onTap, + }) { + return ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.blue.shade800, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + elevation: 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Icon(icon, size: 20), + ], + ), + ); + } + + Widget _buildPageVisualization(int pageIndex) { + switch (pageIndex) { + case 0: + return _buildWelcomeVisualization(); + case 1: + return _buildForwardChainingVisualization(); + case 2: + return _buildWorkflowVisualization(); + case 3: + return _buildGetStartedVisualization(); + default: + return const SizedBox.shrink(); + } + } + + Widget _buildWelcomeVisualization() { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return CustomPaint( + painter: BrainNetworkPainter( + animation: _animationController.value, + color: Colors.white.withOpacity(0.6), + ), + size: const Size(300, 150), + ); + }, + ); + } + + Widget _buildForwardChainingVisualization() { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + // Animation cycle position (0-4) + final cyclePosition = (_animationController.value * 4) % 4; + final currentStep = cyclePosition.floor(); + final progress = cyclePosition - currentStep; + + // Define forward chaining steps + final List> steps = [ + { + 'icon': Icons.help_outline, + 'label': 'Input', + 'color': Colors.blue.shade400, + }, + { + 'icon': Icons.psychology, + 'label': 'Proses', + 'color': Colors.purple.shade400, + }, + { + 'icon': Icons.lightbulb_outline, + 'label': 'Analisis', + 'color': Colors.deepPurple.shade400, + }, + { + 'icon': Icons.check_circle_outline, + 'label': 'Hasil', + 'color': Colors.green.shade400, + }, + ]; + + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + + return Container( + height: 100, + child: Stack( + alignment: Alignment.center, + fit: StackFit.expand, + children: [ + // Custom painted curved line + CustomPaint( + painter: CurvedWorkflowPainter( + progress: currentStep + progress, + maxProgress: steps.length - 1, + stepColors: + steps.map((s) => s['color'] as Color).toList(), + ), + size: Size(width, 100), + ), + + // Step nodes + ...List.generate(4, (i) { + // Position calculation + final stepPosition = + width * 0.1 + (width * 0.8 * (i / (steps.length - 1))); + + // Animation states + bool isActive = currentStep == i; + bool isPrevious = currentStep > i; + bool isNext = (currentStep + 1) % 4 == i; + + double scale = 1.0; + + if (isActive) { + scale = 1.0 + 0.1 * math.sin(progress * math.pi * 2); + } else if (isNext && progress > 0.5) { + scale = 0.8 + + (0.2 * + ((progress - 0.5) * + 2)); // Start scaling up at progress 0.5 + } else { + scale = isPrevious ? 0.9 : 0.8; + } + + Color nodeColor = isActive + ? steps[i]['color'] + : isPrevious + ? steps[i]['color'].withOpacity(0.7) + : Colors.white.withOpacity(0.3); + + // Calculate Y position with slight wave effect + final nodeY = i % 2 == 0 ? 10.0 : 40.0; + + return Positioned( + top: nodeY, + left: stepPosition - 20, // Center node on position + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Transform.scale( + scale: scale, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + shape: BoxShape.circle, + border: Border.all( + color: nodeColor, + width: 2, + ), + boxShadow: isActive + ? [ + BoxShadow( + color: steps[i]['color'] + .withOpacity(0.4), + blurRadius: 8, + spreadRadius: 1, + ) + ] + : [], + ), + child: Center( + child: Icon( + steps[i]['icon'], + color: isActive || isPrevious + ? Colors.white + : Colors.white.withOpacity(0.5), + size: 20, + ), + ), + ), + ), + const SizedBox(height: 6), + Text( + steps[i]['label'], + style: TextStyle( + color: isActive || isPrevious + ? Colors.white + : Colors.white.withOpacity(0.5), + fontSize: 11, + fontWeight: isActive + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ); + }), + + // Animated pulse point on the curve + if (progress > 0 && progress < 1 && currentStep < 3) + AnimatedPulsePoint( + progress: currentStep + progress, + maxProgress: steps.length - 1, + width: width, + color: steps[(currentStep + 1) % 4]['color'], + ), + ], + ), + ); + }, + ); + }, + ); + } + +// WORKFLOW VISUALIZATION - Untuk halaman "Alur Penggunaan" + Widget _buildWorkflowVisualization() { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + // Animation cycle position (0-4) + final cyclePosition = (_animationController.value * 4) % 4; + final currentStep = cyclePosition.floor(); + final progress = cyclePosition - currentStep; + + // Define workflow steps + final List> steps = [ + { + 'icon': Icons.question_answer_outlined, + 'label': 'Jawab', + 'color': Colors.orange.shade400, + }, + { + 'icon': Icons.psychology, + 'label': 'Proses', + 'color': Colors.blue.shade400, + }, + { + 'icon': Icons.integration_instructions_outlined, + 'label': 'Analisis', + 'color': Colors.purple.shade400, + }, + { + 'icon': Icons.emoji_events_outlined, + 'label': 'Hasil', + 'color': Colors.green.shade400, + }, + ]; + + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + + return Container( + height: 100, + child: Stack( + alignment: Alignment.center, + fit: StackFit.expand, + children: [ + // Custom painted curved line + CustomPaint( + painter: CurvedWorkflowPainter( + progress: currentStep + progress, + maxProgress: steps.length - 1, + stepColors: + steps.map((s) => s['color'] as Color).toList(), + ), + size: Size(width, 100), + ), + + // Step nodes + ...List.generate(4, (i) { + // Position calculation + final stepPosition = + width * 0.1 + (width * 0.8 * (i / (steps.length - 1))); + + // Animation states + bool isActive = currentStep == i; + bool isPrevious = currentStep > i; + bool isNext = (currentStep + 1) % 4 == i; + + double scale = 1.0; + + if (isActive) { + scale = 1.0 + 0.1 * math.sin(progress * math.pi * 2); + } else if (isNext && progress > 0.5) { + scale = 0.8 + + (0.2 * + ((progress - 0.5) * + 2)); // Start scaling up at progress 0.5 + } else { + scale = isPrevious ? 0.9 : 0.8; + } + + Color nodeColor = isActive + ? steps[i]['color'] + : isPrevious + ? steps[i]['color'].withOpacity(0.7) + : Colors.white.withOpacity(0.3); + + // Calculate Y position with slight wave effect + final nodeY = i % 2 == 0 ? 10.0 : 40.0; + + return Positioned( + top: nodeY, + left: stepPosition - 20, // Center node on position + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Transform.scale( + scale: scale, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + shape: BoxShape.circle, + border: Border.all( + color: nodeColor, + width: 2, + ), + boxShadow: isActive + ? [ + BoxShadow( + color: steps[i]['color'] + .withOpacity(0.4), + blurRadius: 8, + spreadRadius: 1, + ) + ] + : [], + ), + child: Center( + child: Icon( + steps[i]['icon'], + color: isActive || isPrevious + ? Colors.white + : Colors.white.withOpacity(0.5), + size: 20, + ), + ), + ), + ), + const SizedBox(height: 6), + Text( + steps[i]['label'], + style: TextStyle( + color: isActive || isPrevious + ? Colors.white + : Colors.white.withOpacity(0.5), + fontSize: 11, + fontWeight: isActive + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ); + }), + + // Animated pulse point on the curve + if (progress > 0 && progress < 1 && currentStep < 3) + AnimatedPulsePoint( + progress: currentStep + progress, + maxProgress: steps.length - 1, + width: width, + color: steps[(currentStep + 1) % 4]['color'], + ), + ], + ), + ); + }, + ); + }, + ); + } + + Widget _buildStepBox(String text, IconData icon, double delay) { + final animValue = (_animationController.value + delay) % 1.0; + final pulseScale = 0.9 + 0.1 * math.sin(animValue * 2 * math.pi); + + return Container( + padding: const EdgeInsets.all(6), + margin: const EdgeInsets.all(3), + width: 85, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: Column( + children: [ + Transform.scale( + scale: pulseScale, + child: Icon(icon, color: Colors.white, size: 18), + ), + const SizedBox(height: 4), + Text( + text, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ], + ), + ); + } + + Widget _buildGetStartedVisualization() { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final animValue = _animationController.value; + + return Container( + height: 120, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildAnimatedRoleCard( + title: 'Siswa', + icon: Icons.school, + color: Colors.blue.shade500, + offset: 0, + ), + const SizedBox(width: 40), + _buildAnimatedRoleCard( + title: 'Guru', + icon: Icons.person_pin, + color: Colors.purple.shade500, + offset: 0.5, + ), + ], + ), + ); + }, + ); + } + + Widget _buildAnimatedRoleCard({ + required String title, + required IconData icon, + required Color color, + required double offset, + }) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final animValue = (_animationController.value + offset) % 1.0; + final scale = 0.9 + 0.1 * math.sin(animValue * 2 * math.pi); + final bounce = math.sin(animValue * 2 * math.pi) * 5; + + return Transform.translate( + offset: Offset(0, bounce), + child: Transform.scale( + scale: scale, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: color.withOpacity(0.5), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.3), + blurRadius: 10, + spreadRadius: 1, + ) + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: Colors.white, + size: 32, + ), + const SizedBox(height: 8), + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildRoleIcon(IconData icon, String label, double delay) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + final animValue = (_animationController.value + delay) % 1.0; + final scale = 0.9 + 0.1 * math.sin(animValue * 2 * math.pi); + + return Column( + children: [ + Transform.scale( + scale: scale, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: Colors.white, + size: 30, + ), + ), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + }, + ); + } + + Widget _buildRoleSelectionSection() { + return Column( + children: [ + // Student Role Button + _buildRoleButton( + title: 'Siswa', + subtitle: 'Dapatkan rekomendasi karir dan kuliah', + icon: Icons.school, + color: Colors.blue.shade600, + onTap: () { + _saveRole('student'); + Get.to( + () => StudentLoginPage(), + transition: Transition.rightToLeft, + duration: const Duration(milliseconds: 400), + ); + }, + ), + + const SizedBox(height: 16), + + // Teacher Role Button + _buildRoleButton( + title: 'Guru', + subtitle: 'Kelola data dan pantau perkembangan siswa', + icon: Icons.person_pin, + color: Colors.deepPurple, + onTap: () { + _saveRole('teacher'); + Get.to( + () => const TeacherLoginPage(), + transition: Transition.rightToLeft, + duration: const Duration(milliseconds: 400), + ); + }, + ), + ], + ); + } + + // Helper method to save user role preference + Future _saveRole(String role) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('user_role', role); + } + + // Helper method to build role selection button + Widget _buildRoleButton({ + required String title, + required String subtitle, + required IconData icon, + required Color color, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + Container( + width: 45, + height: 45, + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(15), + ), + child: Icon( + icon, + color: Colors.blue.shade600, + size: 28, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: const TextStyle( + fontSize: 12, + color: Colors.black54, + ), + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + color: color, + size: 14, + ), + ], + ), + ), + ); + } +} + +class WorkflowPathPainter extends CustomPainter { + final double progress; + final int currentStep; + final List stepColors; + + WorkflowPathPainter({ + required this.progress, + required this.currentStep, + required this.stepColors, + }); + + @override + void paint(Canvas canvas, Size size) { + final Paint pathPaint = Paint() + ..color = Colors.white.withOpacity(0.2) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + + final Paint activePaint = Paint() + ..color = stepColors[currentStep] + ..style = PaintingStyle.stroke + ..strokeWidth = 3.0; + + // We'll draw a curved path connecting all points + final Path path = Path(); + + // Calculate arc path + final width = size.width; + final height = size.height * 0.5; + final centerY = size.height * 0.5; + + // Start at left + path.moveTo(width * 0.25, centerY); + + // Draw curve through points + path.cubicTo( + width * 0.4, centerY - height, // control point 1 + width * 0.6, centerY + height, // control point 2 + width * 0.75, centerY, // end point + ); + + // Draw the inactive path + canvas.drawPath(path, pathPaint); + + // Draw the active progress path + if (currentStep < 3) { + // Determine path metrics for precise length calculations + PathMetrics metrics = path.computeMetrics(); + PathMetric pathMetric = metrics.first; + + // Calculate the percentage of path to draw based on current step and progress + double totalPathPercent = (currentStep + progress) / 3; + + // Extract the portion of the path that should be drawn + Path extractPath = Path(); + extractPath.addPath( + pathMetric.extractPath(0, pathMetric.length * totalPathPercent), + Offset.zero, + ); + + // Draw the active portion of the path + canvas.drawPath(extractPath, activePaint); + + // Add animated dots/pulse + if (progress > 0 && progress < 1) { + // Calculate the exact position for the animating dot + double animatingPointPercent = (currentStep + progress) / 3; + Tangent? tangent = pathMetric + .getTangentForOffset(pathMetric.length * animatingPointPercent); + + if (tangent != null) { + // Draw pulsing dot at the current animation point + canvas.drawCircle( + tangent.position, + 4 + 2 * math.sin(progress * math.pi), // Pulse effect + Paint()..color = stepColors[(currentStep + 1) % 4]); + } + } + } else { + // If we're at the last step, animate the whole path + canvas.drawPath(path, activePaint); + } + } + + @override + bool shouldRepaint(WorkflowPathPainter oldDelegate) { + return oldDelegate.progress != progress || + oldDelegate.currentStep != currentStep; + } +} + +// Node in the network +class NetworkNode { + final double x; // Position (0-1) + final double y; + final double size; + + NetworkNode({ + required this.x, + required this.y, + required this.size, + }); +} + +// Connection between nodes +class NetworkConnection { + final int sourceIndex; + final int targetIndex; + final double pulseOffset; // Random offset for animation + final double pulseSpeed; // Speed of pulse animation + + NetworkConnection({ + required this.sourceIndex, + required this.targetIndex, + required this.pulseOffset, + required this.pulseSpeed, + }); +} + +// Neural network visualization +class NetworkPainter extends CustomPainter { + final double progress; + final List nodes; + final List connections; + + NetworkPainter({ + required this.progress, + required this.nodes, + required this.connections, + }); + + @override + void paint(Canvas canvas, Size size) { + // Draw connections first (lines between nodes) + for (final connection in connections) { + final source = nodes[connection.sourceIndex]; + final target = nodes[connection.targetIndex]; + + final sourcePos = Offset(source.x * size.width, source.y * size.height); + final targetPos = Offset(target.x * size.width, target.y * size.height); + + // Calculate distance for line dashing + final dx = targetPos.dx - sourcePos.dx; + final dy = targetPos.dy - sourcePos.dy; + final distance = math.sqrt(dx * dx + dy * dy); + + // Create a normalized direction vector + final dirX = dx / distance; + final dirY = dy / distance; + + // Calculate pulse position based on progress + final pulseProgress = + (progress * connection.pulseSpeed + connection.pulseOffset) % 1.0; + final pulsePos = Offset( + sourcePos.dx + dx * pulseProgress, + sourcePos.dy + dy * pulseProgress, + ); + + // Draw the connection line + final linePaint = Paint() + ..color = Colors.white.withOpacity(0.15) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.2; + + canvas.drawLine(sourcePos, targetPos, linePaint); + + // Draw pulse traveling along the connection + final pulsePaint = Paint() + ..color = Colors.blue.shade100.withOpacity(0.5) + ..style = PaintingStyle.fill; + + canvas.drawCircle(pulsePos, 1.5, pulsePaint); + + // Add a subtle glow around the pulse + final glowPaint = Paint() + ..color = Colors.blue.shade100.withOpacity(0.2) + ..style = PaintingStyle.fill; + + canvas.drawCircle(pulsePos, 5.0, glowPaint); + } + + // Draw nodes + for (final node in nodes) { + final nodePos = Offset(node.x * size.width, node.y * size.height); + + // Node glow (outer circle) + final glowPaint = Paint() + ..color = Colors.blue.shade200.withOpacity(0.2) + ..style = PaintingStyle.fill; + + canvas.drawCircle(nodePos, node.size * 3.8, glowPaint); + + // Node main circle + final nodePaint = Paint() + ..color = Colors.white.withOpacity(0.7) + ..style = PaintingStyle.fill; + + canvas.drawCircle(nodePos, node.size, nodePaint); + + // Node inner circle + final innerPaint = Paint() + ..color = Colors.blue.shade100 + ..style = PaintingStyle.fill; + + canvas.drawCircle(nodePos, node.size * 0.6, innerPaint); + } + } + + @override + bool shouldRepaint(NetworkPainter oldDelegate) { + return oldDelegate.progress != progress; + } +} + +// Glowing circle around the icon +class GlowingCirclePainter extends CustomPainter { + final double progress; + final Color color; + + GlowingCirclePainter({required this.progress, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + + // Pulsing circular glow - very subtle + final double pulseSize = 1.0 + math.sin(progress * math.pi) * 0.08; + + // Draw multiple circles with diminishing opacity + for (int i = 0; i < 3; i++) { + final paint = Paint() + ..color = color.withOpacity(0.2 - (i * 0.05)) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 - (i * 0.5); + + // Each circle is larger than the previous + canvas.drawCircle( + center, + (40 + i * 5) * pulseSize, + paint, + ); + } + + // Draw spinning arc - very slow + final spinnerPaint = Paint() + ..color = color.withOpacity(0.7) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + + // Rotation angle changes very slowly with progress + final startAngle = progress * math.pi; + const arcLength = math.pi * 1.2; // Longer arc + + canvas.drawArc( + Rect.fromCircle(center: center, radius: 45), + startAngle, + arcLength, + false, + spinnerPaint, + ); + + // Second arc in opposite direction - even slower + canvas.drawArc( + Rect.fromCircle(center: center, radius: 35), + -startAngle * 0.7, + -arcLength * 0.8, + false, + spinnerPaint..strokeWidth = 1.5, + ); + } + + @override + bool shouldRepaint(GlowingCirclePainter oldDelegate) { + return oldDelegate.progress != progress || oldDelegate.color != color; + } +} + +// Custom painter for brain network visualization +class BrainNetworkPainter extends CustomPainter { + final double animation; + final Color color; + + BrainNetworkPainter({required this.animation, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke; + + final dotPaint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + // Create nodes + final nodes = []; + final random = math.Random(42); // Fixed seed for consistent points + for (int i = 0; i < 7; i++) { + nodes.add(Offset( + random.nextDouble() * size.width, + random.nextDouble() * size.height, + )); + } + + // Draw connections between nodes + for (int i = 0; i < nodes.length; i++) { + for (int j = i + 1; j < nodes.length; j++) { + final distance = (nodes[i] - nodes[j]).distance; + if (distance < size.width * 0.4) { + // Only connect nearby nodes + final path = Path() + ..moveTo(nodes[i].dx, nodes[i].dy) + ..lineTo(nodes[j].dx, nodes[j].dy); + + canvas.drawPath(path, paint); + + // Draw pulse along the line + final progress = (animation + i * 0.1) % 1.0; + final pointOffset = Offset.lerp(nodes[i], nodes[j], progress)!; + canvas.drawCircle(pointOffset, 2.5, dotPaint); + } + } + } + + // Draw nodes + for (final node in nodes) { + final animatedRadius = + 3.0 + 1.5 * math.sin(animation * 2 * math.pi + nodes.indexOf(node)); + canvas.drawCircle(node, animatedRadius, dotPaint); + } + } + + @override + bool shouldRepaint(covariant BrainNetworkPainter oldDelegate) { + return true; + } +} + +class CurvedWorkflowPainter extends CustomPainter { + final double progress; + final double maxProgress; + final List stepColors; + + CurvedWorkflowPainter({ + required this.progress, + required this.maxProgress, + required this.stepColors, + }); + + @override + void paint(Canvas canvas, Size size) { + final width = size.width; + final height = size.height; + + // Create base path for the full curve + final path = Path(); + path.moveTo( + width * 0.1, height * 0.25); // Start at 10% from left, at 25% height + + // Control points for the curve + List points = []; + + // Calculate points for the S curve + for (int i = 0; i < 4; i++) { + final x = width * 0.1 + (width * 0.8 * (i / (maxProgress))); + final y = + i % 2 == 0 ? height * 0.25 : height * 0.55; // Alternate Y positions + points.add(Offset(x, y)); + } + + // Draw curved path through points + for (int i = 0; i < points.length - 1; i++) { + final current = points[i]; + final next = points[i + 1]; + + // Middle point for control + final midX = (current.dx + next.dx) / 2; + + // Add curve segment + if (i == 0) { + path.quadraticBezierTo( + midX, current.dy, // Control point + next.dx, next.dy, // End point + ); + } else if (i == points.length - 2) { + path.quadraticBezierTo( + midX, current.dy, // Control point + next.dx, next.dy, // End point + ); + } else { + path.cubicTo( + midX, current.dy, // First control point + midX, next.dy, // Second control point + next.dx, next.dy, // End point + ); + } + } + + // Draw inactive path + final inactivePaint = Paint() + ..color = Colors.white.withOpacity(0.2) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round; + + canvas.drawPath(path, inactivePaint); + + // Draw active progress path + if (progress > 0) { + // Calculate total length + final PathMetrics pathMetrics = path.computeMetrics(); + final PathMetric pathMetric = pathMetrics.first; + + // Extract the active portion of the path + final double pathLength = pathMetric.length; + final double activePortion = (progress / maxProgress) * pathLength; + + final Path activePath = Path(); + activePath.addPath( + pathMetric.extractPath(0, activePortion), + Offset.zero, + ); + + // Create gradient for active path + final activeGradient = LinearGradient( + colors: [ + stepColors[0], + stepColors[math.min(progress.floor(), stepColors.length - 1)], + ], + ).createShader(Rect.fromLTWH(0, 0, width, height)); + + final activePaint = Paint() + ..shader = activeGradient + ..style = PaintingStyle.stroke + ..strokeWidth = 3.0 + ..strokeCap = StrokeCap.round; + + canvas.drawPath(activePath, activePaint); + } + } + + @override + bool shouldRepaint(CurvedWorkflowPainter oldDelegate) { + return oldDelegate.progress != progress; + } +} + +// Animated pulse point that follows the curve +class AnimatedPulsePoint extends StatelessWidget { + final double progress; + final double maxProgress; + final double width; + final Color color; + + const AnimatedPulsePoint({ + required this.progress, + required this.maxProgress, + required this.width, + required this.color, + }); + + @override + Widget build(BuildContext context) { + // Calculate position along the curve + final double x = width * 0.1 + (width * 0.8 * (progress / maxProgress)); + + // Calculate y position (alternating wave) + final int floorPos = progress.floor(); + final double fraction = progress - floorPos; + + double y; + if (floorPos % 2 == 0) { + // Moving from top to bottom + y = 30.0 + (40.0 * fraction); + } else { + // Moving from bottom to top + y = 70.0 - (40.0 * fraction); + } + + return Positioned( + left: x - 4, + top: y - 4, + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.6), + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/views/page_login.dart b/lib/app/views/page_login.dart new file mode 100644 index 0000000..f218e93 --- /dev/null +++ b/lib/app/views/page_login.dart @@ -0,0 +1,1114 @@ +import 'package:flutter/material.dart'; +import 'package:forward_chaining_man_app/app/views/student/page_student_dashboard.dart'; +import 'package:forward_chaining_man_app/main.dart'; +import 'package:get/get.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class StudentLoginController extends GetxController { + final FirebaseAuth _auth = FirebaseAuth.instance; + final GoogleSignIn _googleSignIn = GoogleSignIn(); + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + var isLoading = false.obs; + var errorMessage = ''.obs; + var isRegistering = false.obs; + var isRegisterUsingGoogle = false.obs; + // Text controllers for login/register form + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); + final nameController = TextEditingController(); + final selectedClass = Rx(null); + final selectedSchoolId = Rx(null); + + void checkValidClass() { + if (selectedClass.value != null && + !classOptions.contains(selectedClass.value)) { + selectedClass.value = null; + } + } + + final List asalSekolah = ["MAN 1 NGANJUK"]; + + final List classOptions = [ + 'X IPA A (1)', + 'X IPA B (2)', + 'X IPA C (3)', + 'X IPA D (4)', + 'XI IPA A (1)', + 'XI IPA B (2)', + 'XI IPA C (3)', + 'XI IPA D (4)', + 'XII IPA A (1)', + 'XII IPA B (2)', + 'XII IPA C (3)', + 'XII IPA D (4)', + ]; + + @override + void onInit() { + super.onInit(); + checkValidClass(); + } + + @override + void onClose() { + emailController.dispose(); + passwordController.dispose(); + confirmPasswordController.dispose(); + nameController.dispose(); + super.onClose(); + } + + // Check if user is already logged in + Future checkExistingSession() async { + try { + User? currentUser = _auth.currentUser; + if (currentUser != null) { + // Get the schoolId from shared preferences or another source + final prefs = await SharedPreferences.getInstance(); + final schoolId = prefs.getString('school_id'); + + if (schoolId != null) { + // Check if user data exists in Firestore as a subcollection of schools + final docSnapshot = await _firestore + .collection('schools') + .doc(schoolId) + .collection('students') + .doc(currentUser.uid) + .get(); + + if (docSnapshot.exists) { + // Save login session + await saveLoginSession(currentUser.uid, schoolId); + // Navigate to main app + Get.offAll(() => const PageStudentDashboard()); + } else { + // User authenticated but no profile exists + if (currentUser.displayName != null) { + nameController.text = currentUser.displayName!; + } + isRegistering.value = true; + } + } else { + // No school selected yet, user needs to complete registration + if (currentUser.displayName != null) { + nameController.text = currentUser.displayName!; + } + isRegistering.value = true; + } + } + } catch (e) { + print('Error checking session: $e'); + } + } + + // Save login session to shared preferences + Future saveLoginSession(String uid, String schoolId) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('user_uid', uid); + await prefs.setString('school_id', schoolId); + await prefs.setBool('is_logged_in', true); + + // Update last login timestamp + await _firestore + .collection('schools') + .doc(schoolId) + .collection('students') + .doc(uid) + .update({ + 'lastLogin': FieldValue.serverTimestamp(), + }); + } catch (e) { + print('Error saving session: $e'); + } + } + + // Toggle between login and register views + void toggleRegistrationMode() { + isRegistering.value = !isRegistering.value; + errorMessage.value = ''; + + // Clear fields when toggling + if (!isRegistering.value) { + nameController.clear(); + confirmPasswordController.clear(); + selectedClass.value = null; + } + } + + // Sign in with email and password + Future signInWithEmailPassword() async { + try { + isLoading.value = true; + errorMessage.value = ''; + + if (emailController.text.trim().isEmpty || + passwordController.text.isEmpty) { + errorMessage.value = 'Email dan password tidak boleh kosong'; + return; + } + + final userCredential = await _auth.signInWithEmailAndPassword( + email: emailController.text.trim(), + password: passwordController.text, + ); + + if (userCredential.user != null) { + // Find which school this user belongs to + String? schoolId; + + // Query all schools to find the student + final schoolsSnapshot = await _firestore.collection('schools').get(); + + for (var schoolDoc in schoolsSnapshot.docs) { + final studentDoc = await schoolDoc.reference + .collection('students') + .doc(userCredential.user!.uid) + .get(); + + if (studentDoc.exists) { + schoolId = schoolDoc.id; + break; + } + } + + if (schoolId != null) { + // Save login session with school ID + await saveLoginSession(userCredential.user!.uid, schoolId); + // Navigate to main app + Get.offAll(() => const PageStudentDashboard()); + } else { + errorMessage.value = 'Akun tidak ditemukan di sekolah manapun'; + await _auth.signOut(); + } + } + } on FirebaseAuthException catch (e) { + switch (e.code) { + case 'user-not-found': + errorMessage.value = 'Email tidak terdaftar'; + break; + case 'wrong-password': + errorMessage.value = 'Password salah'; + break; + case 'invalid-email': + errorMessage.value = 'Format email tidak valid'; + break; + default: + errorMessage.value = 'Gagal masuk: ${e.message}'; + } + } catch (e) { + errorMessage.value = 'Terjadi kesalahan: ${e.toString()}'; + } finally { + isLoading.value = false; + } + } + + // Register with email and password + Future registerWithEmailPassword() async { + try { + isLoading.value = true; + errorMessage.value = ''; + + // Validasi input + if (emailController.text.trim().isEmpty || + passwordController.text.isEmpty || + nameController.text.trim().isEmpty || + selectedClass.value == null || + selectedSchoolId.value == null) { + errorMessage.value = 'Semua field harus diisi'; + return; + } + + if (passwordController.text != confirmPasswordController.text) { + errorMessage.value = 'Password tidak cocok'; + return; + } + + if (passwordController.text.length < 6) { + errorMessage.value = 'Password minimal 6 karakter'; + return; + } + + // Buat akun user + final userCredential = await _auth.createUserWithEmailAndPassword( + email: emailController.text.trim(), + password: passwordController.text, + ); + + if (userCredential.user != null) { + String studentId = userCredential.user!.uid; + String schoolId = selectedSchoolId.value!; // Pastikan schoolId valid + + // Pastikan sekolah ada di Firestore + DocumentReference schoolRef = + _firestore.collection('schools').doc(schoolId); + DocumentSnapshot schoolSnapshot = await schoolRef.get(); + + if (!schoolSnapshot.exists) { + errorMessage.value = 'Sekolah yang dipilih tidak ditemukan'; + return; + } + + // Perbarui nama tampilan di Firebase Auth + await userCredential.user! + .updateDisplayName(nameController.text.trim()); + + // Simpan data siswa dalam subkoleksi `students` di dalam `schools` + await schoolRef.collection('students').doc(studentId).set({ + 'name': nameController.text.trim(), + 'class': selectedClass.value, + 'email': userCredential.user!.email, + 'registrationType': 'email', + 'createdAt': FieldValue.serverTimestamp(), + 'lastLogin': FieldValue.serverTimestamp(), + }); + + // Simpan sesi login + await saveLoginSession(studentId, schoolId); + + // Navigasi ke dashboard siswa + Get.offAll(() => const PageStudentDashboard()); + } + } on FirebaseAuthException catch (e) { + switch (e.code) { + case 'email-already-in-use': + errorMessage.value = 'Email sudah terdaftar'; + break; + case 'invalid-email': + errorMessage.value = 'Format email tidak valid'; + break; + case 'weak-password': + errorMessage.value = 'Password terlalu lemah'; + break; + default: + errorMessage.value = 'Gagal mendaftar: ${e.message}'; + } + } catch (e) { + errorMessage.value = 'Terjadi kesalahan: ${e.toString()}'; + } finally { + isLoading.value = false; + } + } + +// First, add this to your StudentLoginController class: + +// Add this reactive variable to track Google sign-in state + +// Updated Google Sign-In method with the new variable + Future signInWithGoogle() async { + try { + isLoading.value = true; + errorMessage.value = ''; + + // Start the Google Sign-in process + final GoogleSignInAccount? googleUser = await _googleSignIn.signIn(); + + if (googleUser == null) { + isLoading.value = false; + return; // User canceled the sign-in + } + + // Get authentication details from Google + final GoogleSignInAuthentication googleAuth = + await googleUser.authentication; + + // Create credential for Firebase + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + // Sign in with Firebase + final userCredential = await _auth.signInWithCredential(credential); + final user = userCredential.user; + + if (user != null) { + // Find which school this user belongs to + String? schoolId; + bool userExists = false; + + // Query all schools to find the student + final schoolsSnapshot = await _firestore.collection('schools').get(); + + for (var schoolDoc in schoolsSnapshot.docs) { + final studentDoc = await schoolDoc.reference + .collection('students') + .doc(user.uid) + .get(); + + if (studentDoc.exists) { + schoolId = schoolDoc.id; + userExists = true; + // Update login time + await schoolDoc.reference + .collection('students') + .doc(user.uid) + .update({ + 'lastLogin': FieldValue.serverTimestamp(), + }); + break; + } + } + + if (userExists && schoolId != null) { + // Existing user - save login session + await saveLoginSession(user.uid, schoolId); + // Navigate to main app + Get.offAll(() => const PageStudentDashboard()); + } else { + // New user - need to complete profile + // Pre-fill name from Google account + nameController.text = user.displayName ?? ''; + emailController.text = user.email ?? ''; + isRegistering.value = true; + isRegisterUsingGoogle.value = + true; // Set to true when signing in with Google + + // Show toast message + Get.snackbar( + 'Lengkapi Profil', + 'Silakan pilih kelas dan sekolah untuk melanjutkan', + backgroundColor: Colors.blue.shade100, + colorText: Colors.blue.shade800, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(16), + ); + } + } + } catch (e) { + errorMessage.value = 'Login gagal: ${e.toString()}'; + } finally { + isLoading.value = false; + } + } + +// Modify the completeGoogleProfile method to use the isRegisterUsingGoogle flag + Future completeGoogleProfile() async { + try { + isLoading.value = true; + errorMessage.value = ''; + + User? currentUser = _auth.currentUser; + if (currentUser == null) { + errorMessage.value = 'Tidak ada user yang terautentikasi'; + return; + } + + if (nameController.text.trim().isEmpty || + selectedClass.value == null || + selectedSchoolId.value == null) { + errorMessage.value = 'Nama, kelas, dan sekolah tidak boleh kosong'; + return; + } + + String studentId = currentUser.uid; + String schoolId = + selectedSchoolId.value!; // Pastikan schoolId dipilih dan valid + + // Pastikan sekolah sudah ada di Firestore + DocumentReference schoolRef = + _firestore.collection('schools').doc(schoolId); + DocumentSnapshot schoolSnapshot = await schoolRef.get(); + + if (!schoolSnapshot.exists) { + errorMessage.value = 'Sekolah yang dipilih tidak ditemukan'; + return; + } + + // Simpan data siswa dalam subkoleksi students di dalam sekolah + await schoolRef.collection('students').doc(studentId).set({ + 'name': nameController.text.trim(), + 'class': selectedClass.value, + 'email': currentUser.email, + 'photoURL': currentUser.photoURL, + 'registrationType': 'google', + 'createdAt': FieldValue.serverTimestamp(), + 'lastLogin': FieldValue.serverTimestamp(), + }); + + // Update display name jika perlu + if (currentUser.displayName != nameController.text.trim()) { + await currentUser.updateDisplayName(nameController.text.trim()); + } + + // Simpan sesi login + await saveLoginSession(studentId, schoolId); + + // Reset the isRegisterUsingGoogle flag + isRegisterUsingGoogle.value = false; + + // Navigasi ke dashboard + Get.offAll(() => const PageStudentDashboard()); + } catch (e) { + errorMessage.value = 'Gagal menyimpan profil: ${e.toString()}'; + } finally { + isLoading.value = false; + } + } +} + +class StudentLoginPage extends StatelessWidget { + String routeName = "/StudentLoginPage"; + StudentLoginPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: SafeArea( + child: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only(top: 8, bottom: 20), + child: TextButton( + onPressed: () { + Get.back(); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + backgroundColor: Colors.white.withOpacity(0.2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.arrow_back, size: 18), + SizedBox(width: 8), + Text('Kembali ke Pemilihan Peran'), + ], + ), + ), + ), + ), + // App Logo + Hero( + tag: 'app_logo', + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: const Center( + child: Icon( + Icons.psychology, + size: 60, + color: Colors.indigo, + ), + ), + ), + ), + + const SizedBox(height: 20), + + // App Title + const Text( + 'EduGuide', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + ), + ), + + const Text( + 'Sistem Rekomendasi Karir & Kuliah', + style: TextStyle( + fontSize: 15, + color: Colors.white70, + letterSpacing: 0.5, + ), + ), + + const SizedBox(height: 30), + + // Login/Register Card + Obx(() => Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Text( + controller.isRegistering.value + ? 'Daftar Akun' + : 'Masuk Akun', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + + const SizedBox(height: 8), + + Text( + controller.isRegistering.value + ? 'Lengkapi informasi untuk mendaftar' + : 'Masuk untuk melanjutkan ke aplikasi', + style: const TextStyle( + fontSize: 14, + color: Colors.black54, + ), + ), + + const SizedBox(height: 24), + + // Registration form fields + if (controller.isRegistering.value) ...[ + // Name field + _buildTextField( + controller: controller.nameController, + label: 'Nama Lengkap', + prefixIcon: Icons.person_outline, + ), + ], + + const SizedBox(height: 24), +// Email field - Hide if Google sign-in + Obx(() => controller.isRegisterUsingGoogle.value + ? const SizedBox + .shrink() // Hide if Google sign-in + : _buildTextField( + controller: controller.emailController, + label: 'Email', + prefixIcon: Icons.email_outlined, + keyboardType: + TextInputType.emailAddress, + )), + + Obx(() => controller.isRegisterUsingGoogle.value + ? const SizedBox + .shrink() // No spacer if email is hidden + : const SizedBox(height: 16)), + +// Password field - Hide if Google sign-in + Obx(() => controller.isRegisterUsingGoogle.value + ? const SizedBox + .shrink() // Hide if Google sign-in + : CustomTextField( + controller: + controller.passwordController, + label: 'Password', + prefixIcon: Icons.lock_outline, + isPassword: true, + )), + +// Only show confirm password in registration mode and not Google sign-in + if (controller.isRegistering.value) ...[ + Obx(() => controller + .isRegisterUsingGoogle.value + ? const SizedBox + .shrink() // No spacer if password is hidden + : const SizedBox(height: 16)), + + // Confirm password field - Hide if Google sign-in + Obx(() => controller + .isRegisterUsingGoogle.value + ? const SizedBox + .shrink() // Hide if Google sign-in + : CustomTextField( + controller: controller + .confirmPasswordController, + label: 'Konfirmasi Password', + prefixIcon: Icons.lock_outline, + isPassword: true, + )), + + const SizedBox(height: 16), + + // School selection - Always show in registration + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.grey.shade300), + ), + child: Obx(() => + DropdownButtonHideUnderline( + child: DropdownButton( + value: controller + .selectedSchoolId.value, + hint: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 16), + child: Row( + children: [ + Icon(Icons.school_outlined, + color: Colors + .indigo.shade300), + const SizedBox(width: 12), + const Text('Pilih Sekolah'), + ], + ), + ), + isExpanded: true, + borderRadius: + BorderRadius.circular(15), + icon: Padding( + padding: const EdgeInsets.only( + right: 16), + child: Icon(Icons.arrow_drop_down, + color: + Colors.indigo.shade400), + ), + items: controller.asalSekolah + .map((String kelas) { + return DropdownMenuItem( + value: kelas, + child: Padding( + padding: const EdgeInsets + .symmetric( + horizontal: 16), + child: Row( + children: [ + Icon( + Icons.school_outlined, + color: Colors + .indigo.shade300), + const SizedBox(width: 12), + Text(kelas), + ], + ), + ), + ); + }).toList(), + onChanged: (String? newValue) { + controller.selectedSchoolId + .value = newValue; + }, + ), + )), + ), + const SizedBox(height: 16), + + // Class dropdown - Always show in registration + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.grey.shade300), + ), + child: Obx(() => + DropdownButtonHideUnderline( + child: DropdownButton( + value: + controller.selectedClass.value, + hint: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 16), + child: Row( + children: [ + Icon(Icons.school_outlined, + color: Colors + .indigo.shade300), + const SizedBox(width: 12), + const Text('Pilih Kelas'), + ], + ), + ), + isExpanded: true, + borderRadius: + BorderRadius.circular(15), + icon: Padding( + padding: const EdgeInsets.only( + right: 16), + child: Icon(Icons.arrow_drop_down, + color: + Colors.indigo.shade400), + ), + items: controller.classOptions + .map((String kelas) { + return DropdownMenuItem( + value: kelas, + child: Padding( + padding: const EdgeInsets + .symmetric( + horizontal: 16), + child: Row( + children: [ + Icon( + Icons.school_outlined, + color: Colors + .indigo.shade300), + const SizedBox(width: 12), + Text(kelas), + ], + ), + ), + ); + }).toList(), + onChanged: (String? newValue) { + controller.selectedClass.value = + newValue; + }, + ), + )), + ), + ], + + const SizedBox(height: 24), + + // Error message + Obx(() => controller + .errorMessage.value.isNotEmpty + ? Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: + BorderRadius.circular(12), + border: Border.all( + color: Colors.red.shade200, + ), + ), + child: Row( + children: [ + Icon(Icons.error_outline, + color: Colors.red.shade800), + const SizedBox(width: 8), + Expanded( + child: Text( + controller.errorMessage.value, + style: TextStyle( + color: Colors.red.shade700, + fontSize: 14, + ), + ), + ), + ], + ), + ) + : const SizedBox.shrink()), + + const SizedBox(height: 24), + + // Login/Register button + _buildPrimaryButton( + label: controller.isRegistering.value + ? 'Daftar' + : 'Masuk', + icon: controller.isRegistering.value + ? Icons.person_add + : Icons.login, + isLoading: controller.isLoading.value, + onPressed: () { + if (controller.isRegistering.value) { + // Check if completing Google profile or regular registration + if (FirebaseAuth.instance.currentUser != + null && + FirebaseAuth.instance.currentUser! + .providerData + .any((provider) => + provider.providerId == + 'google.com')) { + controller.completeGoogleProfile(); + } else { + controller.registerWithEmailPassword(); + } + } else { + controller.signInWithEmailPassword(); + } + }, + ), + + const SizedBox(height: 16), + + // Google sign-in button + if (!controller.isRegistering.value || + FirebaseAuth.instance.currentUser == null) + _buildGoogleButton( + onPressed: controller.isLoading.value + ? null + : controller.signInWithGoogle, + isLoading: controller.isLoading.value, + ), + + const SizedBox(height: 24), + + // Toggle between login and register + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + controller.isRegistering.value + ? 'Sudah punya akun? ' + : 'Belum punya akun? ', + style: const TextStyle( + color: Colors.black54, + ), + ), + TextButton( + onPressed: controller.isLoading.value + ? null + : controller.toggleRegistrationMode, + child: Text( + controller.isRegistering.value + ? 'Masuk' + : 'Daftar', + style: TextStyle( + color: Colors.indigo.shade700, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + ), + )), + + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ), + ), + ); + } + + // Helper method to build text fields + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required IconData prefixIcon, + bool isPassword = false, + TextInputType keyboardType = TextInputType.text, + }) { + return TextField( + controller: controller, + obscureText: isPassword, + keyboardType: keyboardType, + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(prefixIcon, color: Colors.indigo.shade300), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide(color: Colors.indigo.shade500), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + ); + } + + // Helper method to build primary button + Widget _buildPrimaryButton({ + required String label, + required IconData icon, + required VoidCallback onPressed, + required bool isLoading, + }) { + return ElevatedButton( + onPressed: isLoading ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo.shade800, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + elevation: 0, + disabledBackgroundColor: Colors.indigo.shade300, + ), + child: isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 20), + const SizedBox(width: 10), + Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ); + } + + // Helper method to build Google button + Widget _buildGoogleButton({ + required VoidCallback? onPressed, + required bool isLoading, + }) { + return OutlinedButton( + onPressed: onPressed, + style: OutlinedButton.styleFrom( + foregroundColor: Colors.black87, + backgroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + side: BorderSide(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/ic_google.png', + height: 24, + width: 24, + errorBuilder: (context, error, stackTrace) => const Icon( + Icons.g_mobiledata, + size: 24, + color: Colors.deepOrange, + ), + ), + const SizedBox(width: 10), + const Text( + 'Lanjutkan dengan Google', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} + +class CustomTextField extends StatefulWidget { + final TextEditingController controller; + final String label; + final IconData prefixIcon; + final bool isPassword; + final TextInputType keyboardType; + + const CustomTextField({ + Key? key, + required this.controller, + required this.label, + required this.prefixIcon, + this.isPassword = false, + this.keyboardType = TextInputType.text, + }) : super(key: key); + + @override + _CustomTextFieldState createState() => _CustomTextFieldState(); +} + +class _CustomTextFieldState extends State { + bool _obscureText = true; + + @override + void initState() { + super.initState(); + _obscureText = widget.isPassword; + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: widget.controller, + obscureText: widget.isPassword ? _obscureText : false, + keyboardType: widget.keyboardType, + decoration: InputDecoration( + labelText: widget.label, + prefixIcon: Icon(widget.prefixIcon, color: Colors.indigo.shade300), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide(color: Colors.indigo.shade500), + ), + filled: true, + fillColor: Colors.grey.shade50, + suffixIcon: widget.isPassword + ? IconButton( + icon: Icon( + _obscureText ? Icons.visibility_off : Icons.visibility, + color: Colors.grey.shade600, + ), + onPressed: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + ) + : null, + ), + ); + } +} diff --git a/lib/app/views/page_profile.dart b/lib/app/views/page_profile.dart new file mode 100644 index 0000000..28b3820 --- /dev/null +++ b/lib/app/views/page_profile.dart @@ -0,0 +1,817 @@ +import 'package:flutter/material.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:get/get.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ProfileController extends GetxController { + final FirebaseAuth _auth = FirebaseAuth.instance; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + var isLoading = false.obs; + var isEditing = false.obs; + var userProfile = Rx>({}); + var schoolId = ''.obs; + + final nameController = TextEditingController(); + final selectedClass = Rx(null); + + // Class options for the dropdown + final List classOptions = [ + 'X IPA A (1)', + 'X IPA B (2)', + 'X IPA C (3)', + 'X IPA D (4)', + 'XI IPA A (1)', + 'XI IPA B (2)', + 'XI IPA C (3)', + 'XI IPA D (4)', + 'XII IPA A (1)', + 'XII IPA B (2)', + 'XII IPA C (3)', + 'XII IPA D (4)', + ]; + + @override + void onInit() { + super.onInit(); + loadUserProfile(); + } + + @override + void onClose() { + nameController.dispose(); + super.onClose(); + } + + Future loadUserProfile() async { + try { + isLoading.value = true; + + User? currentUser = _auth.currentUser; + if (currentUser == null) { + Get.offAll(() => StudentLoginPage()); + return; + } + + // Get schoolId from SharedPreferences + final prefs = await SharedPreferences.getInstance(); + final storedSchoolId = prefs.getString('school_id'); + + if (storedSchoolId != null && storedSchoolId.isNotEmpty) { + schoolId.value = storedSchoolId; + + // Get student data from the subcollection + final docSnapshot = await _firestore + .collection('schools') + .doc(schoolId.value) + .collection('students') + .doc(currentUser.uid) + .get(); + + if (docSnapshot.exists) { + userProfile.value = docSnapshot.data() as Map; + + // Initialize controllers with current values + nameController.text = userProfile.value['name'] ?? ''; + selectedClass.value = userProfile.value['class']; + } else { + // Student not found in this school + await _findStudentInAllSchools(currentUser); + } + } else { + // School ID not found in shared preferences, search in all schools + await _findStudentInAllSchools(currentUser); + } + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal memuat profil: ${e.toString()}', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + ); + } finally { + isLoading.value = false; + } + } + + // Helper method to find a student in all schools + Future _findStudentInAllSchools(User currentUser) async { + try { + final schoolsSnapshot = await _firestore.collection('schools').get(); + bool found = false; + + for (var schoolDoc in schoolsSnapshot.docs) { + final studentDoc = await schoolDoc.reference + .collection('students') + .doc(currentUser.uid) + .get(); + + if (studentDoc.exists) { + // Found the student + schoolId.value = schoolDoc.id; + userProfile.value = studentDoc.data() as Map; + + // Save school ID to SharedPreferences + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('school_id', schoolId.value); + + // Initialize controllers with current values + nameController.text = userProfile.value['name'] ?? ''; + selectedClass.value = userProfile.value['class']; + found = true; + break; + } + } + + // If student was not found in any school + if (!found) { + nameController.text = currentUser.displayName ?? ''; + selectedClass.value = null; + + Get.snackbar( + 'Perhatian', + 'Profil tidak ditemukan. Silakan lengkapi data profil Anda.', + backgroundColor: Colors.orange.shade100, + colorText: Colors.orange.shade800, + ); + + // Enter edit mode automatically + isEditing.value = true; + } + } catch (e) { + print('Error finding student in schools: $e'); + throw e; + } + } + + void toggleEditMode() { + isEditing.value = !isEditing.value; + + // Reset controllers to current values when canceling edit + if (!isEditing.value) { + nameController.text = userProfile.value['name'] ?? ''; + selectedClass.value = userProfile.value['class']; + } else { + selectedClass.value = ""; + } + } + + Future saveProfile() async { + try { + isLoading.value = true; + + if (nameController.text.trim().isEmpty || + selectedClass.value == null || + selectedClass.value!.isEmpty) { + Get.snackbar( + 'Error', + 'Nama dan kelas tidak boleh kosong', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + ); + return; + } + + User? currentUser = _auth.currentUser; + if (currentUser == null) { + return; + } + + // If schoolId is empty, we need to select a school + if (schoolId.value.isEmpty) { + // For simplicity, using the first school available + // In a real app, you might want to show a school selection UI + final schoolsSnapshot = await _firestore.collection('schools').get(); + if (schoolsSnapshot.docs.isNotEmpty) { + schoolId.value = schoolsSnapshot.docs.first.id; + + // Save school ID to SharedPreferences + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('school_id', schoolId.value); + } else { + Get.snackbar( + 'Error', + 'Tidak ada sekolah yang tersedia', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + ); + return; + } + } + + // Update Firestore in the appropriate subcollection + await _firestore + .collection('schools') + .doc(schoolId.value) + .collection('students') + .doc(currentUser.uid) + .set({ + 'name': nameController.text.trim(), + 'class': selectedClass.value, + 'email': currentUser.email, + 'photoURL': currentUser.photoURL, + 'updatedAt': FieldValue.serverTimestamp(), + }, SetOptions(merge: true)); + + // Update display name if needed + if (currentUser.displayName != nameController.text.trim()) { + await currentUser.updateDisplayName(nameController.text.trim()); + } + + // Reload profile + await loadUserProfile(); + + // Exit edit mode + isEditing.value = false; + + Get.snackbar( + 'Sukses', + 'Profil berhasil diperbarui', + backgroundColor: Colors.green.shade100, + colorText: Colors.green.shade800, + ); + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal menyimpan profil: ${e.toString()}', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + ); + } finally { + isLoading.value = false; + } + } + + Future logout() async { + try { + isLoading.value = true; + + // Clear shared preferences + final prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + + // Sign out from Firebase + await _auth.signOut(); + + // Navigate to login page + Get.offAll(() => IntroPage()); + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal keluar: ${e.toString()}', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + ); + } finally { + isLoading.value = false; + } + } +} + +class ProfilePage extends StatelessWidget { + const ProfilePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(ProfileController()); + + return Scaffold( + appBar: AppBar( + title: const Text( + 'Profil Saya', + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.indigo.shade800, + foregroundColor: Colors.white, + elevation: 0, + actions: [ + Obx( + () => controller.isEditing.value + ? IconButton( + onPressed: controller.toggleEditMode, + icon: const Icon(Icons.close), + tooltip: 'Batal', + ) + : IconButton( + onPressed: controller.toggleEditMode, + icon: const Icon(Icons.edit), + tooltip: 'Edit Profil', + ), + ), + ], + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.indigo.shade800, + Colors.blue.shade800, + ], + ), + ), + child: SafeArea( + bottom: false, + child: Obx( + () => controller.isLoading.value + ? const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + // Profile Header + _buildProfileHeader(controller), + + const SizedBox(height: 30), + + // Profile Content + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Informasi Profil', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + + const SizedBox(height: 24), + + // Profile Fields + Obx( + () => controller.isEditing.value + ? _buildEditFields(controller) + : _buildDisplayFields(controller), + ), + + const SizedBox(height: 24), + + // Save Button (only in edit mode) + Obx( + () => controller.isEditing.value + ? ElevatedButton( + onPressed: controller.saveProfile, + style: ElevatedButton.styleFrom( + backgroundColor: + Colors.indigo.shade800, + foregroundColor: Colors.white, + padding: + const EdgeInsets.symmetric( + vertical: 16), + minimumSize: const Size( + double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(15), + ), + elevation: 0, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: const [ + Icon(Icons.save, size: 20), + SizedBox(width: 10), + Text( + 'Simpan Perubahan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ) + : const SizedBox.shrink(), + ), + + const SizedBox(height: 8), + + // Logout Button + ElevatedButton( + onPressed: () { + Get.dialog( + AlertDialog( + title: const Text('Konfirmasi'), + content: const Text( + 'Apakah Anda yakin ingin keluar?'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Batal'), + ), + ElevatedButton( + onPressed: () { + Get.back(); + controller.logout(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + child: const Text('Keluar'), + ), + ], + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade50, + foregroundColor: Colors.red.shade700, + padding: const EdgeInsets.symmetric( + vertical: 16), + minimumSize: + const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + elevation: 0, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: const [ + Icon(Icons.logout, size: 20), + SizedBox(width: 10), + Text( + 'Keluar Akun', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // App version + Center( + child: Text( + 'App Version 1.0.0', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildProfileHeader(ProfileController controller) { + final User? currentUser = FirebaseAuth.instance.currentUser; + + return Column( + children: [ + // Profile Picture + CircleAvatar( + radius: 50, + backgroundColor: Colors.white, + backgroundImage: currentUser?.photoURL != null + ? NetworkImage(currentUser!.photoURL!) + : null, + child: currentUser?.photoURL == null + ? Text( + controller.userProfile.value['name'] != null + ? controller.userProfile.value['name'] + .substring(0, 1) + .toUpperCase() + : '?', + style: const TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + color: Colors.indigo, + ), + ) + : null, + ), + + const SizedBox(height: 16), + + // Name + Text( + controller.userProfile.value['name'] ?? 'Siswa', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + + const SizedBox(height: 4), + + // Email + Text( + currentUser?.email ?? '', + style: const TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + + const SizedBox(height: 8), + + // Class Badge + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.school, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 6), + Text( + controller.userProfile.value['class'] ?? 'Kelas tidak diatur', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildDisplayFields(ProfileController controller) { + final User? currentUser = FirebaseAuth.instance.currentUser; + + return Column( + children: [ + // Name field + _buildInfoItem( + icon: Icons.person, + title: 'Nama Lengkap', + value: controller.userProfile.value['name'] ?? '-', + ), + + const SizedBox(height: 16), + + // Class field + _buildInfoItem( + icon: Icons.school, + title: 'Kelas', + value: controller.userProfile.value['class'] ?? '-', + ), + + const SizedBox(height: 16), + + // Email field + _buildInfoItem( + icon: Icons.email, + title: 'Email', + value: currentUser?.email ?? '-', + ), + + const SizedBox(height: 16), + + // Registration Type + _buildInfoItem( + icon: Icons.login, + title: 'Jenis Registrasi', + value: controller.userProfile.value['registrationType'] == 'google' + ? 'Google' + : 'Email & Password', + ), + + const SizedBox(height: 16), + + // Join Date + _buildInfoItem( + icon: Icons.calendar_today, + title: 'Tanggal Bergabung', + value: controller.userProfile.value['createdAt'] != null + ? _formatTimestamp(controller.userProfile.value['createdAt']) + : '-', + ), + ], + ); + } + + Widget _buildEditFields(ProfileController controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name field + const Text( + 'Nama Lengkap', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.black54, + ), + ), + const SizedBox(height: 8), + TextField( + controller: controller.nameController, + decoration: InputDecoration( + prefixIcon: Icon(Icons.person, color: Colors.indigo.shade300), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide(color: Colors.indigo.shade500), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + + const SizedBox(height: 20), + + // Class dropdown + const Text( + 'Kelas', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.black54, + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.grey.shade300), + ), + child: Obx(() { + // Validate that value exists in items list + final currentValue = controller.selectedClass.value; + final isValueValid = currentValue != null && + controller.classOptions.contains(currentValue); + + return DropdownButtonHideUnderline( + child: DropdownButton( + // Only set value if it's valid + value: isValueValid ? currentValue : null, + hint: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Icon(Icons.school_outlined, + color: Colors.indigo.shade300), + const SizedBox(width: 12), + const Text('Pilih Kelas'), + ], + ), + ), + isExpanded: true, + borderRadius: BorderRadius.circular(15), + icon: Padding( + padding: const EdgeInsets.only(right: 16), + child: Icon(Icons.arrow_drop_down, + color: Colors.indigo.shade400), + ), + items: controller.classOptions.map((String kelas) { + return DropdownMenuItem( + value: kelas, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Icon(Icons.school_outlined, + color: Colors.indigo.shade300), + const SizedBox(width: 12), + Text(kelas), + ], + ), + ), + ); + }).toList(), + onChanged: (String? newValue) { + controller.selectedClass.value = newValue; + }, + ), + ); + }), + ), + ], + ); + } + + Widget _buildInfoItem({ + required IconData icon, + required String title, + required String value, + }) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(15), + border: Border.all(color: Colors.grey.shade200), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.indigo.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: Colors.indigo.shade400, + size: 20, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + color: Colors.black54, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ); + } + + String _formatTimestamp(dynamic timestamp) { + if (timestamp == null) return '-'; + + try { + if (timestamp is Timestamp) { + final DateTime dateTime = timestamp.toDate(); + return '${dateTime.day}/${dateTime.month}/${dateTime.year}'; + } + } catch (e) { + print('Error formatting timestamp: $e'); + } + + return '-'; + } +} diff --git a/lib/app/views/page_teacher_register.dart b/lib/app/views/page_teacher_register.dart new file mode 100644 index 0000000..4c24f36 --- /dev/null +++ b/lib/app/views/page_teacher_register.dart @@ -0,0 +1,465 @@ +import 'package:flutter/material.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:get/get.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +class TeacherRegisterController extends GetxController { + final FirebaseAuth _auth = FirebaseAuth.instance; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + var isLoading = false.obs; + var errorMessage = ''.obs; + + // Text controllers for registration form + final nameController = TextEditingController(); + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); + final schoolController = TextEditingController(); + final subjectController = TextEditingController(); + + @override + void onClose() { + nameController.dispose(); + emailController.dispose(); + passwordController.dispose(); + confirmPasswordController.dispose(); + schoolController.dispose(); + subjectController.dispose(); + super.onClose(); + } + + // Register new teacher account + Future registerTeacher() async { + try { + // Reset error message + errorMessage.value = ''; + + // Basic validation + if (nameController.text.trim().isEmpty || + emailController.text.trim().isEmpty || + passwordController.text.isEmpty || + confirmPasswordController.text.isEmpty || + schoolController.text.trim().isEmpty) { + errorMessage.value = 'Semua kolom wajib diisi'; + return; + } + + // Password validation + if (passwordController.text.length < 6) { + errorMessage.value = 'Password minimal 6 karakter'; + return; + } + + // Confirm password validation + if (passwordController.text != confirmPasswordController.text) { + errorMessage.value = 'Password tidak sama'; + return; + } + + // Start loading + isLoading.value = true; + + // Create user in Firebase Auth + final userCredential = await _auth.createUserWithEmailAndPassword( + email: emailController.text.trim(), + password: passwordController.text, + ); + + if (userCredential.user != null) { + // Add teacher data to Firestore + await _firestore + .collection('teachers') + .doc(userCredential.user!.uid) + .set({ + 'name': nameController.text.trim(), + 'email': emailController.text.trim(), + 'school': schoolController.text.trim(), + 'subject': subjectController.text.trim(), + 'createdAt': FieldValue.serverTimestamp(), + 'lastLogin': FieldValue.serverTimestamp(), + 'role': 'teacher', + }); + + // Show success message + Get.snackbar( + 'Berhasil', + 'Akun guru berhasil dibuat', + backgroundColor: Colors.green.shade100, + colorText: Colors.green.shade800, + snackPosition: SnackPosition.BOTTOM, + margin: const EdgeInsets.all(16), + ); + + // Navigate back to login + Get.back(); + } + } on FirebaseAuthException catch (e) { + switch (e.code) { + case 'email-already-in-use': + errorMessage.value = 'Email sudah digunakan'; + break; + case 'invalid-email': + errorMessage.value = 'Format email tidak valid'; + break; + case 'weak-password': + errorMessage.value = 'Password terlalu lemah'; + break; + default: + errorMessage.value = 'Gagal mendaftar: ${e.message}'; + } + } catch (e) { + errorMessage.value = 'Terjadi kesalahan: ${e.toString()}'; + } finally { + isLoading.value = false; + } + } +} + +class TeacherRegisterPage extends StatelessWidget { + const TeacherRegisterPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(TeacherRegisterController()); + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: SafeArea( + child: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Back button at the top + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only(top: 8, bottom: 20), + child: TextButton( + onPressed: () { + Get.back(); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + backgroundColor: Colors.white.withOpacity(0.2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.arrow_back, size: 18), + SizedBox(width: 8), + Text('Kembali ke Halaman Login'), + ], + ), + ), + ), + ), + + // App Logo with Hero animation + Hero( + tag: 'app_logo', + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: Center( + child: Icon( + Icons.psychology, + size: 60, + color: Colors.blue.shade700, + ), + ), + ), + ), + + const SizedBox(height: 20), + + // App Title + const Text( + 'EduGuide', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + ), + ), + + const SizedBox(height: 8), + + // Register Subtitle + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + 'Daftar Akun Guru', + style: TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + + const SizedBox(height: 30), + + // Registration Card + Obx(() => Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.app_registration_rounded, + color: Colors.blue.shade600, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Daftar Akun Baru', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 4), + const Text( + 'Lengkapi data diri Anda', + style: TextStyle( + fontSize: 14, + color: Colors.black54, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 30), + + // Name field + CustomTextField( + controller: controller.nameController, + label: 'Nama Lengkap', + prefixIcon: Icons.person_outline, + ), + + const SizedBox(height: 16), + + // Email field + CustomTextField( + controller: controller.emailController, + label: 'Email', + prefixIcon: Icons.email_outlined, + keyboardType: TextInputType.emailAddress, + ), + + const SizedBox(height: 16), + + // School field + CustomTextField( + controller: controller.schoolController, + label: 'Sekolah', + prefixIcon: Icons.school_outlined, + ), + + const SizedBox(height: 16), + + // Subject field + CustomTextField( + controller: controller.subjectController, + label: 'Mata Pelajaran (Opsional)', + prefixIcon: Icons.book_outlined, + ), + + const SizedBox(height: 16), + + // Password field + CustomTextField( + controller: controller.passwordController, + label: 'Password', + prefixIcon: Icons.lock_outline, + isPassword: true, + ), + + const SizedBox(height: 16), + + // Confirm Password field + CustomTextField( + controller: + controller.confirmPasswordController, + label: 'Konfirmasi Password', + prefixIcon: Icons.lock_outline, + isPassword: true, + ), + + const SizedBox(height: 24), + + // Error message + Obx(() => controller + .errorMessage.value.isNotEmpty + ? Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: + BorderRadius.circular(12), + border: Border.all( + color: Colors.red.shade200, + ), + ), + child: Row( + children: [ + Icon(Icons.error_outline, + color: Colors.red.shade800), + const SizedBox(width: 8), + Expanded( + child: Text( + controller.errorMessage.value, + style: TextStyle( + color: Colors.red.shade700, + fontSize: 14, + ), + ), + ), + ], + ), + ) + : const SizedBox.shrink()), + + const SizedBox(height: 24), + + // Register button + _buildPrimaryButton( + label: 'Daftar', + icon: Icons.how_to_reg, + isLoading: controller.isLoading.value, + onPressed: controller.registerTeacher, + ), + ], + ), + ), + )), + + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ), + ), + ); + } + + // Helper method to build text fields + + // Helper method to build primary button + Widget _buildPrimaryButton({ + required String label, + required IconData icon, + required VoidCallback onPressed, + required bool isLoading, + }) { + return ElevatedButton( + onPressed: isLoading ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + elevation: 0, + disabledBackgroundColor: Colors.blue.shade300, + ), + child: isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 20), + const SizedBox(width: 10), + Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ); + } +} diff --git a/lib/app/views/splash_screen/page/page_splash_screen.dart b/lib/app/views/splash_screen/page/page_splash_screen.dart new file mode 100644 index 0000000..a29d948 --- /dev/null +++ b/lib/app/views/splash_screen/page/page_splash_screen.dart @@ -0,0 +1,693 @@ +// SplashScreen - Halaman pertama yang muncul dengan animasi dan cek sesi +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/student/page_student_dashboard.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; + +import 'package:shared_preferences/shared_preferences.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({Key? key}) : super(key: key); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State + with TickerProviderStateMixin { + late AnimationController _mainController; + late AnimationController _networkController; + late Animation _fadeAnimation; + late Animation _scaleAnimation; + late Animation _translateAnimation; + + // List of network nodes for neural network visualization + final List _nodes = []; + final List _connections = []; + + @override + void initState() { + super.initState(); + + // Create network nodes and connections + _setupNetworkGraph(); + + // Main controller for logo animations - 3 seconds + _mainController = AnimationController( + duration: const Duration(milliseconds: 3000), + vsync: this, + ); + + // Separate slow controller for network animations - 15 seconds and repeat + _networkController = AnimationController( + duration: const Duration(milliseconds: 15000), + vsync: this, + ); + + // Fade in animation for text + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _mainController, + curve: const Interval(0.3, 0.8, curve: Curves.easeInOut), + )); + + // Scale animation for logo - starts very small + _scaleAnimation = Tween( + begin: 0.1, + end: 1.0, + ).animate(CurvedAnimation( + parent: _mainController, + curve: const Interval(0.1, 0.6, curve: Curves.elasticOut), + )); + + // Translation animation - starts way below screen + _translateAnimation = Tween( + begin: 300.0, // Start far below center + end: 0.0, // End at center + ).animate(CurvedAnimation( + parent: _mainController, + curve: const Interval(0.1, 0.6, curve: Curves.easeOutCubic), + )); + + // Start animations + _mainController.forward(); + _networkController.repeat(); // Continuous very slow animation for network + + // Check user session after animation completes + Future.delayed(const Duration(milliseconds: 3500), () { + checkExistingSession(); + }); + } + + // Setup network graph with nodes and connections + void _setupNetworkGraph() { + final random = math.Random(42); // Fixed seed for consistent layout + + // Create nodes - spreading them beyond screen boundaries + for (int i = 0; i < 25; i++) { + // Increased number of nodes + _nodes.add( + NetworkNode( + x: -0.3 + + random.nextDouble() * + 1.6, // Expand beyond screen boundaries (-0.3 to 1.3) + y: -0.3 + random.nextDouble() * 1.6, + size: 3.0 + random.nextDouble() * 5.0, // Slightly larger nodes + ), + ); + } + + // Create connections between nodes - increased connections + for (int i = 0; i < _nodes.length; i++) { + // Each node connects to 3-6 other nodes + final connectionCount = 3 + random.nextInt(4); + final connectedIndices = {}; + + for (int j = 0; j < connectionCount; j++) { + // Try to find a new node to connect to + int attempts = 0; + while (attempts < 10) { + final targetIndex = random.nextInt(_nodes.length); + + // Don't connect to self and don't duplicate connections + if (targetIndex != i && !connectedIndices.contains(targetIndex)) { + connectedIndices.add(targetIndex); + + // Add the connection + _connections.add( + NetworkConnection( + sourceIndex: i, + targetIndex: targetIndex, + pulseOffset: random.nextDouble(), + pulseSpeed: + 0.2 + random.nextDouble() * 0.3, // Slightly slower pulse + ), + ); + break; + } + attempts++; + } + } + } + } + + Future checkExistingSession() async { + final FirebaseAuth _auth = FirebaseAuth.instance; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + try { + User? currentUser = _auth.currentUser; + if (currentUser != null) { + // Get the schoolId from shared preferences + final prefs = await SharedPreferences.getInstance(); + final schoolId = prefs.getString('school_id'); + + if (schoolId != null && schoolId.isNotEmpty) { + // Check if user data exists in the specific school + final docSnapshot = await _firestore + .collection('schools') + .doc(schoolId) + .collection('students') + .doc(currentUser.uid) + .get(); + + if (docSnapshot.exists) { + // Navigate to dashboard + Get.offAll(() => const PageStudentDashboard()); + return; + } + } + + // If no school ID found or user not found in that school, search all schools + final schoolsSnapshot = await _firestore.collection('schools').get(); + bool foundInAnySchool = false; + + for (var schoolDoc in schoolsSnapshot.docs) { + final studentDoc = await schoolDoc.reference + .collection('students') + .doc(currentUser.uid) + .get(); + + if (studentDoc.exists) { + // Found student in this school, save the school ID + await prefs.setString('school_id', schoolDoc.id); + foundInAnySchool = true; + + // Navigate to dashboard + Get.offAll(() => const PageStudentDashboard()); + break; + } + } + + if (!foundInAnySchool) { + // User authenticated but no profile found in any school + Get.offAll(() => const IntroPage()); + } + } else { + // User not logged in, go to intro + Get.offAll(() => const IntroPage()); + } + } catch (e) { + print('Error checking session: $e'); + // If error, go to intro + Get.offAll(() => const IntroPage()); + } + } + + @override + void dispose() { + _mainController.dispose(); + _networkController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + // Beautiful gradient background + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: Stack( + children: [ + // Grid background pattern + AnimatedBuilder( + animation: _networkController, + builder: (context, child) { + return CustomPaint( + painter: GridPatternPainter( + progress: _networkController.value, + ), + size: Size.infinite, + ); + }, + ), + + // Neural network background - using CustomPaint for better performance + AnimatedBuilder( + animation: _networkController, + builder: (context, child) { + return CustomPaint( + painter: NetworkPainter( + progress: _networkController.value, + nodes: _nodes, + connections: _connections, + ), + size: Size.infinite, + ); + }, + ), + + // Subtle wave pattern in background + AnimatedBuilder( + animation: _networkController, + builder: (context, child) { + return CustomPaint( + painter: WavePatternPainter( + progress: _networkController.value, + ), + size: Size.infinite, + ); + }, + ), + + // Main content - centered column + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo with animation from bottom to center with scaling + AnimatedBuilder( + animation: _mainController, + builder: (context, child) { + return Transform.translate( + offset: Offset(0, _translateAnimation.value), + child: Transform.scale( + scale: _scaleAnimation.value, + child: child, + ), + ); + }, + child: Container( + width: 150, + height: 150, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.blue.shade700.withOpacity(0.6), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: Center( + child: Stack( + alignment: Alignment.center, + children: [ + // Brain icon with glow effect + Container( + width: 110, + height: 110, + decoration: BoxDecoration( + color: Colors.blue.shade100.withOpacity(0.2), + shape: BoxShape.circle, + ), + ), + Icon( + Icons.psychology, + size: 95, + color: Colors.blue.shade700, + ), + // Animated glow around the icon + AnimatedBuilder( + animation: _networkController, + builder: (context, child) { + return CustomPaint( + painter: GlowingCirclePainter( + progress: _networkController.value, + color: Colors.blue.shade500, + ), + size: const Size(120, 120), + ); + }, + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 40), + + // App title with fade animation + FadeTransition( + opacity: _fadeAnimation, + child: Text( + 'EduGuide', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.5, + shadows: [ + Shadow( + color: Colors.black54, + blurRadius: 12, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Subtitle with fade animation + FadeTransition( + opacity: _fadeAnimation, + child: Text( + 'Rekomendasi Minat Bakat Anda', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w300, + color: Colors.white.withOpacity(0.8), + letterSpacing: 1.0, + ), + ), + ), + + const SizedBox(height: 40), + + // Loading indicator with delayed fade + AnimatedBuilder( + animation: _mainController, + builder: (context, child) { + return Opacity( + opacity: _mainController.value > 0.5 + ? (_mainController.value - 0.5) * 2 + : 0, + child: child, + ); + }, + child: SizedBox( + width: 40, + height: 40, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Colors.white.withOpacity(0.9)), + strokeWidth: 3, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +// Node in the network +class NetworkNode { + final double x; // Position (0-1) + final double y; + final double size; + + NetworkNode({ + required this.x, + required this.y, + required this.size, + }); +} + +// Connection between nodes +class NetworkConnection { + final int sourceIndex; + final int targetIndex; + final double pulseOffset; // Random offset for animation + final double pulseSpeed; // Speed of pulse animation + + NetworkConnection({ + required this.sourceIndex, + required this.targetIndex, + required this.pulseOffset, + required this.pulseSpeed, + }); +} + +// Neural network visualization +class NetworkPainter extends CustomPainter { + final double progress; + final List nodes; + final List connections; + + NetworkPainter({ + required this.progress, + required this.nodes, + required this.connections, + }); + + @override + void paint(Canvas canvas, Size size) { + // Draw connections first (lines between nodes) + for (final connection in connections) { + final source = nodes[connection.sourceIndex]; + final target = nodes[connection.targetIndex]; + + final sourcePos = Offset(source.x * size.width, source.y * size.height); + final targetPos = Offset(target.x * size.width, target.y * size.height); + + // Calculate distance for line dashing + final dx = targetPos.dx - sourcePos.dx; + final dy = targetPos.dy - sourcePos.dy; + final distance = math.sqrt(dx * dx + dy * dy); + + // Create a normalized direction vector + final dirX = dx / distance; + final dirY = dy / distance; + + // Calculate pulse position based on progress + final pulseProgress = + (progress * connection.pulseSpeed + connection.pulseOffset) % 1.0; + final pulsePos = Offset( + sourcePos.dx + dx * pulseProgress, + sourcePos.dy + dy * pulseProgress, + ); + + // Draw the connection line + final linePaint = Paint() + ..color = Colors.white.withOpacity(0.15) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.2; + + canvas.drawLine(sourcePos, targetPos, linePaint); + + // Draw pulse traveling along the connection + final pulsePaint = Paint() + ..color = Colors.blue.shade100.withOpacity(0.5) + ..style = PaintingStyle.fill; + + canvas.drawCircle(pulsePos, 2.5, pulsePaint); + + // Add a subtle glow around the pulse + final glowPaint = Paint() + ..color = Colors.blue.shade100.withOpacity(0.2) + ..style = PaintingStyle.fill; + + canvas.drawCircle(pulsePos, 5.0, glowPaint); + } + + // Draw nodes + for (final node in nodes) { + final nodePos = Offset(node.x * size.width, node.y * size.height); + + // Node glow (outer circle) + final glowPaint = Paint() + ..color = Colors.blue.shade200.withOpacity(0.2) + ..style = PaintingStyle.fill; + + canvas.drawCircle(nodePos, node.size * 1.8, glowPaint); + + // Node main circle + final nodePaint = Paint() + ..color = Colors.white.withOpacity(0.7) + ..style = PaintingStyle.fill; + + canvas.drawCircle(nodePos, node.size, nodePaint); + + // Node inner circle + final innerPaint = Paint() + ..color = Colors.blue.shade100 + ..style = PaintingStyle.fill; + + canvas.drawCircle(nodePos, node.size * 0.6, innerPaint); + } + } + + @override + bool shouldRepaint(NetworkPainter oldDelegate) { + return oldDelegate.progress != progress; + } +} + +// Glowing circle around the icon +class GlowingCirclePainter extends CustomPainter { + final double progress; + final Color color; + + GlowingCirclePainter({required this.progress, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + + // Pulsing circular glow - very subtle + final double pulseSize = 1.0 + math.sin(progress * math.pi) * 0.08; + + // Draw multiple circles with diminishing opacity + for (int i = 0; i < 3; i++) { + final paint = Paint() + ..color = color.withOpacity(0.2 - (i * 0.05)) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 - (i * 0.5); + + // Each circle is larger than the previous + canvas.drawCircle( + center, + (50 + i * 5) * pulseSize, + paint, + ); + } + + // Draw spinning arc - very slow + final spinnerPaint = Paint() + ..color = color.withOpacity(0.7) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + + // Rotation angle changes very slowly with progress + final startAngle = progress * math.pi; + const arcLength = math.pi * 1.2; // Longer arc + + canvas.drawArc( + Rect.fromCircle(center: center, radius: 55), + startAngle, + arcLength, + false, + spinnerPaint, + ); + + // Second arc in opposite direction - even slower + canvas.drawArc( + Rect.fromCircle(center: center, radius: 45), + -startAngle * 0.7, + -arcLength * 0.8, + false, + spinnerPaint..strokeWidth = 1.5, + ); + } + + @override + bool shouldRepaint(GlowingCirclePainter oldDelegate) { + return oldDelegate.progress != progress || oldDelegate.color != color; + } +} + +// Subtle wave pattern in background +class WavePatternPainter extends CustomPainter { + final double progress; + + WavePatternPainter({required this.progress}); + + @override + void paint(Canvas canvas, Size size) { + // Very subtle horizontal waves + final wavePaint = Paint() + ..color = Colors.blue.shade200.withOpacity(0.03) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.0; + + const waveCount = 4; + final waveHeight = size.height / waveCount; + + for (int i = 0; i < waveCount; i++) { + final path = Path(); + final baseY = i * waveHeight; + final amplitude = 8.0; // Reduced wave height + + path.moveTo(0, baseY); + + // Draw a smooth sine wave across the screen - very slow movement + for (double x = 0; x <= size.width; x += 10) { + // Very slow wave movement (reduced multiplier from 0.5 to 0.2) + final waveY = baseY + + math.sin((x / size.width * 4) + progress * math.pi * 0.2) * + amplitude; + path.lineTo(x, waveY); + } + + canvas.drawPath(path, wavePaint); + } + } + + @override + bool shouldRepaint(WavePatternPainter oldDelegate) { + return oldDelegate.progress != progress; + } +} + +// Grid background pattern +class GridPatternPainter extends CustomPainter { + final double progress; + + GridPatternPainter({required this.progress}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.05) + ..style = PaintingStyle.stroke + ..strokeWidth = 0.8; + + // Create animated grid pattern + final spacing = 35.0; + final xCount = (size.width / spacing).ceil() + 1; + final yCount = (size.height / spacing).ceil() + 1; + final offset = progress * spacing * 0.5; // Slow movement + + // Horizontal lines + for (int i = 0; i < yCount; i++) { + final y = i * spacing - offset; + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + } + + // Vertical lines + for (int i = 0; i < xCount; i++) { + final x = i * spacing - offset; + canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint); + } + + // Add some diagonal lines for more depth + final diagonalPaint = Paint() + ..color = Colors.blue.shade100.withOpacity(0.04) + ..style = PaintingStyle.stroke + ..strokeWidth = 0.7; + + final maxDim = math.max(size.width, size.height); + final diagonalSpacing = 70.0; + final diagCount = (maxDim / diagonalSpacing).ceil() * 2; + final diagOffset = progress * diagonalSpacing * 0.3; // Very slow movement + + for (int i = -diagCount; i < diagCount; i++) { + final startX = i * diagonalSpacing + diagOffset; + canvas.drawLine( + Offset(startX, 0), + Offset(startX + maxDim, maxDim), + diagonalPaint, + ); + } + } + + @override + bool shouldRepaint(GridPatternPainter oldDelegate) { + return oldDelegate.progress != progress; + } +} diff --git a/lib/app/views/student/controller/student_controller.dart b/lib/app/views/student/controller/student_controller.dart new file mode 100644 index 0000000..6d43860 --- /dev/null +++ b/lib/app/views/student/controller/student_controller.dart @@ -0,0 +1,13 @@ +import 'package:forward_chaining_man_app/app/controllers/developer_controller.dart'; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:get/get_state_manager/src/simple/get_controllers.dart'; + +/// Controller untuk DeveloperModePage +class DeveloperModeController extends GetxController { + final RxBool isDeveloperMode = developerMode.obs; + + void toggleDeveloperMode(bool value) { + isDeveloperMode.value = value; + developerMode = value; + } +} diff --git a/lib/app/views/student/feature/quiz/controller/question_controller.dart b/lib/app/views/student/feature/quiz/controller/question_controller.dart new file mode 100644 index 0000000..3de7032 --- /dev/null +++ b/lib/app/views/student/feature/quiz/controller/question_controller.dart @@ -0,0 +1,1081 @@ +/// Controller untuk QuestionPage +/// import 'dart:convert'; +import 'dart:convert'; +import 'dart:math'; +import 'dart:developer' as dev; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/app/views/about/page_about.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:forward_chaining_man_app/app/views/student/model/data_student.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:firebase_core/firebase_core.dart'; +import 'package:intl/intl.dart' as intl; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:url_launcher/url_launcher.dart'; + +class QuestionController extends GetxController { + final bool isKerja; + final String majorType; // "SAINS" atau "TEKNIK" + final RxList highlightedQuestionIds = [].obs; + bool shouldScrollToTop = false; + + QuestionController({ + required this.isKerja, + required this.majorType, + }); + void autoFillAnswers(bool answer) { + for (var q in questionsThisPage) { + setAnswer(q, answer); + } + } + +// Tambahkan metode berikut ke dalam QuestionController + Future autoFillAllPagesNotCorrect(bool? answer) async { + // Simpan halaman saat ini + final currentPageValue = currentPage.value; + + // Iterasi melalui semua halaman + for (var page = 0; page < totalPages; page++) { + // Pindah ke halaman tersebut + currentPage.value = page; + + // Tunggu rendering selesai + await Future.delayed(const Duration(milliseconds: 10)); + + // Isi jawaban pada halaman ini + if (answer != null) { + // Isi semua jawaban dengan nilai yang sama + for (var q in questionsThisPage) { + setAnswer(q, answer); + } + } else { + // Isi jawaban secara acak + final random = Random(); + for (var q in questionsThisPage) { + setAnswer(q, random.nextBool()); + } + } + } + + // Pindah ke halaman terakhir + currentPage.value = totalPages - 1; + } + +// Metode untuk mengisi jawaban di semua halaman + Future autoFillAllPages(bool? answer) async { + // Simpan halaman saat ini + final currentPageValue = currentPage.value; + + // Iterasi melalui semua halaman + for (var page = 0; page < totalPages; page++) { + // Pindah ke halaman tersebut + currentPage.value = page; + + // Tunggu rendering selesai + await Future.delayed(const Duration(milliseconds: 10)); + + // Isi jawaban pada halaman ini + if (answer != null) { + // Isi semua jawaban dengan nilai yang sama + for (var q in questionsThisPage) { + setAnswer(q, answer); + } + } else { + // Isi jawaban secara acak + final random = Random(); + for (var q in questionsThisPage) { + setAnswer(q, random.nextBool()); + } + } + } + + // Pindah ke halaman terakhir + currentPage.value = totalPages - 1; + } + +// Metode untuk mengisi jawaban secara acak + void autoFillRandomAnswers() { + final random = Random(); + for (var q in questionsThisPage) { + setAnswer(q, random.nextBool()); + } + } + +// Metode untuk mengisi sebagian jawaban saja (antara 50-80%) + void autoFillPartialAnswers() { + final random = Random(); + + // Acak urutan pertanyaan agar yang dijawab juga acak + final shuffledQuestions = List.from(questionsThisPage) + ..shuffle(); + + // Tentukan berapa banyak yang akan dijawab (50-80%) + final totalQuestions = questionsThisPage.length; + final percentToAnswer = random.nextInt(31) + 50; // 50% - 80% + final questionsToAnswer = (totalQuestions * percentToAnswer / 100).round(); + + // Isi jawaban untuk pertanyaan yang dipilih + for (var i = 0; i < questionsToAnswer; i++) { + if (i < shuffledQuestions.length) { + setAnswer(shuffledQuestions[i], random.nextBool()); + } + } + } + + // Akan menampung data ProgramStudi lengkap (untuk lookup karir di akhir) + final Rx> programList = Rx>([]); + + // Daftar pertanyaan yang sudah di-flatten + final RxList allQuestions = [].obs; + void clearQuestion() { + allQuestions.clear(); + } + + void highlightUnansweredQuestions(ScrollController scrollController) { + // Reset daftar highlight + highlightedQuestionIds.clear(); + + // Kumpulkan semua pertanyaan yang belum dijawab di halaman ini + final List unansweredQuestions = questionsThisPage + .where((q) => + allQuestions.firstWhere((aq) => aq.id == q.id).userAnswer == null) + .toList(); + + if (unansweredQuestions.isEmpty) return; + + // Tambahkan semua ID pertanyaan yang belum dijawab ke daftar highlight + highlightedQuestionIds + .addAll(unansweredQuestions.map((q) => q.id).toList()); + + // Scroll ke pertanyaan pertama yang belum dijawab (jika ada) + if (unansweredQuestions.isNotEmpty && scrollController.hasClients) { + // Cari posisi item yang belum dijawab di ListView + final firstUnansweredIndex = + questionsThisPage.indexOf(unansweredQuestions.first); + + if (firstUnansweredIndex != -1) { + // Hitung perkiraan posisi pertanyaan berdasarkan indeks dan tinggi item + // Asumsi bahwa setiap item pertanyaan memiliki tinggi sekitar 180 piksel + // Anda dapat menyesuaikan nilai ini sesuai dengan tinggi sebenarnya + final estimatedPosition = firstUnansweredIndex * 180.0; + + // Scroll ke posisi dengan offset sedikit di atas (50 piksel) agar lebih jelas + scrollController.animateTo( + estimatedPosition - 50, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } + } + + // Tambahkan getaran (vibration) untuk feedback tambahan + HapticFeedback.mediumImpact(); + + // Hapus highlight setelah beberapa detik + Future.delayed(const Duration(seconds: 5), () { + if (highlightedQuestionIds.isNotEmpty) { + highlightedQuestionIds.clear(); + } + }); + } + + // Paging + final RxInt currentPage = 0.obs; + static const pageSize = 5; + + // Untuk tampilan loading + final RxBool isLoading = true.obs; + final RxString errorMessage = ''.obs; + + @override + void onInit() { + super.onInit(); + loadProgramData(isKerja, majorType); + } + + void nextPage() { + if (currentPage.value < totalPages - 1) { + currentPage.value++; + shouldScrollToTop = true; + } + } + + void prevPage() { + if (currentPage.value > 0) { + currentPage.value--; + shouldScrollToTop = true; + } + } + + @override + void onClose() { + highlightedQuestionIds.clear(); + super.onClose(); + } + + /// Computed property: pertanyaan pada halaman saat ini + List get questionsThisPage { + final totalPages = (allQuestions.length / pageSize).ceil(); + if (currentPage.value >= totalPages) currentPage.value = totalPages - 1; + if (currentPage.value < 0) currentPage.value = 0; + + final startIndex = currentPage.value * pageSize; + final endIndex = + ((currentPage.value + 1) * pageSize).clamp(0, allQuestions.length); + return allQuestions.sublist(startIndex, endIndex); + } + + /// Computed property: total halaman + int get totalPages => (allQuestions.length / pageSize).ceil(); + + /// Computed property: jumlah pertanyaan terjawab + int get answeredCount => + allQuestions.where((q) => q.userAnswer != null).length; + + /// Computed property: total pertanyaan + int get totalCount => allQuestions.length; + + /// Computed property: semua pertanyaan di halaman ini terjawab + bool get allAnsweredThisPage => + questionsThisPage.every((q) => q.userAnswer != null); + Future loadProgramData(bool isKerja, String majorType) async { + currentPage.value = 0; + isLoading.value = true; + errorMessage.value = ''; + + try { + String jsonFile; + + // Tentukan file berdasarkan kombinasi preferensi + if (majorType == "SAINS") { + jsonFile = isKerja + ? 'assets/ipa_sains_kerja.json' + : 'assets/ipa_sains_kuliah.json'; + } else { + // TEKNIK + jsonFile = isKerja + ? 'assets/ipa_teknik_kerja.json' + : 'assets/ipa_teknik_kuliah.json'; + } + + // Baca file JSON + final jsonString = await rootBundle.rootBundle.loadString(jsonFile); + final jsonMap = json.decode(jsonString) as Map; + + // Ubah ke list ProgramStudi + final programs = []; + for (var entry in jsonMap.entries) { + programs.add(ProgramStudi.fromJson(entry.value)); + } + + programList.value = programs; + + // Flatten jadi QuestionItem dengan pengacakan + randomizeQuestions(programs); + + isLoading.value = false; + } catch (e) { + errorMessage.value = e.toString(); + isLoading.value = false; + } + } + + /// Flatten pertanyaan dari programList -> allQuestions (Q1, Q2, dsb) + void flattenQuestions(List programs) { + final all = []; + int counter = 1; + + for (var prog in programs) { + // prog.name = "IPA (Sains Murni) - Kerja" atau "IPA (Sains Murni)" + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + final minatVal = minatEntry.value; // punya pertanyaan, karir, dsb. + + for (int i = 0; i < minatVal.pertanyaan.length; i++) { + final p = minatVal.pertanyaan[i]; + final bobot = extractBobot(p); + final cleaned = cleanPertanyaan(p); + final qId = 'Q$counter'; + counter++; + + // Ambil data RIASEC jika tersedia + List? riasecTypes; + List? riasecBobot; + + // Periksa apakah informasi RIASEC tersedia untuk pertanyaan ini + if (minatVal.riasecType != null && + i < minatVal.riasecType.length && + minatVal.riasecType[i] != null) { + final riasecInfo = minatVal.riasecType[i]; + if (riasecInfo['type'] != null) { + riasecTypes = List.from(riasecInfo['type']); + } + if (riasecInfo['bobot'] != null) { + riasecBobot = List.from(riasecInfo['bobot']); + } + } + + all.add( + QuestionItem( + id: qId, + programName: prog.name, + minatKey: minatKey, + questionText: cleaned, + rawQuestionText: p, + bobot: bobot, + riasecTypes: riasecTypes, + riasecBobot: riasecBobot, + ), + ); + } + } + } + + // Set state + allQuestions.value = all; + } + + void randomizeQuestions(List programs) { + // 1. Kumpulkan pertanyaan berdasarkan minat dan program (stratifikasi) + final questionsByInterest = >{}; + + for (var prog in programs) { + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + final minatVal = minatEntry.value; + final minatIdentifier = '${prog.name}|$minatKey'; // Unique identifier + + questionsByInterest[minatIdentifier] = []; + + for (int i = 0; i < minatVal.pertanyaan.length; i++) { + final p = minatVal.pertanyaan[i]; + final bobot = extractBobot(p); + final cleaned = cleanPertanyaan(p); + + // Ambil data RIASEC jika tersedia + List? riasecTypes; + List? riasecBobot; + + // Periksa apakah informasi RIASEC tersedia untuk pertanyaan ini + if (minatVal.riasecType != null && + i < minatVal.riasecType.length && + minatVal.riasecType[i] != null) { + final riasecInfo = minatVal.riasecType[i]; + if (riasecInfo['type'] != null) { + riasecTypes = List.from(riasecInfo['type']); + } + if (riasecInfo['bobot'] != null) { + riasecBobot = List.from(riasecInfo['bobot']); + } + } + + questionsByInterest[minatIdentifier]!.add( + QuestionItem( + id: '', // Temporary, will assign later + programName: prog.name, + minatKey: minatKey, + questionText: cleaned, + rawQuestionText: p, + bobot: bobot, + riasecTypes: riasecTypes, + riasecBobot: riasecBobot, + ), + ); + } + } + } + + // 2. Acak masing-masing grup minat secara terpisah + questionsByInterest.forEach((minat, questions) { + questions.shuffle(Random()); + }); + + // 3. Ambil pertanyaan secara bergantian dari setiap minat untuk distribusi merata + final finalQuestions = []; + bool continueRotation = true; + int rotationIndex = 0; + + while (continueRotation) { + continueRotation = false; + + // Iterasi melalui semua grup minat + for (var minat in questionsByInterest.keys.toList()..shuffle(Random())) { + final questions = questionsByInterest[minat]!; + + // Jika grup minat ini masih memiliki pertanyaan pada indeks rotasi ini + if (rotationIndex < questions.length) { + finalQuestions.add(questions[rotationIndex]); + continueRotation = true; + } + } + + rotationIndex++; + } + + // 4. Renumber pertanyaan setelah pengacakan + for (int i = 0; i < finalQuestions.length; i++) { + final q = finalQuestions[i]; + finalQuestions[i] = QuestionItem( + id: 'Q${i + 1}', + programName: q.programName, + minatKey: q.minatKey, + questionText: q.questionText, + rawQuestionText: q.rawQuestionText, + bobot: q.bobot, + riasecTypes: q.riasecTypes, + riasecBobot: q.riasecBobot, + ); + } + + // 5. Acak sekali lagi untuk memastikan tidak ada pola yang dapat diprediksi + // (Opsional: hapus baris ini jika Anda ingin mempertahankan pola rotasi yang sempurna) + finalQuestions.shuffle(Random()); + + // 6. Renumber lagi setelah pengacakan terakhir + for (int i = 0; i < finalQuestions.length; i++) { + final q = finalQuestions[i]; + finalQuestions[i] = QuestionItem( + id: 'Q${i + 1}', + programName: q.programName, + minatKey: q.minatKey, + questionText: q.questionText, + rawQuestionText: q.rawQuestionText, + bobot: q.bobot, + riasecTypes: q.riasecTypes, + riasecBobot: q.riasecBobot, + ); + } + + // 7. Set state dengan hasil pengacakan stratifikasi + allQuestions.value = finalQuestions; + + // 8. Analisis distribusi (opsional) + if (true) { + printRiasecDistribution(finalQuestions); + } + } + + void printDistributionSummary(List questions) { + final countByInterest = {}; + + for (var q in questions) { + final key = '${q.programName}|${q.minatKey}'; + countByInterest[key] = (countByInterest[key] ?? 0) + 1; + } + + print('=== Distribusi Pertanyaan ==='); + countByInterest.forEach((minat, count) { + print('$minat: $count pertanyaan'); + }); + + // Analisis pengelompokan + print('\n=== Analisis Urutan ==='); + String prevMinat = ''; + int switchCount = 0; + + for (var q in questions) { + final currentMinat = '${q.programName}|${q.minatKey}'; + if (prevMinat != '' && prevMinat != currentMinat) { + switchCount++; + } + prevMinat = currentMinat; + } + + print( + 'Jumlah pergantian minat: $switchCount dari ${questions.length - 1} kemungkinan pergantian'); + final switchRatio = switchCount / (questions.length - 1); + print('Rasio pergantian: ${(switchRatio * 100).toStringAsFixed(2)}%'); + print('Semakin tinggi rasio, semakin baik pengacakannya'); + } + + void printRiasecDistribution(List questions) { + // Hitung jumlah pertanyaan per tipe RIASEC + final countByRiasecType = { + 'R': 0, + 'I': 0, + 'A': 0, + 'S': 0, + 'E': 0, + 'C': 0 + }; + + int questionsWithRiasec = 0; + + for (var q in questions) { + if (q.riasecTypes != null && q.riasecTypes!.isNotEmpty) { + questionsWithRiasec++; + + for (var type in q.riasecTypes!) { + countByRiasecType[type] = (countByRiasecType[type] ?? 0) + 1; + } + } + } + + print('\n=== Distribusi RIASEC ==='); + print( + 'Pertanyaan dengan informasi RIASEC: $questionsWithRiasec dari ${questions.length} (${(questionsWithRiasec / questions.length * 100).toStringAsFixed(1)}%)'); + + countByRiasecType.forEach((type, count) { + print('Tipe $type: $count pertanyaan'); + }); + + // Hitung rata-rata jumlah tipe RIASEC per pertanyaan + int totalTypes = 0; + for (var q in questions) { + if (q.riasecTypes != null) { + totalTypes += q.riasecTypes!.length; + } + } + + final avgTypesPerQuestion = + questionsWithRiasec > 0 ? totalTypes / questionsWithRiasec : 0; + + print( + 'Rata-rata tipe RIASEC per pertanyaan: ${avgTypesPerQuestion.toStringAsFixed(2)}'); + } + + /// Set jawaban user + void setAnswer(QuestionItem question, bool? answer) { + final index = allQuestions.indexWhere((q) => q.id == question.id); + if (index != -1) { + allQuestions[index].userAnswer = answer; + allQuestions.refresh(); // trigger UI refresh + } + } + + int extractWeight(String questionText) { + final regex = RegExp(r'\[(\d+)\]'); + final match = regex.firstMatch(questionText); + if (match != null && match.groupCount >= 1) { + return int.parse(match.group(1)!); + } + return 5; // Default weight jika tidak ditemukan + } + +// ─────────────────────────────────────────────────────────────── +// 0. Dua saklar log sederhana +// ─────────────────────────────────────────────────────────────── + bool logVerbose = true; // ganti false jika ingin output ringkas + bool logTree = true; // ganti false jika tak perlu pohon + + void _log(String msg, {bool verboseOnly = false}) { + if (!logVerbose && verboseOnly) return; + print(msg); + } + +// ─────────────────────────────────────────────────────────────── +// 1. Forward-chaining engine lengkap +// ─────────────────────────────────────────────────────────────── + RecommendationResult runForwardChaining() { + final workingMemoryList = []; + final workingMemory = {}; + + _log('📌 DAFTAR PERTANYAAN & JAWABAN PENGGUNA:'); + for (var q in allQuestions) { + final ans = q.userAnswer == true ? '✅ Yes' : '❌ No'; + _log( + '\n❓ ID: ${q.id}\n' + '🔹 Program : ${q.programName}\n' + '🔹 Minat : ${q.minatKey}\n' + '🔹 Pertanyaan: ${q.questionText}\n' + '🔹 Bobot : ${q.bobot}\n' + '🔹 Raw Q : ${q.rawQuestionText}\n' + '🔸 Jawaban : $ans', + verboseOnly: true, + ); + } + + // ― Inisialisasi working memory ― + final codeRegex = RegExp(r'([A-Z]+\d+):'); + for (var q in allQuestions) { + final code = codeRegex.firstMatch(q.rawQuestionText)?.group(1) ?? q.id; + final fact = '$code=${q.userAnswer == true ? "Yes" : "No"}'; + workingMemory..add(fact); + workingMemoryList.add(fact); + } + _log('\n🔹 Final Working Memory: $workingMemoryList', verboseOnly: true); + + // ― Hitung bobot total per-minat ― + final minatBobotTotal = {}; + for (var q in allQuestions) { + final key = '${q.programName}|${q.minatKey}'; + minatBobotTotal[key] = (minatBobotTotal[key] ?? 0) + q.bobot; + } + + _log('\n📌 TOTAL BOBOT PER MINAT:'); + minatBobotTotal.forEach((k, v) => _log(' 🔹 $k → $v')); + + // ― Generate rule ― + final rules = []; + final ruleIds = {}; + int ruleCounter = 0; + final minatBobotBenar = {}; + final minatContrib = >{}; + + for (var q in allQuestions) { + final code = codeRegex.firstMatch(q.rawQuestionText)?.group(1) ?? q.id; + final keyMinat = '${q.programName}|${q.minatKey}'; + + final rule = Rule( + ifFacts: ['$code=Yes'], + thenAction: (_) { + minatBobotBenar[keyMinat] = + (minatBobotBenar[keyMinat] ?? 0) + q.bobot; + + minatContrib[keyMinat] ??= []; + minatContrib[keyMinat]!.add( + 'IF ($code=Yes) THEN +${q.bobot} → $keyMinat ' + '[${q.questionText}]', + ); + }, + ); + + final id = 'R${(ruleCounter++).toString().padLeft(3, '0')}'; + ruleIds[rule] = id; + rules.add(rule); + + _log('🔧 Generated $id IF $code=Yes THEN +${q.bobot} → $keyMinat', + verboseOnly: true); + } + + // ― Forward-chaining eksekusi ― + _log('\n🚀 Starting Forward Chaining...\n'); + final firedRules = {}; + bool firedSomething = true; + + while (firedSomething) { + firedSomething = false; + final firedThisRound = []; + + for (var r in rules) { + if (firedRules.contains(r)) continue; + if (r.ifFacts.every(workingMemory.contains)) { + r.thenAction(workingMemory); + firedRules.add(r); + firedThisRound.add(ruleIds[r]!); + firedSomething = true; + } + } + if (firedThisRound.isNotEmpty) { + _log('🔥 Fired: ${firedThisRound.join(', ')}', verboseOnly: true); + } + } + + // ― Hitung skor minat ― + final minatScores = {}; + _log('\n📌 PERHITUNGAN SKOR MINAT:'); + minatBobotTotal.forEach((key, total) { + final benar = minatBobotBenar[key] ?? 0; + final pct = total == 0 ? 0 : ((benar / total) * 100).round(); + minatScores[key] = pct; + _log('➡️ $key: ($benar / $total) * 100 = $pct%'); + }); + + // ― Urutkan & ambil top-3 ― + final sorted = minatScores.entries.toList() + ..sort((a, b) { + final c = b.value.compareTo(a.value); + return c != 0 + ? c + : (minatBobotBenar[b.key] ?? 0) + .compareTo(minatBobotBenar[a.key] ?? 0); + }); + + _log('\n📌 Sorted Minat Scores: $sorted', verboseOnly: true); + + final recommendations = []; + for (var i = 0; i < sorted.take(3).length; i++) { + final key = sorted[i].key; + final score = sorted[i].value; + final parts = key.split('|'); + if (parts.length != 2) continue; + + final progName = parts[0]; + final minatKey = parts[1]; + + final prog = programList.value.firstWhere((p) => p.name == progName, + orElse: () => ProgramStudi.empty()); + final mObj = prog.minat[minatKey]; + if (mObj == null) continue; + + recommendations.add( + RecommendationItem( + title: key, + score: score, + careers: mObj.karir, + majors: mObj.jurusanTerkait, + rules: minatContrib[key] ?? [], + index: i, + recommendedCourses: mObj.rekomendasi_kursus, + recommendedUniversities: mObj.universitas_rekomendasi, + ), + ); + } + + // ― Cetak pohon keputusan jika diminta ― + if (logTree) _printDecisionTree(minatScores, minatContrib); + _printTreeAsJson( + workingMemoryList, + minatScores, + minatContrib, + recommendations, + ); + + return RecommendationResult( + workingMemory: workingMemoryList, + recommendations: recommendations, + ); + } + + void _printTreeAsJson( + List workingMemoryList, + Map minatScores, + Map> minatContrib, + List recommendations, + ) { + final treeJson = { + 'generatedAt': DateTime.now().toIso8601String(), + 'workingMemory': workingMemoryList, + 'minat': minatScores.entries.map((e) { + final parts = e.key.split('|'); + final progName = parts[0]; + final minatKey = parts[1]; + + final prog = programList.value.firstWhere((p) => p.name == progName, + orElse: () => ProgramStudi.empty()); + final mObj = prog.minat[minatKey]; + + return { + 'key': e.key, + 'score': e.value, + 'rules': minatContrib[e.key] ?? [], + 'careers': mObj?.karir ?? [], + 'majors': mObj?.jurusanTerkait ?? [], + }; + }).toList(), + 'topRecommendations': recommendations + .map((r) => { + 'title': r.title, + 'score': r.score, + 'careers': r.careers, + 'majors': r.majors, + }) + .toList(), + }; + + final pretty = const JsonEncoder.withIndent(' ').convert(treeJson); + dev.log('\n🔍 JSON Pohon Keputusan ↓↓↓\n$pretty\n'); + } + +// ─────────────────────────────────────────────────────────────── +// 2. Cetak pohon keputusan (opsional) +// ─────────────────────────────────────────────────────────────── + void _printDecisionTree( + Map scores, Map> contrib) { + _log('\n🌳 POHON KEPUTUSAN'); + final ordered = scores.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + for (var e in ordered) { + final key = e.key; + final score = e.value; + _log('├─ $key [$score%]'); + + // ── 1. Baris per-rule (IF … THEN …) ──────────────── + for (var line in contrib[key] ?? const []) { + _log('│ ├─ $line'); + } + + // ── 2. Tambahkan “hasil akhir”: Karier & Jurusan ─── + final parts = key.split('|'); + if (parts.length == 2) { + final progName = parts[0]; + final minatKey = parts[1]; + + final prog = programList.value.firstWhere((p) => p.name == progName, + orElse: () => ProgramStudi.empty()); + final mObj = prog.minat[minatKey]; + + if (mObj != null) { + final careers = mObj.karir.join(', '); + final majors = mObj.jurusanTerkait.join(', '); + + _log('│ │'); + _log('│ ├─ ⇒ Karier : $careers'); + _log('│ └─ ⇒ Jurusan: $majors'); + } + } + } + _log('└─ EOF\n'); + } + + Future saveResultsToFirestore(RecommendationResult results) async { + try { + // Get current user + final user = FirebaseAuth.instance.currentUser; + if (user == null) { + Get.snackbar( + 'Error', + 'Kamu perlu login untuk menyimpan hasil', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + return; + } + + // Get schoolId from SharedPreferences + final prefs = await SharedPreferences.getInstance(); + String? schoolId = prefs.getString('school_id'); + + // If schoolId is not found in SharedPreferences, try to find it by searching all schools + if (schoolId == null || schoolId.isEmpty) { + schoolId = await _findStudentSchoolId(user.uid); + + if (schoolId == null || schoolId.isEmpty) { + Get.snackbar( + 'Error', + 'Tidak dapat menemukan sekolah terkait', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + return; + } + + // Save school ID for future use + await prefs.setString('school_id', schoolId); + } + + // Dapatkan informasi kelas siswa dari Firestore dengan struktur baru + String? studentClass; + try { + DocumentSnapshot studentDoc = await FirebaseFirestore.instance + .collection('schools') + .doc(schoolId) + .collection('students') + .doc(user.uid) + .get(); + + if (studentDoc.exists) { + final data = studentDoc.data() as Map; + studentClass = data['class'] as String?; + } + } catch (e) { + print('Error fetching student class: $e'); + } + + // Prepare data to save + final timestamp = DateTime.now(); + final userAnswers = allQuestions + .map((q) => { + 'questionId': q.id, + 'question': q.questionText, + 'rawQuestion': q.rawQuestionText, + 'answer': q.userAnswer, + 'programName': q.programName, + 'minatKey': q.minatKey, + 'bobot': q.bobot, + // Tambahkan data RIASEC jika ada + 'riasecTypes': q.riasecTypes ?? [], + 'riasecBobot': q.riasecBobot ?? [], + }) + .toList(); + + // Format recommendations for Firestore + final recommendationsData = results.recommendations + .map((rec) => { + 'title': rec.title, + 'score': rec.score, + 'careers': rec.careers, + 'majors': rec.majors, + 'rules': rec.rules, + 'index': rec.index, + 'recommendedCourses': rec.recommendedCourses ?? [], + 'recommendedUniversities': rec.recommendedUniversities ?? [], + // Tambahkan data RIASEC + 'riasecCompatibility': rec.riasecCompatibility ?? 0.0, + 'matchingRiasecCareers': rec.matchingRiasecCareers ?? [], + }) + .toList(); + + // Format RIASEC profile data jika ada + Map riasecProfileData = {}; + if (results.riasecProfile != null) { + riasecProfileData = { + 'scores': results.riasecProfile!.scores, + 'dominantTypes': results.riasecProfile!.dominantTypes, + 'code': results.riasecProfile!.code, + 'matchingCareers': results.riasecProfile!.matchingCareers, + }; + } + + // Prepare recommendation data + Map recommendationData = { + 'userId': user.uid, + 'userEmail': user.email, + 'userName': user.displayName, + 'schoolId': schoolId, // Add schoolId to data + 'studentClass': studentClass, + 'timestamp': FieldValue.serverTimestamp(), + 'formattedTimestamp': timestamp.toString(), + 'isKerja': isKerja, + 'questionMode': isKerja ? 'Rekomendasi Karir' : 'Rekomendasi Kuliah', + 'userAnswers': userAnswers, + 'workingMemory': results.workingMemory, + 'recommendations': recommendationsData, + 'totalQuestions': totalCount, + 'answeredQuestions': answeredCount, + 'riasecProfile': riasecProfileData, + }; + + // Save to the school's recommendation_history subcollection with user ID as document ID + await FirebaseFirestore.instance + .collection('schools') + .doc(schoolId) + .collection('recommendation_history') + .doc(user.uid) + .set(recommendationData, SetOptions(merge: true)); + + Get.snackbar( + 'Berhasil', + 'Hasil rekomendasi telah disimpan', + backgroundColor: Colors.green.shade100, + colorText: Colors.green.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } catch (e) { + print('Error saving results: $e'); + Get.snackbar( + 'Error', + 'Gagal menyimpan hasil: ${e.toString()}', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } + } + +// Helper function to find a student's school ID + Future _findStudentSchoolId(String userId) async { + try { + final schoolsSnapshot = + await FirebaseFirestore.instance.collection('schools').get(); + + for (var schoolDoc in schoolsSnapshot.docs) { + final studentDoc = + await schoolDoc.reference.collection('students').doc(userId).get(); + + if (studentDoc.exists) { + return schoolDoc.id; + } + } + return null; + } catch (e) { + print('Error finding student school ID: $e'); + return null; + } + } + + /// Still needed for legacy reasons - converts the RecommendationResult to a string + String runForwardChainingAsString() { + final result = runForwardChaining(); + + // Convert to string format (legacy format) + String message = 'HASIL FORWARD CHAINING:\n\n'; + message += 'Working Memory (fakta): ${result.workingMemory.join(', ')}\n\n'; + + // Tambahkan informasi RIASEC profile jika ada + if (result.riasecProfile != null) { + message += 'PROFIL RIASEC:\n'; + message += 'Kode RIASEC: ${result.riasecProfile!.code}\n'; + message += + 'Tipe Dominan: ${result.riasecProfile!.dominantTypes.join(", ")}\n'; + + message += 'Skor per Tipe:\n'; + result.riasecProfile!.scores.forEach((type, score) { + message += ' - $type: $score\n'; + }); + + if (result.riasecProfile!.matchingCareers.isNotEmpty) { + message += 'Karir yang Cocok dengan Profil RIASEC Anda:\n'; + for (var career in result.riasecProfile!.matchingCareers) { + message += ' - $career\n'; + } + } + message += '\n'; + } + + message += 'Top 3 Rekomendasi:\n'; + for (int i = 0; i < result.recommendations.length; i++) { + final rec = result.recommendations[i]; + message += '${i + 1}. ${rec.title} (Skor: ${rec.score})\n'; + + // Rules + if (rec.rules.isNotEmpty) { + message += ' RULES YANG:\n'; + for (var rule in rec.rules) { + message += ' - $rule\n'; + } + } + + // Careers + if (rec.careers.isNotEmpty) { + message += ' Karir:\n'; + for (var career in rec.careers) { + message += ' - $career\n'; + } + } else { + message += ' Karir: (Tidak ada data)\n'; + } + + // RIASEC matching careers (jika ada) + if (rec.matchingRiasecCareers != null && + rec.matchingRiasecCareers!.isNotEmpty) { + message += + ' Karir yang Cocok dengan Profil RIASEC Anda (${rec.riasecCompatibility?.toStringAsFixed(1)}% kecocokan):\n'; + for (var career in rec.matchingRiasecCareers!) { + message += ' - $career\n'; + } + } + + // Majors + if (rec.majors.isNotEmpty) { + message += ' Jurusan Terkait:\n'; + for (var major in rec.majors) { + message += ' - $major\n'; + } + } + + // Recommended Courses (jika ada) + if (rec.recommendedCourses != null && + rec.recommendedCourses!.isNotEmpty) { + message += ' Kursus yang Direkomendasikan:\n'; + for (var course in rec.recommendedCourses!) { + message += ' - $course\n'; + } + } + + // Recommended Universities (jika ada) + if (rec.recommendedUniversities != null && + rec.recommendedUniversities!.isNotEmpty) { + message += ' Universitas yang Direkomendasikan:\n'; + for (var university in rec.recommendedUniversities!) { + message += ' - $university\n'; + } + } + + message += '\n'; + } + + return message; + } +} diff --git a/lib/app/views/student/feature/quiz/page_feedback_evaluation.dart b/lib/app/views/student/feature/quiz/page_feedback_evaluation.dart new file mode 100644 index 0000000..b19620c --- /dev/null +++ b/lib/app/views/student/feature/quiz/page_feedback_evaluation.dart @@ -0,0 +1,1067 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/controller/question_controller.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/view/page_select_economy.dart'; +import 'package:forward_chaining_man_app/app/views/student/model/data_student.dart'; +import 'package:get/get.dart'; +import 'package:flutter/services.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +// Import models +// Pastikan path import disesuaikan dengan struktur proyek Anda +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get/get.dart'; +import 'package:flutter/services.dart'; + +// Import models +// Pastikan path import disesuaikan dengan struktur proyek Anda + +/// Halaman untuk mengumpulkan feedback dari pengguna untuk evaluasi akurasi sistem +class FeedbackEvaluationPage extends StatefulWidget { + final RecommendationResult recommendationResults; + final String majorType; // "SAINS" atau "TEKNIK" + final bool isKerja; // true untuk karir, false untuk pendidikan + + const FeedbackEvaluationPage({ + Key? key, + required this.recommendationResults, + required this.majorType, + required this.isKerja, + }) : super(key: key); + + @override + State createState() => _FeedbackEvaluationPageState(); +} + +class _FeedbackEvaluationPageState extends State { + String? selectedMinat; + String? confidenceLevel; + final commentController = TextEditingController(); + final _formKey = GlobalKey(); + bool _isSubmitting = false; + + // Tambahkan variabel untuk skala Likert tambahan + final Map likertRatings = { + 'relevance': '3', // Relevansi pertanyaan + 'difficulty': '3', // Tingkat kesulitan kuesioner + 'interface': '3', // Kemudahan antarmuka + 'satisfaction': '3', // Kepuasan keseluruhan + }; + + // Teks untuk setiap kategori penilaian + final Map likertLabels = { + 'relevance': 'Relevansi pertanyaan dengan minat saya', + 'difficulty': 'Tingkat kesulitan dalam menjawab kuesioner', + 'interface': 'Kemudahan penggunaan aplikasi', + 'satisfaction': 'Kepuasan keseluruhan dengan rekomendasi', + }; + + // Daftar minat berdasarkan jurusan yang dipilih user + late List availableMinat = []; + + @override + void initState() { + super.initState(); + _initializeMinatList(); + } + + /// Inisialisasi daftar minat berdasarkan majorType yang dipilih + void _initializeMinatList() { + // Mulai dengan set kosong untuk menghindari duplikasi + Set minatSet = {}; + + try { + // Coba dapatkan QuestionController untuk mengakses data program studi + final questionController = Get.find(); + + // Tambahkan SEMUA minat yang sesuai dengan majorType (SAINS atau TEKNIK) + for (var program in questionController.programList.value) { + // Filter berdasarkan nama program yang mengandung majorType + if (program.name + .toLowerCase() + .contains(widget.majorType.toLowerCase())) { + // Tambahkan semua minat dari program ini + for (var minatEntry in program.minat.entries) { + final minatKey = '${program.name}|${minatEntry.key}'; + minatSet.add(minatKey); + print('Menambahkan minat: $minatKey'); + } + } + } + + // Jika tidak ada minat yang ditemukan dari controller, gunakan alternatif dari data statis + if (minatSet.isEmpty) { + // Gunakan daftar statis berdasarkan majorType + if (widget.majorType.toLowerCase().contains('sains')) { + // Daftar minat IPA Sains + minatSet.addAll([ + 'IPA (Sains Murni)|Biologi', + 'IPA (Sains Murni)|Kimia', + 'IPA (Sains Murni)|Fisika', + 'IPA (Sains Murni)|Matematika', + 'IPA (Sains Murni)|Kesehatan', + 'IPA (Sains Murni)|Farmasi', + 'IPA (Sains Murni)|Kedokteran', + 'IPA (Sains Murni)|Lingkungan', + ]); + } else if (widget.majorType.toLowerCase().contains('teknik')) { + // Daftar minat IPA Teknik + minatSet.addAll([ + 'IPA (Teknik)|Teknik Sipil', + 'IPA (Teknik)|Teknik Elektro', + 'IPA (Teknik)|Teknik Mesin', + 'IPA (Teknik)|Informatika', + 'IPA (Teknik)|Arsitektur', + 'IPA (Teknik)|Robotik', + 'IPA (Teknik)|Telekomunikasi', + 'IPA (Teknik)|Industri', + ]); + } + } + } catch (e) { + print('Error saat mengambil minat dari controller: $e'); + + // Jika error, gunakan data dari rekomendasi sebagai fallback terakhir + for (var rec in widget.recommendationResults.recommendations) { + minatSet.add(rec.title); + } + } + + // Jika masih kosong, tambahkan pesan error + if (minatSet.isEmpty) { + minatSet.add('Error|Tidak ada data minat'); + } + + // Konversi ke list dan urutkan + availableMinat = minatSet.toList(); + availableMinat.sort(); + + // Debug info + print('Total minat tersedia: ${availableMinat.length}'); + print('Daftar minat: $availableMinat'); + } + + /// Mengambil label yang lebih mudah dibaca dari key minat + String getReadableMinatLabel(String minatKey) { + final parts = minatKey.split('|'); + if (parts.length != 2) return minatKey; + + String program = parts[0].trim(); + String minat = parts[1].trim(); + + // Hapus prefix IPA jika ada + if (program.startsWith('IPA')) { + program = program.replaceFirst('IPA', '').trim(); + if (program.startsWith('(') && program.endsWith(')')) { + program = program.substring(1, program.length - 1).trim(); + } + } + + return "$program - $minat"; + } + + /// Mengolah key minat dari label yang dibaca + String getMinatKeyFromLabel(String label, List availableKeys) { + // Cari key yang cocok dengan label + for (var key in availableKeys) { + if (getReadableMinatLabel(key) == label) { + return key; + } + } + return label; // Fallback jika tidak ditemukan + } + + /// Menyimpan feedback dan menampilkan hasil rekomendasi + Future _submitFeedbackAndContinue() async { + if (_formKey.currentState?.validate() != true) { + // Tampilkan feedback visual untuk form yang belum lengkap + HapticFeedback.mediumImpact(); + return; + } + + setState(() { + _isSubmitting = true; + }); + + try { + final user = FirebaseAuth.instance.currentUser; + if (user == null) { + Get.snackbar( + 'Error', + 'Anda perlu login untuk melanjutkan', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + ); + return; + } + + // Dapatkan school_id dari SharedPreferences + final prefs = await SharedPreferences.getInstance(); + String? schoolId = prefs.getString('school_id'); + + // Jika tidak ada, coba cari + if (schoolId == null || schoolId.isEmpty) { + schoolId = await _findStudentSchoolId(user.uid); + if (schoolId != null) { + await prefs.setString('school_id', schoolId); + } + } + + // Buat dokumen feedback untuk analisis akurasi + final feedbackData = { + 'userId': user.uid, + 'userEmail': user.email, + 'userName': user.displayName, + 'schoolId': schoolId, + 'timestamp': FieldValue.serverTimestamp(), + 'majorType': widget.majorType, + 'isKerja': widget.isKerja, + 'selectedMinat': selectedMinat, + 'confidenceLevel': confidenceLevel, + 'comment': commentController.text, + + // Data untuk evaluasi akurasi + 'systemRecommendations': widget.recommendationResults.recommendations + .map((r) => r.title) + .toList(), + 'topRecommendation': + widget.recommendationResults.recommendations.isNotEmpty + ? widget.recommendationResults.recommendations[0].title + : null, + 'secondRecommendation': + widget.recommendationResults.recommendations.length > 1 + ? widget.recommendationResults.recommendations[1].title + : null, + 'thirdRecommendation': + widget.recommendationResults.recommendations.length > 2 + ? widget.recommendationResults.recommendations[2].title + : null, + + // Evaluation metrics yang akan dihitung + 'isCorrectTopPrediction': selectedMinat == + (widget.recommendationResults.recommendations.isNotEmpty + ? widget.recommendationResults.recommendations[0].title + : null), + 'isInTop3Predictions': widget.recommendationResults.recommendations + .any((r) => r.title == selectedMinat), + + // Likert scale ratings untuk berbagai aspek + 'confidenceRating': confidenceLevel, + 'likertRatings': likertRatings, + }; + + // Simpan ke Firestore dalam koleksi terpisah untuk evaluasi + String docId = '${user.uid}_${DateTime.now().millisecondsSinceEpoch}'; + + if (schoolId != null && schoolId.isNotEmpty) { + // Simpan di bawah sekolah jika schoolId ada + await FirebaseFirestore.instance + .collection('schools') + .doc(schoolId) + .collection('accuracy_evaluations') + .doc(docId) + .set(feedbackData); + } else { + // Simpan di koleksi global jika tidak ada schoolId + await FirebaseFirestore.instance + .collection('accuracy_evaluations') + .doc(docId) + .set(feedbackData); + } + + Get.snackbar( + 'Berhasil', + 'Terima kasih atas feedback Anda', + backgroundColor: Colors.green.shade100, + colorText: Colors.green.shade800, + ); + + // Tampilkan hasil rekomendasi + showRecommendationResultsGetx(widget.recommendationResults); + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal menyimpan feedback: ${e.toString()}', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + ); + } finally { + setState(() { + _isSubmitting = false; + }); + } + } + + /// Helper function untuk mencari schoolId berdasarkan userId + Future _findStudentSchoolId(String userId) async { + try { + final schoolsSnapshot = + await FirebaseFirestore.instance.collection('schools').get(); + + for (var schoolDoc in schoolsSnapshot.docs) { + final studentDoc = + await schoolDoc.reference.collection('students').doc(userId).get(); + + if (studentDoc.exists) { + return schoolDoc.id; + } + } + return null; + } catch (e) { + print('Error finding school ID: $e'); + return null; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: SafeArea( + top: true, + bottom: false, + child: Column( + children: [ + // Header + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: GestureDetector( + onTap: () => Get.back(), + child: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 16), + const Expanded( + child: Text( + 'Evaluasi Sistem', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.amber.shade600, + borderRadius: BorderRadius.circular(50), + ), + child: Text( + widget.isKerja ? 'Karir' : 'Kuliah', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 13, + ), + ), + ), + ], + ), + ), + + // Content + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 24), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(30), + ), + ), + child: ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(30), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Ikon dan judul + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.amber.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.psychology_outlined, + size: 24, + color: Colors.amber.shade800, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Bantu Kami Evaluasi Sistem', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + 'Jawablah sesuai dengan pendapat Anda', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 30), + + // Pertanyaan 1: Bidang Minat + Text( + 'Menurut Anda, bidang minat apa yang paling sesuai dengan diri Anda?', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 8), + Text( + 'Pilih salah satu bidang minat yang menurut Anda paling mencerminkan diri Anda', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + + // Daftar Minat + Container( + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: DropdownButtonFormField( + value: selectedMinat, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Silakan pilih bidang minat'; + } + return null; + }, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: InputBorder.none, + hintText: 'Pilih Bidang Minat', + hintStyle: TextStyle( + color: Colors.grey.shade500, + ), + ), + icon: Icon( + Icons.arrow_drop_down_circle, + color: Colors.blue.shade700, + ), + isExpanded: + true, // Pastikan dropdown bisa melebar penuh + menuMaxHeight: + 300, // Tinggi maksimum menu dropdown + dropdownColor: + Colors.white, // Warna background dropdown + items: availableMinat + .map((minat) => DropdownMenuItem( + value: minat, + child: Text( + getReadableMinatLabel(minat), + style: const TextStyle( + fontSize: 15, + ), + overflow: TextOverflow.ellipsis, + ), + )) + .toList(), + onChanged: (value) { + if (value != null) { + setState(() { + selectedMinat = value; + }); + } + }, + ), + ), + + const SizedBox(height: 24), + + // Pertanyaan 2: Tingkat Keyakinan (Skala Likert) + Text( + 'Seberapa yakin Anda dengan pilihan tersebut?', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 16), + + // Skala Likert untuk tingkat keyakinan + buildConfidenceLevelOptions(), + + const SizedBox(height: 30), + + // Penilaian Tambahan dengan Skala Likert + Text( + 'Berikan Penilaian Sistem', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 8), + Text( + 'Berikan penilaian untuk aspek-aspek berikut', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + + // Likert scales untuk berbagai aspek + ...likertLabels.entries.map((entry) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + entry.value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + buildLikertScale(entry.key), + const SizedBox(height: 16), + ], + ); + }).toList(), + + const SizedBox(height: 10), + + // Pertanyaan 3: Komentar Tambahan + Text( + 'Ada komentar tambahan? (Opsional)', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 16), + + // Text area untuk komentar + Container( + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: TextFormField( + controller: commentController, + decoration: InputDecoration( + hintText: 'Tulis komentar Anda di sini...', + hintStyle: TextStyle( + color: Colors.grey.shade500, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + border: InputBorder.none, + ), + maxLines: 3, + ), + ), + + const SizedBox(height: 40), + + // Tombol Submit + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: + Colors.blue.shade300.withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, 6), + ), + ], + ), + child: ElevatedButton( + onPressed: _isSubmitting + ? null + : _submitFeedbackAndContinue, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + padding: + const EdgeInsets.symmetric(vertical: 16), + minimumSize: const Size(double.infinity, 54), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + child: _isSubmitting + ? Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 3, + color: Colors.white, + ), + ), + const SizedBox(width: 12), + const Text( + 'Menyimpan...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Text( + 'Lihat Hasil Rekomendasi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward, + size: 20), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // // Skip button (with warning) + // TextButton( + // onPressed: () { + // // Show confirmation dialog + // showDialog( + // context: context, + // builder: (context) => AlertDialog( + // title: const Text('Lewati Evaluasi?'), + // content: const Text( + // 'Feedback Anda sangat penting untuk meningkatkan akurasi sistem. Yakin ingin melewati tahap ini?'), + // actions: [ + // TextButton( + // onPressed: () => Navigator.pop(context), + // child: const Text('KEMBALI'), + // ), + // ElevatedButton( + // onPressed: () { + // Navigator.pop(context); + // showRecommendationResultsGetx( + // widget.recommendationResults); + // }, + // style: ElevatedButton.styleFrom( + // backgroundColor: Colors.red.shade600, + // ), + // child: const Text('YA, LEWATI'), + // ), + // ], + // ), + // ); + // }, + // style: TextButton.styleFrom( + // foregroundColor: Colors.grey.shade700, + // minimumSize: const Size(double.infinity, 44), + // ), + // child: const Text( + // 'Lewati Evaluasi', + // style: TextStyle( + // decoration: TextDecoration.underline, + // ), + // ), + // ), + + const SizedBox(height: 40), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + /// Widget untuk skala Likert (multiple scales) + Widget buildLikertScale(String key) { + return Container( + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: List.generate(5, (index) { + final value = (index + 1).toString(); + final isSelected = likertRatings[key] == value; + + return InkWell( + onTap: () { + setState(() { + likertRatings[key] = value; + }); + HapticFeedback.selectionClick(); + }, + borderRadius: BorderRadius.circular(20), + child: Container( + width: 42, + height: 42, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected + ? getConfidenceColor(value) + : Colors.grey.shade200, + border: Border.all( + color: isSelected + ? getConfidenceColor(value).withOpacity(0.8) + : Colors.grey.shade400, + width: 2, + ), + ), + child: Center( + child: Text( + value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isSelected ? Colors.white : Colors.grey.shade700, + ), + ), + ), + ), + ); + }), + ), + ); + } + + /// Widget untuk skala Likert tingkat keyakinan + Widget buildConfidenceLevelOptions() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Skala 1-5:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + 'Sangat Tidak Yakin', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ), + Expanded( + child: Text( + 'Sangat Yakin', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + textAlign: TextAlign.right, + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + buildLikertOption('1', '1'), + buildLikertOption('2', '2'), + buildLikertOption('3', '3'), + buildLikertOption('4', '4'), + buildLikertOption('5', '5'), + ], + ), + ), + // Keterangan skala + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(5, (index) { + return SizedBox( + width: 50, + child: Text( + '${index + 1}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + textAlign: index == 0 + ? TextAlign.left + : (index == 4 ? TextAlign.right : TextAlign.center), + ), + ); + }), + ), + const SizedBox(height: 10), + // Deskripsi Skala + buildLikertDescription(), + ], + ); + } + + /// Widget untuk tiap opsi skala Likert + Widget buildLikertOption(String value, String label) { + final isSelected = confidenceLevel == value; + + return InkWell( + onTap: () { + setState(() { + confidenceLevel = value; + }); + // Tambahkan haptic feedback untuk respon sentuhan + HapticFeedback.selectionClick(); + }, + borderRadius: BorderRadius.circular(24), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? getConfidenceColor(value) : Colors.grey.shade200, + border: Border.all( + color: isSelected + ? getConfidenceColor(value).withOpacity(0.8) + : Colors.grey.shade400, + width: 2, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: getConfidenceColor(value).withOpacity(0.3), + blurRadius: 8, + spreadRadius: 2, + ) + ] + : null, + ), + child: Center( + child: Text( + label, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isSelected ? Colors.white : Colors.grey.shade700, + ), + ), + ), + ), + ); + } + + /// Mendapatkan warna berdasarkan level keyakinan + Color getConfidenceColor(String value) { + switch (value) { + case '1': + return Colors.red.shade600; + case '2': + return Colors.orange.shade600; + case '3': + return Colors.amber.shade600; + case '4': + return Colors.lightGreen.shade600; + case '5': + return Colors.green.shade600; + default: + return Colors.blue.shade600; + } + } + + /// Deskripsi untuk setiap skala Likert + Widget buildLikertDescription() { + return Container( + margin: const EdgeInsets.only(top: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Keterangan Skala:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 8), + buildLikertDescriptionItem( + '1', 'Sangat Tidak Yakin', Colors.red.shade600), + buildLikertDescriptionItem( + '2', 'Tidak Yakin', Colors.orange.shade600), + buildLikertDescriptionItem('3', 'Netral', Colors.amber.shade600), + buildLikertDescriptionItem('4', 'Yakin', Colors.lightGreen.shade600), + buildLikertDescriptionItem( + '5', 'Sangat Yakin', Colors.green.shade600), + ], + ), + ); + } + + /// Item deskripsi untuk setiap skala + Widget buildLikertDescriptionItem( + String scale, String description, Color color) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + child: Center( + child: Text( + scale, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(width: 12), + Text( + description, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ], + ), + ); + } +} + +// Tambahkan juga modifikasi di QuestionPage untuk mengarahkan ke FeedbackEvaluationPage + +/// Helper function untuk menampilkan FeedbackEvaluationPage dari QuestionPage +/// Tambahkan ini ke QuestionPage atau buatkan extension +void showFeedbackEvaluationPage( + RecommendationResult results, bool isKerja, String majorType) { + Get.to(() => FeedbackEvaluationPage( + recommendationResults: results, + isKerja: isKerja, + majorType: majorType, + )); +} + +/// Modifikasi fungsi di QuestionPage untuk menggunakan FeedbackEvaluationPage +/// +/// // Di QuestionPage, ganti +/// final results = controller.runForwardChaining(); +/// controller.saveResultsToFirestore(results).then((_) { +/// showRecommendationResultsGetx(results); +/// }); +/// +/// // Menjadi +/// final results = controller.runForwardChaining(); +/// controller.saveResultsToFirestore(results).then((_) { +/// showFeedbackEvaluationPage(results, controller.isKerja, controller.majorType); +/// }); \ No newline at end of file diff --git a/lib/app/views/student/feature/quiz/view/page_question.dart b/lib/app/views/student/feature/quiz/view/page_question.dart new file mode 100644 index 0000000..82ef91b --- /dev/null +++ b/lib/app/views/student/feature/quiz/view/page_question.dart @@ -0,0 +1,1628 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/app/views/about/page_about.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/controller/question_controller.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/page_feedback_evaluation.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/view/page_select_economy.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/view/widget/shimmer.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:firebase_core/firebase_core.dart'; +import 'package:get/get_connect/http/src/utils/utils.dart'; +import 'package:intl/intl.dart' as intl; + +import 'package:url_launcher/url_launcher.dart'; + +class PatternPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.blue.withOpacity(0.1) + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke; + + final random = math.Random(42); // Fixed seed for consistent pattern + + // Draw simple patterns + for (int i = 0; i < 10; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + + if (i % 3 == 0) { + // Draw circles + canvas.drawCircle( + Offset(x, y), + 10 + random.nextDouble() * 20, + paint, + ); + } else if (i % 3 == 1) { + // Draw squares + canvas.drawRect( + Rect.fromCenter( + center: Offset(x, y), + width: 20 + random.nextDouble() * 20, + height: 20 + random.nextDouble() * 20, + ), + paint, + ); + } else { + // Draw stars + _drawStar(canvas, Offset(x, y), 5 + random.nextDouble() * 10, paint); + } + } + } + + void _drawStar(Canvas canvas, Offset center, double radius, Paint paint) { + const int numPoints = 5; + final path = Path(); + + for (int i = 0; i < numPoints * 2; i++) { + final angle = i * math.pi / numPoints; + final r = i.isEven ? radius : radius * 0.4; + final x = center.dx + r * math.cos(angle); + final y = center.dy + r * math.sin(angle); + + if (i == 0) { + path.moveTo(x, y); + } else { + path.lineTo(x, y); + } + } + path.close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +// Animated science icons that float around +// Simple animated icon for background +class AnimatedBackgroundIcon extends StatefulWidget { + final IconData icon; + final double size; + final Color color; + final Offset position; + + const AnimatedBackgroundIcon({ + Key? key, + required this.icon, + required this.size, + required this.color, + required this.position, + }) : super(key: key); + + @override + State createState() => _AnimatedBackgroundIconState(); +} + +class _AnimatedBackgroundIconState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + + final random = math.Random(); + + _controller = AnimationController( + duration: Duration(milliseconds: 2000 + random.nextInt(2000)), + vsync: this, + )..repeat(reverse: true); + + _animation = Tween( + begin: const Offset(0.0, 0.0), + end: Offset( + (random.nextDouble() - 0.5) * 0.4, + (random.nextDouble() - 0.5) * 0.4, + ), + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Positioned( + left: widget.position.dx, + top: widget.position.dy, + child: SlideTransition( + position: _animation, + child: Icon( + widget.icon, + size: widget.size, + color: widget.color, + ), + ), + ); + } +} + +// Background with multiple animated icons +class FloatingIconsBackground extends StatelessWidget { + const FloatingIconsBackground({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final random = math.Random(); + final icons = [ + Icons.science, + Icons.psychology, + Icons.sports_soccer, + Icons.computer, + Icons.brush, + Icons.business, + Icons.music_note, + Icons.health_and_safety, + ]; + + // Generate backgrounds icons with random positions, sizes and colors + final backgroundIcons = List.generate( + 8, + (i) => AnimatedBackgroundIcon( + icon: icons[i % icons.length], + size: 12.0 + random.nextDouble() * 12, + color: Color.fromRGBO( + 50 + random.nextInt(150), + 50 + random.nextInt(150), + 150 + random.nextInt(100), + 0.3, + ), + position: Offset( + random.nextDouble() * 300, + random.nextDouble() * 300, + ), + ), + ); + + return SizedBox( + width: double.infinity, + height: double.infinity, + child: Stack( + children: backgroundIcons, + ), + ); + } +} + +class PulsingWarningIcon extends StatefulWidget { + const PulsingWarningIcon({Key? key}) : super(key: key); + + @override + State createState() => _PulsingWarningIconState(); +} + +class _PulsingWarningIconState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + + _animation = Tween(begin: 1.0, end: 1.2).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ScaleTransition( + scale: _animation, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: const Icon( + Icons.warning_rounded, + color: Colors.white, + size: 36, + ), + ), + ); + } +} + +// Dialog entrance animation +class FadeScaleTransition extends StatelessWidget { + final Widget child; + final Animation animation; + + const FadeScaleTransition({ + Key? key, + required this.child, + required this.animation, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animation, + builder: (context, child) { + return Transform.scale( + scale: Curves.easeOutBack.transform(animation.value), + child: Opacity( + opacity: animation.value, + child: child, + ), + ); + }, + child: child, + ); + } +} + +// Custom painter for science-themed background +class SciencePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.blue.withOpacity(0.1) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke; + + // Draw atoms and molecules in background + _drawAtom(canvas, Offset(size.width * 0.2, size.height * 0.2), 15, paint); + _drawAtom(canvas, Offset(size.width * 0.8, size.height * 0.7), 10, paint); + _drawMolecule(canvas, Offset(size.width * 0.7, size.height * 0.3), paint); + + // Draw some math/science symbols + final textPainter = TextPainter( + text: const TextSpan( + text: "E=mc²", + style: TextStyle( + color: Colors.indigo, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + textDirection: TextDirection.ltr, + ); + textPainter.layout(); + textPainter.paint(canvas, Offset(size.width * 0.1, size.height * 0.8)); + + // Draw DNA helix pattern along one side + _drawDnaHelix(canvas, Offset(size.width * 0.05, size.height * 0.3), + size.height * 0.4, paint); + } + + void _drawAtom(Canvas canvas, Offset center, double radius, Paint paint) { + // Nucleus + canvas.drawCircle( + center, + radius * 0.3, + Paint() + ..color = Colors.blue.withOpacity(0.3) + ..style = PaintingStyle.fill); + + // Electron orbits + for (int i = 0; i < 3; i++) { + canvas.drawOval( + Rect.fromCenter( + center: center, + width: radius * 2 * (0.7 + i * 0.3), + height: radius * 2 * (0.5 + i * 0.3), + ), + paint, + ); + } + + // Electrons + final electronPaint = Paint() + ..color = Colors.indigo + ..style = PaintingStyle.fill; + + canvas.drawCircle( + Offset(center.dx + radius * 0.9, center.dy + radius * 0.3), + radius * 0.1, + electronPaint); + + canvas.drawCircle( + Offset(center.dx - radius * 0.8, center.dy - radius * 0.4), + radius * 0.1, + electronPaint); + } + + void _drawMolecule(Canvas canvas, Offset center, Paint paint) { + final molPaint = Paint() + ..color = Colors.purple.withOpacity(0.3) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke; + + // Draw H2O-like molecule + final o = center; + final h1 = Offset(center.dx - 15, center.dy + 10); + final h2 = Offset(center.dx + 15, center.dy + 10); + + // Oxygen + canvas.drawCircle( + o, + 8, + Paint() + ..color = Colors.red.withOpacity(0.3) + ..style = PaintingStyle.fill); + + // Hydrogens + canvas.drawCircle( + h1, + 5, + Paint() + ..color = Colors.blue.withOpacity(0.3) + ..style = PaintingStyle.fill); + canvas.drawCircle( + h2, + 5, + Paint() + ..color = Colors.blue.withOpacity(0.3) + ..style = PaintingStyle.fill); + + // Bonds + canvas.drawLine(o, h1, molPaint); + canvas.drawLine(o, h2, molPaint); + } + + void _drawDnaHelix(Canvas canvas, Offset start, double height, Paint paint) { + final dnaStrandPaint1 = Paint() + ..color = Colors.purple.withOpacity(0.2) + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke; + + final dnaStrandPaint2 = Paint() + ..color = Colors.blue.withOpacity(0.2) + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke; + + const amplitude = 10.0; + const period = 40.0; + const stepSize = 4.0; + + Path path1 = Path(); + Path path2 = Path(); + + path1.moveTo(start.dx, start.dy); + path2.moveTo(start.dx, start.dy + period / 2); + + for (double i = 0; i <= height; i += stepSize) { + final y = start.dy + i; + final x1 = start.dx + amplitude * math.sin((i / period) * 2 * math.pi); + final x2 = start.dx + + amplitude * math.sin(((i + period / 2) / period) * 2 * math.pi); + + path1.lineTo(x1, y); + path2.lineTo(x2, y); + + // Draw connecting "rungs" of the DNA ladder + if (i % 20 < 1) { + canvas.drawLine( + Offset(x1, y), + Offset(x2, y), + Paint() + ..color = Colors.grey.withOpacity(0.2) + ..strokeWidth = 1.0); + } + } + + canvas.drawPath(path1, dnaStrandPaint1); + canvas.drawPath(path2, dnaStrandPaint2); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +class QuestionPage extends StatefulWidget { + final bool isKerja; // true=Kerja, false=Kuliah + QuestionPage({Key? key, required this.isKerja}) : super(key: key); + + @override + State createState() => _QuestionPageState(); +} + +// Tambahkan class ini untuk animasi item +class AnimatedListItem extends StatefulWidget { + final Widget child; + final int index; + final bool isHighlighted; + + const AnimatedListItem({ + Key? key, + required this.child, + required this.index, + this.isHighlighted = false, + }) : super(key: key); + + @override + State createState() => _AnimatedListItemState(); +} + +class _AnimatedListItemState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _opacityAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + + // Buat controller dengan durasi yang lebih lama untuk efek yang lebih halus + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 600 + (widget.index * 100)), + ); + + // Buat animasi scale yang dimulai dari 0.8 hingga 1.0 + _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.8, curve: Curves.easeOutCubic), + ), + ); + + // Buat animasi opacity dari 0 hingga 1 + _opacityAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.6, curve: Curves.easeIn), + ), + ); + + // Buat animasi slide dari bawah + _slideAnimation = Tween( + begin: const Offset(0, 0.5), + end: const Offset(0, 0), + ).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.8, curve: Curves.easeOutCubic), + ), + ); + + // Mulai animasi dengan sedikit delay berdasarkan index + Future.delayed(Duration(milliseconds: widget.index * 60), () { + if (mounted) { + _controller.forward(); + } + }); + } + + @override + void didUpdateWidget(AnimatedListItem oldWidget) { + super.didUpdateWidget(oldWidget); + + // Jika item baru di-highlight, mainkan animasi perhatian + if (widget.isHighlighted && !oldWidget.isHighlighted) { + _playAttentionAnimation(); + } + } + + void _playAttentionAnimation() { + // Animasi perhatian: scale sedikit lebih besar kemudian kembali normal + _controller.reset(); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _opacityAnimation, + child: Transform.scale( + scale: _scaleAnimation.value, + alignment: Alignment.center, + child: child, + ), + ), + ); + }, + child: widget.child, + ); + } +} + +class _QuestionPageState extends State + with TickerProviderStateMixin { +// Modifikasi metode _showAutoFillOptions + void _showAutoFillOptions( + BuildContext context, QuestionController controller) { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2), + ), + ), + Text( + 'Auto Fill Options (Testing)', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 20), + // Opsi untuk mengisi & lanjut ke halaman berikutnya + _buildAutoFillOption( + context, + icon: Icons.fast_forward, + label: 'Lanjut ke Halaman Berikutnya', + color: Colors.blue.shade600, + onTap: () { + controller.autoFillAnswers(true); + Navigator.pop(context); + // Langsung lanjut ke halaman berikutnya + if (controller.currentPage.value < + controller.totalPages - 1) { + controller.nextPage(); + } else { + // Jika halaman terakhir, tampilkan hasil + final results = controller.runForwardChaining(); + controller.saveResultsToFirestore(results).then((_) { + showRecommendationResultsGetx(results); + }); + } + }, + ), + const Divider(height: 1), + // Opsi untuk mengisi & menyelesaikan semua halaman + _buildAutoFillOption( + context, + icon: Icons.done_all, + label: 'Selesaikan Semua & Lihat Hasil', + color: Colors.green.shade600, + onTap: () { + Navigator.pop(context); + // Isi semua pertanyaan di semua halaman + controller.autoFillAllPages(true).then((_) { + // Kemudian jalankan forward chaining + final results = controller.runForwardChaining(); + controller.saveResultsToFirestore(results).then((_) { + showRecommendationResultsGetx(results); + }); + }); + }, + ), + const Divider(height: 1), + // Opsi mengisi secara acak & menyelesaikan semua + _buildAutoFillOption( + context, + icon: Icons.shuffle, + label: 'Isi Acak & Lihat Hasil', + color: Colors.purple.shade600, + onTap: () { + Navigator.pop(context); + // Isi semua pertanyaan secara acak + controller.autoFillAllPages(null).then((_) { + // Kemudian jalankan forward chaining + final results = controller.runForwardChaining(); + controller.saveResultsToFirestore(results).then((_) { + showRecommendationResultsGetx(results); + }); + }); + }, + ), + const SizedBox(height: 16), + const Divider(height: 1), + // Opsi mengisi secara acak & menyelesaikan semua + _buildAutoFillOption( + context, + icon: Icons.shuffle, + label: 'Isi Salah', + color: Colors.purple.shade600, + onTap: () { + Navigator.pop(context); + // Isi semua pertanyaan secara acak + controller.autoFillAllPages(false).then((_) { + // Kemudian jalankan forward chaining + final results = controller.runForwardChaining(); + controller.saveResultsToFirestore(results).then((_) { + showRecommendationResultsGetx(results); + }); + }); + }, + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + foregroundColor: Colors.grey.shade700, + ), + child: const Text('Batal'), + ), + ), + ], + ), + ); + }, + ); + } + + // Helper untuk item menu auto fill + Widget _buildAutoFillOption( + BuildContext context, { + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Row( + children: [ + Icon( + icon, + color: color, + size: 24, + ), + const SizedBox(width: 16), + Text( + label, + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade800, + ), + ), + const Spacer(), + Icon( + Icons.arrow_forward_ios, + color: Colors.grey.shade400, + size: 16, + ), + ], + ), + ), + ); + } + + // Dialog konfirmasi yang ditampilkan ketika pengguna ingin kembali + + // Handler untuk tombol kembali dengan logika berdasarkan halaman + Future _handleBackPress(BuildContext context) async { + // Jika berada di halaman selain halaman pertama, cukup decrement controller + if (controller.currentPage.value > 0) { + controller.prevPage(); + return false; // Jangan keluar dari aplikasi/halaman + } + // Jika di halaman pertama, tampilkan dialog konfirmasi + else { + return await _showExitConfirmationDialog(context); + } + } + + // Dialog konfirmasi saat di halaman pertama + Future _showExitConfirmationDialog(BuildContext context) async { + bool? exitPage = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + elevation: 8, + content: Container( + width: MediaQuery.of(context).size.width * 0.85, + constraints: BoxConstraints( + maxWidth: 400, // Memastikan dialog tidak terlalu lebar + maxHeight: MediaQuery.of(context).size.height * + 0.6, // Memastikan dialog tidak terlalu tinggi + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + Colors.blue.shade50, + ], + ), + ), + child: Stack( + clipBehavior: Clip.none, + children: [ + // Background dengan pattern ringan + Positioned.fill( + child: CustomPaint( + painter: PatternPainter(), + ), + ), + + // Main content + Padding( + padding: const EdgeInsets.fromLTRB(24, 60, 24, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Peringatan!', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.7), + borderRadius: BorderRadius.circular(16), + border: + Border.all(color: Colors.red.withOpacity(0.3)), + ), + child: Column( + children: [ + RichText( + textAlign: TextAlign.center, + text: const TextSpan( + style: TextStyle( + fontSize: 16, + color: Colors.black87, + height: 1.4, + ), + children: [ + TextSpan( + text: + 'Semua jawaban pada kuesioner minat akan ', + ), + TextSpan( + text: 'HILANG ', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + TextSpan( + text: + 'jika Anda kembali ke halaman pemilihan minat.\n\n', + ), + TextSpan( + text: + 'Anda harus mengisi ulang semua pertanyaan jika melanjutkan.', + style: + TextStyle(fontWeight: FontWeight.w500), + ), + ], + ), + ), + const SizedBox(height: 12), + const Text( + 'Yakin ingin kembali?', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment + .spaceBetween, // Menggunakan spaceBetween alih-alih spaceEvenly + children: [ + // Tombol Kembali (kiri) + OutlinedButton( + onPressed: () { + // Tampilkan dialog konfirmasi kedua + _showSecondConfirmation(dialogContext); + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10), // Mengurangi padding + side: BorderSide(color: Colors.red.shade400), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: const Text( + 'Kembali', + style: TextStyle( + fontSize: 14, // Mengurangi ukuran font + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + + // Tombol Lanjutkan Mengisi (kanan) + ElevatedButton( + onPressed: () { + Navigator.of(dialogContext).pop(false); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10), // Mengurangi padding + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: const Text( + 'Lanjutkan Mengisi', + style: TextStyle( + fontSize: 14, // Mengurangi ukuran font + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + ), + + // Top warning icon + const Positioned( + top: -30, + left: 0, + right: 0, + child: Center( + child: PulsingWarningIcon(), + ), + ), + ], + ), + ), + ); + }, + ); + + return exitPage ?? false; + } + + // Konfirmasi kedua sebelum kembali + void _showSecondConfirmation(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext secondDialogContext) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: const Text( + 'Konfirmasi Terakhir', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + content: const Text( + 'Anda yakin ingin kembali? Semua jawaban Anda akan hilang dan tidak dapat dikembalikan.', + style: TextStyle(fontSize: 16), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(secondDialogContext).pop(); + }, + child: const Text( + 'BATAL', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ), + ElevatedButton( + onPressed: () { + // Tutup dialog kedua + Navigator.of(secondDialogContext).pop(); + // Tutup dialog pertama dengan hasil true (kembali) + Navigator.of(context).pop(true); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'YA, KEMBALI', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ], + ); + }, + ); + } + + final List _controllers = []; + + final List> _animations = []; + + final List _icons = [ + Icons.science, + Icons.psychology, + Icons.sports_soccer, + Icons.computer, + Icons.brush, + Icons.business, + Icons.music_note, + Icons.health_and_safety, + ]; + + final List _sizes = []; + + final List _colors = []; + + final List _positions = []; + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _opacityAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + + // Generate random positions, sizes and colors for icons + final random = math.Random(); + for (int i = 0; i < 8; i++) { + // Create controller and add to list + final controller = AnimationController( + duration: Duration(milliseconds: 2000 + (500 * i % 2000)), + vsync: this, + )..repeat(reverse: true); + _controllers.add(controller); + + _sizes.add(12.0 + random.nextDouble() * 12); + _colors.add( + Color.fromRGBO( + 50 + random.nextInt(150), + 50 + random.nextInt(150), + 150 + random.nextInt(100), + 0.3, + ), + ); + _positions.add( + Offset( + random.nextDouble() * 250, + random.nextDouble() * 300, + ), + ); + + // Create animation and add to list + final animation = Tween( + begin: Offset(random.nextDouble() * 0.3, random.nextDouble() * 0.3), + end: Offset(-random.nextDouble() * 0.3, -random.nextDouble() * 0.3), + ).animate(CurvedAnimation( + parent: controller, + curve: Curves.easeInOut, + )); + _animations.add(animation); + } + } + + final controller = Get.find(); + final scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, // Prevent automatic pop + onPopInvoked: (didPop) async { + if (!didPop) { + final shouldExit = await _handleBackPress(context); + if (shouldExit && context.mounted) { + Navigator.of(context).pop(); + } + } + }, + + child: Scaffold( + body: Container( + color: Colors.blue.shade700, + child: Obx(() { + if (controller.isLoading.value) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + if (controller.errorMessage.value.isNotEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, + size: 60, color: Colors.white70), + const SizedBox(height: 16), + Text( + 'Error: ${controller.errorMessage.value}', + style: const TextStyle(color: Colors.white, fontSize: 16), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + if (controller.programList.value.isEmpty) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.folder_open, size: 60, color: Colors.white70), + SizedBox(height: 16), + Text( + 'Data Kosong', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + ); + } + + final questionsThisPage = controller.questionsThisPage; + final startIndex = + controller.currentPage.value * QuestionController.pageSize; + + // Jika flag scroll ke atas aktif, lakukan scrolling + if (controller.shouldScrollToTop) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + scrollController.animateTo(0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut); + // Reset flag + controller.shouldScrollToTop = false; + } + }); + } + + return Column( + children: [ + SafeArea( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Progress Indicator Section - Kept as requested + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 12), + decoration: BoxDecoration( + color: Colors.amber.shade600, + borderRadius: BorderRadius.circular(50), + ), + child: Text( + '${controller.answeredCount}/${controller.totalCount} Pertanyaan', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 13, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Halaman ${controller.currentPage.value + 1} dari ${controller.totalPages}', + style: const TextStyle( + color: Colors.white70, fontSize: 13), + textAlign: TextAlign.right, + ), + ) + ], + ), + + SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LinearProgressIndicator( + value: controller.answeredCount / + controller.totalCount, + backgroundColor: Colors.white24, + color: Colors.amber.shade500, + minHeight: 8, + ), + ), + const SizedBox(height: 10), + ShimmerEffect( + baseColor: Colors.white, + highlightColor: Colors.yellow, + duration: const Duration(seconds: 3), + child: Row( + children: [ + const Icon( + Icons.info_outline, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + 'Jawab dengan jujur pertanyaan di bawah ini.', + style: TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ], + ), + ), + ), + + // Questions Card + Expanded( + child: Container( + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + child: Column( + children: [ + // Title + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 16), + child: Text( + widget.isKerja ? 'Pertanyaan' : 'Pertanyaan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + ), + + // List of Questions + Expanded( + child: ListView.builder( + controller: + scrollController, // Tambahkan controller + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + itemCount: questionsThisPage.length, + itemBuilder: (context, index) { + final qItem = questionsThisPage[index]; + final globalIndex = startIndex + index; + + return Obx(() { + // Cek apakah pertanyaan ini perlu di-highlight + final isHighlighted = controller + .highlightedQuestionIds + .contains(qItem.id); + + // Cek status jawaban dari pertanyaan ini + final isAnswered = controller.allQuestions + .firstWhere((q) => q.id == qItem.id) + .userAnswer != + null; + + return Card( + key: ValueKey( + 'question_${qItem.id}'), // Key untuk identifikasi + margin: const EdgeInsets.only(bottom: 16), + elevation: 2, + shadowColor: Colors.black.withOpacity(0.1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + // Wrap dengan Container biasa agar tidak memicu animasi saat jawaban dipilih + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isHighlighted + ? Colors.amber.shade500 + : Colors.transparent, + width: 2, + ), + boxShadow: isHighlighted + ? [ + BoxShadow( + color: Colors.amber.shade200 + .withOpacity(0.5), + blurRadius: 8, + spreadRadius: 2, + ) + ] + : null, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // Question header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isHighlighted + ? Colors.amber.shade50 + : Colors.blue.shade50, + borderRadius: + const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( + children: [ + Container( + padding: + const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isHighlighted + ? Colors.amber.shade600 + : Colors.blue.shade600, + shape: BoxShape.circle, + ), + child: Text( + '${globalIndex + 1}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + qItem.questionText, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + ), + // Badge untuk pertanyaan yang di-highlight + if (isHighlighted && !isAnswered) + Container( + padding: const EdgeInsets + .symmetric( + horizontal: 8, + vertical: 4), + decoration: BoxDecoration( + color: + Colors.amber.shade100, + borderRadius: + BorderRadius.circular( + 8), + border: Border.all( + color: Colors + .amber.shade600), + ), + child: Text( + 'Belum Diisi', + style: TextStyle( + fontSize: 12, + color: + Colors.amber.shade900, + fontWeight: + FontWeight.bold, + ), + ), + ), + ], + ), + ), + + // Answer options + Column( + children: [ + _buildAnswerOption( + icon: Icons.check_circle_outline, + label: 'Ya', + isSelected: controller + .allQuestions + .firstWhere((q) => + q.id == qItem.id) + .userAnswer == + true, + onTap: () { + controller.setAnswer( + qItem, true); + // Hapus highlight saat jawaban dipilih + if (controller + .highlightedQuestionIds + .contains(qItem.id)) { + controller + .highlightedQuestionIds + .remove(qItem.id); + } + }, + activeColor: + Colors.green.shade600, + ), + Divider( + height: 1, + color: Colors.grey.shade200, + ), + _buildAnswerOption( + icon: Icons.cancel_outlined, + label: 'Tidak', + isSelected: controller + .allQuestions + .firstWhere((q) => + q.id == qItem.id) + .userAnswer == + false, + onTap: () { + controller.setAnswer( + qItem, false); + // Hapus highlight saat jawaban dipilih + if (controller + .highlightedQuestionIds + .contains(qItem.id)) { + controller + .highlightedQuestionIds + .remove(qItem.id); + } + }, + activeColor: Colors.red.shade600, + ), + ], + ), + ], + ), + ), + ); + }); + }, + ), + ), + + // Navigation Buttons + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 16), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + // Back Button + Expanded( + flex: 1, + child: ElevatedButton.icon( + onPressed: controller.currentPage.value > 0 + ? () { + // Hapus semua highlight sebelum pindah halaman + controller.highlightedQuestionIds + .clear(); + controller.prevPage(); + // Set flag untuk scroll ke atas + controller.shouldScrollToTop = true; + } + : null, + icon: const Icon(Icons.arrow_back, size: 18), + label: const Text('Kembali'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.grey.shade700, + backgroundColor: Colors.grey.shade200, + padding: const EdgeInsets.symmetric( + vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + ), + ), + const SizedBox(width: 16), + + // Next/Submit Button + Expanded( + flex: 2, + child: Obx(() { + bool allAnswered = + controller.allAnsweredThisPage; + final isLastPage = + controller.currentPage.value == + controller.totalPages - 1; + + return ElevatedButton.icon( + onPressed: () { + if (allAnswered) { + // Hapus semua highlight sebelum pindah halaman + controller.highlightedQuestionIds + .clear(); + + if (!isLastPage) { + controller.nextPage(); + // Set flag untuk scroll ke atas + controller.shouldScrollToTop = true; + } else { + final results = + controller.runForwardChaining(); + controller + .saveResultsToFirestore(results) + .then((_) { + // Then show results to user + showFeedbackEvaluationPage( + results, + controller.isKerja, + controller.majorType); + }); + } + } else { + // Highlight pertanyaan yang belum dijawab + controller.highlightUnansweredQuestions( + scrollController); + } + }, + icon: Icon( + isLastPage + ? Icons.check_circle + : Icons.arrow_forward, + size: 18), + label: Text( + isLastPage ? 'Cek Rekomendasi' : 'Lanjut', + ), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.blue.shade600, + padding: const EdgeInsets.symmetric( + vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + disabledBackgroundColor: + null, // Hapus ini agar tombol selalu aktif + ), + ); + }), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ); + }), + ), + ), + ); + } + + // Fungsi helper yang sama seperti sebelumnya + Widget _buildAnswerOption({ + required IconData icon, + required String label, + required bool isSelected, + required VoidCallback onTap, + required Color activeColor, + }) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Icon( + icon, + size: 22, + color: isSelected ? activeColor : Colors.grey.shade400, + ), + const SizedBox(width: 16), + Text( + label, + style: TextStyle( + fontSize: 15, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected ? activeColor : Colors.grey.shade700, + ), + ), + const Spacer(), + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? activeColor : Colors.transparent, + border: Border.all( + color: isSelected ? activeColor : Colors.grey.shade400, + width: 2, + ), + ), + child: isSelected + ? const Icon( + Icons.check, + size: 16, + color: Colors.white, + ) + : null, + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/views/student/feature/quiz/view/page_select_economy.dart b/lib/app/views/student/feature/quiz/view/page_select_economy.dart new file mode 100644 index 0000000..227f755 --- /dev/null +++ b/lib/app/views/student/feature/quiz/view/page_select_economy.dart @@ -0,0 +1,677 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/admin_mode.dart'; +import 'package:forward_chaining_man_app/app/views/about/page_about.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/controller/question_controller.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/view/page_question.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/view/page_select_major.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/view/widget/wave_clipper.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/recomendation_screen/view/page_recomendation_screen.dart'; +import 'package:forward_chaining_man_app/app/views/student/model/data_student.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:firebase_core/firebase_core.dart'; +import 'package:intl/intl.dart' as intl; + +import 'package:url_launcher/url_launcher.dart'; + +// Helper function to replace the simple dialog with our new UI +void showRecommendationResultsGetx(RecommendationResult result, + {String rawMessage = ''}) { + Get.to(() => RecommendationResultsScreen( + result: result, + rawMessage: rawMessage, + )); +} + +class HomeController extends GetxController { + final Rx pilihan = + Rx(null); // null=belum pilih; true=Kerja; false=Kuliah + final RxString selectedKode = + "".obs; // Menyimpan kode pilihan yang dipilih user + + // Step tracking - new variable to track current selection step + final RxInt currentStep = 0.obs; // 0=Pilih kondisi ekonomi, 1=Pilih rencana + + // Variable to track selected economic condition + final RxString selectedEconomicCondition = "".obs; // "CUKUP" or "TERBATAS" + + // Dapatkan preferensi minat dari controller sebelumnya + final majorPrefController = Get.find(); + + // Method to select economic condition in step 1 + void setEconomicCondition(String condition) { + selectedEconomicCondition.value = condition; + // Move to next step + currentStep.value = 1; + } + + // Method to select plan in step 2 + void setPilihan(String kode) { + if (selectedKode.value == kode) + return; // Jika memilih yang sama, tidak berubah + selectedKode.value = kode; + + // Logika pemilihan: Kuliah atau Kerja + if (kode == "E01" || kode == "E02" || kode == "E03") { + pilihan.value = false; // Kuliah + } else if (kode == "E04" || kode == "E05") { + pilihan.value = true; // Kerja + } + } + + // Method to go back to step 1 + void goBackToStep1() { + currentStep.value = 0; + selectedKode.value = ""; + pilihan.value = null; + } + + // Tambahkan getter untuk memudahkan akses preferensi minat + String get selectedMajor => majorPrefController.selectedMajor.value; + bool get isSainsMajor => selectedMajor == "SAINS"; +} + +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(HomeController()); + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: Column( + children: [ + // Custom App Bar + SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: GestureDetector( + onTap: () { + // If on step 2, go back to step 1, otherwise go back to previous page + if (controller.currentStep.value == 1) { + controller.goBackToStep1(); + } else { + Get.back(); + } + }, + child: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 16), + Obx(() => Text( + controller.currentStep.value == 0 + ? 'Pilih Kondisi Ekonomi' + : 'Pilih Rencana Anda', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + )), + ], + ), + ), + ), + + // Main Content Area + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 5), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(30), + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 30, 20, 20), + child: Obx(() { + if (controller.currentStep.value == 0) { + // STEP 1: Choose Economic Condition + return _buildEconomicConditionStep(controller); + } else { + // STEP 2: Choose Plan based on Economic Condition + return _buildPlanSelectionStep(controller, context); + } + }), + ), + ), + ), + ], + ), + ), + ); + } + +// Fungsi untuk menampilkan dialog konfirmasi + void _showConfirmationDialog(BuildContext context, VoidCallback onConfirm) { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: 'Konfirmasi', + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (context, animation, secondaryAnimation) { + return Container(); // Tidak digunakan, kita menggunakan transitionBuilder + }, + transitionBuilder: (context, animation, secondaryAnimation, child) { + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.easeOutBack, + ); + + return ScaleTransition( + scale: Tween(begin: 0.8, end: 1.0).animate(curvedAnimation), + child: FadeTransition( + opacity: + Tween(begin: 0.0, end: 1.0).animate(curvedAnimation), + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + insetPadding: const EdgeInsets.symmetric(horizontal: 20), + child: ConfirmationDialogContent(onConfirm: onConfirm), + ), + ), + ); + }, + ); + } + + // STEP 1: Widget for Economic Condition Selection + Widget _buildEconomicConditionStep(HomeController controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.account_balance_wallet_outlined, + size: 24, + color: Colors.blue.shade800, + ), + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Kondisi Ekonomi', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + 'Pilih situasi yang paling sesuai', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 30), + + // Economic Condition Cards + buildEconomicConditionCard( + title: "Kondisi ekonomi cukup untuk kuliah", + subtitle: "Memiliki dana untuk pendidikan lanjutan", + condition: "CUKUP", + icon: Icons.school, + controller: controller, + ), + + const SizedBox(height: 16), + + buildEconomicConditionCard( + title: "Kondisi ekonomi terbatas", + subtitle: "Perlu mempertimbangkan berbagai pilihan", + condition: "TERBATAS", + icon: Icons.attach_money, + controller: controller, + ), + + const Spacer(), + ], + ); + } + + // STEP 2: Widget for Plan Selection based on Economic Condition + Widget _buildPlanSelectionStep( + HomeController controller, BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.lightbulb_outline, + size: 24, + color: Colors.blue.shade800, + ), + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.selectedEconomicCondition.value == "CUKUP" + ? 'Dengan Ekonomi Cukup' + : 'Dengan Ekonomi Terbatas', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + 'Pilih rencana yang paling sesuai', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 24), + + // Options List + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + // Show relevant options based on economic condition + if (controller.selectedEconomicCondition.value == "CUKUP") ...[ + // Options for sufficient economic condition + Obx(() => buildOptionCard( + title: "Kuliah", + subtitle: "Melanjutkan pendidikan ke perguruan tinggi", + kode: "E01", + icon: Icons.school, + controller: controller, + )), + + Obx(() => buildOptionCard( + title: "Mencari beasiswa", + subtitle: "Kuliah dengan bantuan biaya pendidikan", + kode: "E03", + icon: Icons.card_giftcard, + controller: controller, + )), + + Obx(() => buildOptionCard( + title: "Memilih bekerja atau usaha", + subtitle: "Langsung terjun ke dunia kerja", + kode: "E04", + icon: Icons.work, + controller: controller, + )), + + Obx(() => buildOptionCard( + title: "Bekerja dulu, kuliah nanti", + subtitle: "Menunda kuliah untuk bekerja", + kode: "E05", + icon: Icons.timeline, + controller: controller, + )), + ] else ...[ + // Options for limited economic condition + Obx(() => buildOptionCard( + title: "Mencari beasiswa", + subtitle: "Kuliah dengan bantuan biaya pendidikan", + kode: "E03", + icon: Icons.card_giftcard, + controller: controller, + )), + + Obx(() => buildOptionCard( + title: "Kuliah dengan biaya terjangkau", + subtitle: "Memilih perguruan tinggi yang ekonomis", + kode: "E02", + icon: Icons.school, + controller: controller, + )), + + Obx(() => buildOptionCard( + title: "Memilih bekerja atau usaha", + subtitle: "Langsung terjun ke dunia kerja", + kode: "E04", + icon: Icons.work, + controller: controller, + )), + + Obx(() => buildOptionCard( + title: "Bekerja dulu, kuliah nanti", + subtitle: "Menunda kuliah untuk bekerja", + kode: "E05", + icon: Icons.timeline, + controller: controller, + )), + ], + + // Add some space at the bottom for better scrolling + const SizedBox(height: 8), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Next Button + Obx(() => AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: controller.selectedKode.value.isEmpty ? 0.6 : 1.0, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: controller.selectedKode.value.isEmpty + ? [] + : [ + BoxShadow( + color: Colors.blue.shade300.withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, 6), + ), + ], + ), + child: ElevatedButton( + onPressed: controller.selectedKode.value.isEmpty + ? null + : () { + // Munculkan dialog konfirmasi terlebih dahulu + _showConfirmationDialog(context, () { + // Callback ini akan dipanggil ketika user menekan tombol "Ya, Lanjutkan" + // Buat instance QuestionController dengan preferensi yang sesuai + final questionController = + Get.put(QuestionController( + isKerja: controller.pilihan.value!, + majorType: controller.selectedMajor, + )); + + questionController.clearQuestion(); + questionController.loadProgramData( + controller.pilihan.value!, + controller.selectedMajor); + + // Navigasi ke halaman pertanyaan + Get.to(() => QuestionPage( + isKerja: controller.pilihan.value!, + )); + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + minimumSize: const Size(double.infinity, 54), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + disabledBackgroundColor: Colors.blue.shade200, + disabledForegroundColor: Colors.white70, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + controller.selectedKode.value.isEmpty + ? 'Pilih Salah Satu Opsi' + : 'Lanjutkan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward, size: 20), + ], + ), + ), + ), + )), + ], + ); + } + + // Widget for economic condition selection card (Step 1) + Widget buildEconomicConditionCard({ + required String title, + required String subtitle, + required String condition, + required IconData icon, + required HomeController controller, + }) { + return Container( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.setEconomicCondition(condition), + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.white, + border: Border.all( + color: Colors.grey.shade200, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + blurRadius: 4, + offset: const Offset(0, 3), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + size: 30, + color: Colors.blue.shade800, + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + const SizedBox(height: 6), + Text( + subtitle, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + Icon( + Icons.arrow_forward_ios, + color: Colors.blue.shade300, + size: 18, + ), + ], + ), + ), + ), + ), + ); + } + + /// Widget untuk membuat tampilan pilihan lebih menarik dan modern (Step 2) + Widget buildOptionCard({ + required String title, + required String subtitle, + required String kode, + required IconData icon, + required HomeController controller, + }) { + final isSelected = controller.selectedKode.value == kode; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.setPilihan(kode), + borderRadius: BorderRadius.circular(16), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: isSelected ? Colors.blue.shade50 : Colors.white, + border: Border.all( + color: isSelected ? Colors.blue.shade400 : Colors.grey.shade200, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + blurRadius: isSelected ? 8 : 4, + offset: const Offset(0, 3), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade400 + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + size: 24, + color: isSelected ? Colors.white : Colors.grey.shade600, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.blue.shade800 + : Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle( + fontSize: 13, + color: isSelected + ? Colors.blue.shade600 + : Colors.grey.shade600, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isSelected ? 1.0 : 0.0, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.blue.shade400, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check, + color: Colors.white, + size: 16, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/views/student/feature/quiz/view/page_select_major.dart b/lib/app/views/student/feature/quiz/view/page_select_major.dart new file mode 100644 index 0000000..5656e19 --- /dev/null +++ b/lib/app/views/student/feature/quiz/view/page_select_major.dart @@ -0,0 +1,354 @@ +import 'package:flutter/material.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/view/page_select_economy.dart'; +import 'package:get/get.dart'; + +class MajorPreferenceController extends GetxController { + final RxString selectedMajor = "".obs; + + void setMajor(String major) { + if (selectedMajor.value == major) return; + selectedMajor.value = major; + } +} + +class MajorPreferencePage extends StatelessWidget { + const MajorPreferencePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(MajorPreferenceController()); + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: Column( + children: [ + // Custom App Bar + SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: GestureDetector( + onTap: () => Get.back(), + child: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 16), + const Text( + 'Pilih Minat Anda', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + ), + + // Main Content Area + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 24), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(30), + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 30, 20, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.school_outlined, + size: 24, + color: Colors.blue.shade800, + ), + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Minat IPA', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + 'Pilih bidang yang paling Anda minati', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 24), + + // Options List + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + // IPA Sains Option + Obx(() => buildMajorCard( + title: "IPA (Sains Murni)", + subtitle: + "Fokus: Biologi, Kimia, Fisika (kedokteran, farmasi, sains)", + description: + "Pilihan yang tepat jika Anda tertarik pada ilmu-ilmu murni seperti biologi, kimia, dan fisika. Ideal untuk karir di bidang kedokteran, farmasi, penelitian ilmiah, atau bidang kesehatan lainnya.", + kode: "SAINS", + icon: Icons.biotech, + controller: controller, + )), + + const SizedBox(height: 16), + + // IPA Teknik Option + Obx(() => buildMajorCard( + title: "IPA (Teknik)", + subtitle: + "Fokus: Matematika, Fisika, IT (arah teknik/teknologi)", + description: + "Cocok jika Anda memiliki minat kuat di bidang matematika, fisika terapan, dan teknologi informasi. Ideal untuk karir di bidang teknik, IT, rekayasa, atau arsitektur.", + kode: "TEKNIK", + icon: Icons.engineering, + controller: controller, + )), + + // Add some space at the bottom for better scrolling + const SizedBox(height: 8), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Next Button + Obx(() => AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: controller.selectedMajor.value.isEmpty + ? 0.6 + : 1.0, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: + controller.selectedMajor.value.isEmpty + ? [] + : [ + BoxShadow( + color: Colors.blue.shade300 + .withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, 6), + ), + ], + ), + child: ElevatedButton( + onPressed: + controller.selectedMajor.value.isEmpty + ? null + : () { + // Navigate to economic preference page + Get.to(() => HomePage()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + padding: + const EdgeInsets.symmetric(vertical: 16), + minimumSize: const Size(double.infinity, 54), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + disabledBackgroundColor: Colors.blue.shade200, + disabledForegroundColor: Colors.white70, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + controller.selectedMajor.value.isEmpty + ? 'Pilih Salah Satu Minat' + : 'Lanjutkan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_forward, size: 20), + ], + ), + ), + ), + )), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } + + /// Widget untuk membuat card pilihan minat + Widget buildMajorCard({ + required String title, + required String subtitle, + required String description, + required String kode, + required IconData icon, + required MajorPreferenceController controller, + }) { + final isSelected = controller.selectedMajor.value == kode; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.setMajor(kode), + borderRadius: BorderRadius.circular(16), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: isSelected ? Colors.blue.shade50 : Colors.white, + border: Border.all( + color: isSelected ? Colors.blue.shade400 : Colors.grey.shade200, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + blurRadius: isSelected ? 8 : 4, + offset: const Offset(0, 3), + ), + ], + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade400 + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + size: 24, + color: isSelected ? Colors.white : Colors.grey.shade600, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isSelected + ? Colors.blue.shade800 + : Colors.black87, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle( + fontSize: 13, + color: isSelected + ? Colors.blue.shade600 + : Colors.grey.shade600, + ), + ), + ], + ), + ), + AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isSelected ? 1.0 : 0.0, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.blue.shade400, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check, + color: Colors.white, + size: 16, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + description, + style: TextStyle( + fontSize: 14, + color: + isSelected ? Colors.blue.shade700 : Colors.grey.shade700, + height: 1.4, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/app/views/student/feature/quiz/view/widget/shimmer.dart b/lib/app/views/student/feature/quiz/view/widget/shimmer.dart new file mode 100644 index 0000000..511227f --- /dev/null +++ b/lib/app/views/student/feature/quiz/view/widget/shimmer.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +class ShimmerEffect extends StatefulWidget { + final Widget child; + final Color baseColor; + final Color highlightColor; + final Duration duration; + + const ShimmerEffect({ + Key? key, + required this.child, + this.baseColor = Colors.white54, + this.highlightColor = Colors.white, + this.duration = const Duration(seconds: 2), + }) : super(key: key); + + @override + State createState() => _ShimmerEffectState(); +} + +class _ShimmerEffectState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.duration, + )..repeat(); + + _animation = Tween(begin: -1.0, end: 2.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (BuildContext context, Widget? child) { + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (bounds) { + return LinearGradient( + colors: [ + widget.baseColor, + widget.highlightColor, + widget.baseColor, + ], + stops: const [0.0, 0.5, 1.0], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + transform: _SlidingGradientTransform( + slidePercent: _animation.value, + ), + ).createShader(bounds); + }, + child: widget.child, + ); + }, + ); + } +} + +class _SlidingGradientTransform extends GradientTransform { + const _SlidingGradientTransform({ + required this.slidePercent, + }); + + final double slidePercent; + + @override + Matrix4? transform(Rect bounds, {TextDirection? textDirection}) { + return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0); + } +} diff --git a/lib/app/views/student/feature/quiz/view/widget/wave_clipper.dart b/lib/app/views/student/feature/quiz/view/widget/wave_clipper.dart new file mode 100644 index 0000000..9904739 --- /dev/null +++ b/lib/app/views/student/feature/quiz/view/widget/wave_clipper.dart @@ -0,0 +1,378 @@ +// Widget untuk animasi gelombang di bagian bawah dialog +import 'dart:ui'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; + +// Fungsi untuk menampilkan dialog konfirmasi +void _showConfirmationDialog(BuildContext context, VoidCallback onConfirm) { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: 'Konfirmasi', + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (context, animation, secondaryAnimation) { + return Container(); // Tidak digunakan, kita menggunakan transitionBuilder + }, + transitionBuilder: (context, animation, secondaryAnimation, child) { + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.easeOutBack, + ); + + return ScaleTransition( + scale: Tween(begin: 0.8, end: 1.0).animate(curvedAnimation), + child: FadeTransition( + opacity: Tween(begin: 0.0, end: 1.0).animate(curvedAnimation), + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + insetPadding: const EdgeInsets.symmetric(horizontal: 20), + child: ConfirmationDialogContent(onConfirm: onConfirm), + ), + ), + ); + }, + ); +} + +// Widget konten dialog +class ConfirmationDialogContent extends StatefulWidget { + final VoidCallback onConfirm; + + const ConfirmationDialogContent({ + Key? key, + required this.onConfirm, + }) : super(key: key); + + @override + State createState() => + _ConfirmationDialogContentState(); +} + +class _ConfirmationDialogContentState extends State + with SingleTickerProviderStateMixin { + late AnimationController _waveController; + final List _checkedItems = [false, false, false]; + bool get _allChecked => _checkedItems.every((element) => element); + + @override + void initState() { + super.initState(); + _waveController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(); + } + + @override + void dispose() { + _waveController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints( + maxWidth: 400, + maxHeight: MediaQuery.of(context).size.height * 0.8, + ), + child: Stack( + clipBehavior: Clip.none, + children: [ + // Background dengan gradien dan gelombang + Positioned.fill( + child: AnimatedBuilder( + animation: _waveController, + builder: (context, child) { + return ClipPath( + clipper: WaveClipper(_waveController.value), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.blue.shade50, + Colors.blue.shade100, + ], + ), + borderRadius: BorderRadius.circular(20), + ), + ), + ); + }, + ), + ), + + // Konten utama + Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(20, 24, 20, 20), + decoration: BoxDecoration( + color: Colors.blue.shade600, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Column( + children: [ + const Icon( + Icons.check_circle, + color: Colors.white, + size: 48, + ), + const SizedBox(height: 12), + const Text( + 'Konfirmasi Pemilihan', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Pastikan pilihan Anda sudah sesuai sebelum melanjutkan ke kuisioner', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 14, + ), + ), + ], + ), + ), + + // Checklist konfirmasi + Padding( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Silakan konfirmasi bahwa Anda:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 16), + + // Checklist pertama + CheckboxListTile( + value: _checkedItems[0], + onChanged: (value) { + setState(() { + _checkedItems[0] = value ?? false; + }); + }, + title: const Text( + 'Telah memilih minat yang benar-benar sesuai dengan diri Anda', + style: TextStyle(fontSize: 14), + ), + activeColor: Colors.blue.shade600, + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + dense: true, + ), + + // Checklist kedua + CheckboxListTile( + value: _checkedItems[1], + onChanged: (value) { + setState(() { + _checkedItems[1] = value ?? false; + }); + }, + title: const Text( + 'Mempertimbangkan kondisi ekonomi keluarga dalam membuat pilihan', + style: TextStyle(fontSize: 14), + ), + activeColor: Colors.blue.shade600, + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + dense: true, + ), + + // Checklist ketiga + CheckboxListTile( + value: _checkedItems[2], + onChanged: (value) { + setState(() { + _checkedItems[2] = value ?? false; + }); + }, + title: const Text( + 'Memiliki rencana yang jelas untuk masa depan Anda', + style: TextStyle(fontSize: 14), + ), + activeColor: Colors.blue.shade600, + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + dense: true, + ), + ], + ), + ), + + // Pesan tentang kejujuran + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.amber.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.amber.shade200), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Colors.amber.shade800, + size: 24, + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Di halaman selanjutnya Anda akan menjawab beberapa pertanyaan. Pastikan untuk menjawab dengan jujur sesuai kondisi Anda.', + style: TextStyle(fontSize: 13), + ), + ), + ], + ), + ), + ), + + // Tombol-tombol + Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + child: Row( + children: [ + // Tombol Kembali + Expanded( + child: OutlinedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: OutlinedButton.styleFrom( + foregroundColor: Colors.blue.shade700, + side: BorderSide(color: Colors.blue.shade300), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: const Text('Kembali'), + ), + ), + const SizedBox(width: 12), + + // Tombol Lanjutkan + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _allChecked + ? () { + Navigator.of(context).pop(); + widget.onConfirm(); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade600, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: const Text( + 'Ya, Lanjutkan', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ), + ], + ), + + // Lingkaran dekoratif di pojok + Positioned( + right: -15, + top: -15, + child: Container( + height: 60, + width: 60, + decoration: BoxDecoration( + color: Colors.blue.shade400.withOpacity(0.3), + shape: BoxShape.circle, + ), + ), + ), + Positioned( + left: -10, + bottom: -10, + child: Container( + height: 40, + width: 40, + decoration: BoxDecoration( + color: Colors.blue.shade400.withOpacity(0.3), + shape: BoxShape.circle, + ), + ), + ), + ], + ), + ); + } +} + +class WaveClipper extends CustomClipper { + final double animation; + + WaveClipper(this.animation); + + @override + Path getClip(Size size) { + final path = Path(); + final height = size.height; + final width = size.width; + + path.lineTo(0, height * 0.7); + + // Buat gelombang dengan animasi + final firstControlPoint = Offset( + width * 0.25, height * (0.7 + 0.04 * math.sin(animation * math.pi))); + final firstEndPoint = Offset(width * 0.5, height * 0.7); + path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy, + firstEndPoint.dx, firstEndPoint.dy); + + final secondControlPoint = Offset( + width * 0.75, height * (0.7 - 0.04 * math.sin(animation * math.pi))); + final secondEndPoint = Offset(width, height * 0.7); + path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy, + secondEndPoint.dx, secondEndPoint.dy); + + path.lineTo(width, 0); + path.close(); + return path; + } + + @override + bool shouldReclip(covariant CustomClipper oldClipper) => true; +} diff --git a/lib/app/views/student/feature/recomendation_screen/page_cetak_sertifikat.dart b/lib/app/views/student/feature/recomendation_screen/page_cetak_sertifikat.dart new file mode 100644 index 0000000..254805f --- /dev/null +++ b/lib/app/views/student/feature/recomendation_screen/page_cetak_sertifikat.dart @@ -0,0 +1,2950 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:forward_chaining_man_app/app/controllers/home_controller.dart'; +import 'package:forward_chaining_man_app/app/views/student/model/data_student.dart'; +import 'package:get/get.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:math' as math; + +import 'package:url_launcher/url_launcher.dart'; + +class MagicRevealAnimation extends StatefulWidget { + final Widget child; + final VoidCallback? onAnimationComplete; + + const MagicRevealAnimation({ + Key? key, + required this.child, + this.onAnimationComplete, + }) : super(key: key); + + @override + State createState() => _MagicRevealAnimationState(); +} + +class _MagicRevealAnimationState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _revealAnimation; + late Animation _sparkleAnimation; + + // Generate random positions for sparkles + final List _sparklePositions = List.generate( + 20, + (_) => Offset( + math.Random().nextDouble(), + math.Random().nextDouble(), + ), + ); + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2500), + ); + + // Animation for the reveal effect (0.0 to 1.0) + _revealAnimation = CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.6, curve: Curves.easeOutCubic), + ); + + // Animation for the sparkle effect (0.0 to 1.0 and back to 0.0) + _sparkleAnimation = TweenSequence([ + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 1.0) + .chain(CurveTween(curve: Curves.easeOut)), + weight: 40, + ), + TweenSequenceItem( + tween: Tween(begin: 1.0, end: 0.0) + .chain(CurveTween(curve: Curves.easeInExpo)), + weight: 60, + ), + ]).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.3, 1.0), + ), + ); + + // Start the animation after a short delay + Future.delayed(const Duration(milliseconds: 300), () { + _controller.forward().then((_) { + if (widget.onAnimationComplete != null) { + widget.onAnimationComplete!(); + } + }); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Stack( + children: [ + // The certificate with reveal effect + ClipPath( + clipper: RevealClipper(_revealAnimation.value), + child: widget.child, + ), + + // Sparkle effect + ..._buildSparkles(), + ], + ); + }, + ); + } + + List _buildSparkles() { + return _sparklePositions.map((position) { + // Calculate a delay factor based on position (0.0 to 0.5) + final delay = position.dx * 0.5; + + // Apply the delay to the animation + final delayedAnimation = _sparkleAnimation.value - delay; + final opacity = math.max(0.0, math.min(1.0, delayedAnimation)); + + if (opacity <= 0) { + return const SizedBox.shrink(); + } + + // Calculate size based on animation progress (larger at peak) + final sizeFactor = 1.0 + math.sin(delayedAnimation * math.pi) * 1.5; + final size = 10.0 * sizeFactor; + + return Positioned( + left: position.dx * MediaQuery.of(context).size.width, + top: position.dy * MediaQuery.of(context).size.height, + child: Opacity( + opacity: opacity, + child: SparkleWidget( + size: size, + color: const Color(0xFFd9dbe6), + ), + ), + ); + }).toList(); + } +} + +// Custom clipper for the reveal effect +class RevealClipper extends CustomClipper { + final double progress; + + RevealClipper(this.progress); + + @override + Path getClip(Size size) { + final path = Path(); + + if (progress <= 0.0) { + // At the start, clip everything + return path; + } else if (progress >= 1.0) { + // At the end, don't clip anything + path.addRect(Rect.fromLTWH(0, 0, size.width, size.height)); + return path; + } + + // Create a radial reveal effect + final center = Offset(size.width / 2, size.height / 2); + final maxRadius = + math.sqrt(size.width * size.width + size.height * size.height) / 2; + final radius = maxRadius * progress; + + path.addOval(Rect.fromCircle(center: center, radius: radius)); + return path; + } + + @override + bool shouldReclip(RevealClipper oldClipper) { + return oldClipper.progress != progress; + } +} + +// Custom widget for sparkles +class SparkleWidget extends StatelessWidget { + final double size; + final Color color; + + const SparkleWidget({ + Key? key, + required this.size, + required this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: size, + height: size, + child: CustomPaint( + painter: SparklePainter(color: color), + ), + ); + } +} + +// Custom painter for sparkle shape +class SparklePainter extends CustomPainter { + final Color color; + + SparklePainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + // Draw a star shape + final path = Path(); + const spikes = 4; + final outerRadius = size.width / 2; + final innerRadius = size.width / 5; + + for (var i = 0; i < spikes * 2; i++) { + final radius = i.isEven ? outerRadius : innerRadius; + final angle = math.pi * i / spikes; + final x = center.dx + radius * math.cos(angle); + final y = center.dy + radius * math.sin(angle); + + if (i == 0) { + path.moveTo(x, y); + } else { + path.lineTo(x, y); + } + } + path.close(); + + canvas.drawPath(path, paint); + + // Draw a subtle glow effect + final glowPaint = Paint() + ..color = color.withOpacity(0.3) + ..style = PaintingStyle.fill + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3); + + canvas.drawPath(path, glowPaint); + } + + @override + bool shouldRepaint(SparklePainter oldDelegate) { + return oldDelegate.color != color; + } +} + +class CertificateFront extends StatefulWidget { + final RecommendationItem recommendation; + final String certificateId; + + const CertificateFront({ + Key? key, + required this.recommendation, + required this.certificateId, + }) : super(key: key); + + @override + State createState() => _CertificateFrontState(); +} + +class _CertificateFrontState extends State { + @override + Widget build(BuildContext context) { + final Size screenSize = MediaQuery.of(context).size; + final DateFormat dateFormatter = DateFormat('dd MMMM yyyy'); + final String currentDate = dateFormatter.format(DateTime.now()); + final User? currentUser = FirebaseAuth.instance.currentUser; + + return LayoutBuilder( + builder: (context, constraints) { + final double certificateWidth = constraints.maxWidth; + // Menyesuaikan rasio aspek untuk mengurangi tinggi keseluruhan + final double certificateHeight = + certificateWidth * 0.6; // Rasio 10:6 lebih pendek dari 10:7 + + return Container( + width: certificateWidth, + height: certificateHeight, + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment(0.0, -0.4), + radius: 1.5, + colors: [ + Colors.blue.shade700, + Colors.blue.shade800, + Colors.indigo.shade900, + ], + stops: [0.0, 0.4, 0.9], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 10, + offset: Offset(0, 4), + ), + ], + ), + child: Stack( + children: [ + // Background pattern elements + Positioned.fill( + child: CustomPaint( + painter: CertificateBackgroundPainter(), + ), + ), + + // Certificate content + Padding( + padding: EdgeInsets.all(certificateWidth * 0.03), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo and Header + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + child: Text( + "EG", + style: TextStyle( + fontSize: certificateWidth * 0.035, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade900, + ), + ), + ), + SizedBox(width: 10), + Text( + "EduGuide", + style: TextStyle( + fontSize: certificateWidth * 0.045, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + shadows: [ + Shadow( + color: Colors.black.withOpacity(0.3), + offset: Offset(1, 1), + blurRadius: 3, + ), + ], + ), + ), + ], + ), + + SizedBox(height: certificateHeight * 0.01), + + // Certificate title + Text( + "SERTIFIKAT TES MINAT BAKAT", + style: TextStyle( + fontSize: certificateWidth * 0.033, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.5, + ), + ), + + Divider( + color: Colors.white.withOpacity(0.5), + thickness: 1, + indent: certificateWidth * 0.2, + endIndent: certificateWidth * 0.2, + height: certificateHeight * 0.04, + ), + + // Student name placeholder + Text( + "Dengan ini menyatakan bahwa", + style: TextStyle( + fontSize: certificateWidth * 0.03, + color: Colors.white.withOpacity(0.8), + fontStyle: FontStyle.italic, + ), + ), + + SizedBox(height: certificateHeight * 0.02), + + // Student name will be added dynamically + // Student name - Firebase integration + FutureBuilder( + // First, get the school ID from SharedPreferences + future: () async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('school_id') ?? ''; + }(), + builder: (context, schoolSnapshot) { + if (schoolSnapshot.connectionState == + ConnectionState.waiting) { + return Container( + width: 320, + height: 65, + decoration: BoxDecoration( + color: const Color(0xFF159895).withOpacity(0.07), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Color(0xFF1A5F7A)), + ), + ), + ), + ); + } + + // Once we have the school ID (or not), proceed + String schoolId = schoolSnapshot.data ?? ''; + + return FutureBuilder( + future: schoolId.isNotEmpty + // If we have a school ID, try to get the student directly + ? FirebaseFirestore.instance + .collection('schools') + .doc(schoolId) + .collection('students') + .doc(currentUser?.uid) + .get() + // If no school ID, find the student in all schools + : _findStudentInAllSchools(currentUser?.uid), + builder: (context, snapshot) { + String userName = "Siswa"; + String userClass = ""; + + if (snapshot.hasData && + snapshot.data != null && + snapshot.data!.exists) { + final userData = + snapshot.data!.data() as Map; + userName = userData['name'] ?? "Siswa"; + userClass = userData['class'] ?? ""; + } + + return Column( + children: [ + Stack( + alignment: Alignment.center, + children: [ + Container( + width: 320, + height: 65, + decoration: BoxDecoration( + color: const Color(0xFF159895) + .withOpacity(0.07), + borderRadius: BorderRadius.circular(12), + ), + ), + Text( + userName, + style: TextStyle( + fontSize: certificateWidth * 0.05, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + if (userClass.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + userClass, + style: TextStyle( + fontSize: certificateWidth * 0.03, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ); + }, + ); + }, + ), + + SizedBox(height: certificateHeight * 0.02), + + // Award information + Text( + "telah berhasil menyelesaikan Tes Minat Bakat", + style: TextStyle( + fontSize: certificateWidth * 0.03, + color: Colors.white.withOpacity(0.8), + ), + ), + + SizedBox(height: certificateHeight * 0.02), + + // Recommendation title with accent + Container( + padding: EdgeInsets.symmetric( + horizontal: certificateWidth * 0.03, + vertical: certificateHeight * 0.005, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 0.5, + ), + ), + child: Text( + widget.recommendation.title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: certificateWidth * 0.03, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + + SizedBox(height: certificateHeight * 0.01), + + // Recommended majors section - Lebih compact + + SizedBox(height: certificateHeight * 0.02), + + // Date and signature row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Date + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + currentDate, + style: TextStyle( + fontSize: certificateWidth * 0.02, + color: Colors.white, + ), + ), + Container( + width: certificateWidth * 0.15, + height: 1, + color: Colors.white.withOpacity(0.5), + margin: EdgeInsets.only(top: 3), + ), + Text( + "Tanggal", + style: TextStyle( + fontSize: certificateWidth * 0.018, + color: Colors.white.withOpacity(0.7), + ), + ), + ], + ), + + // Certificate ID + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "ID: ${widget.certificateId}", + style: TextStyle( + fontSize: certificateWidth * 0.018, + color: Colors.white.withOpacity(0.7), + ), + ), + ], + ), + + // Signature + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "EduGuide Team", + style: TextStyle( + fontSize: certificateWidth * 0.02, + fontFamily: 'Signature', + color: Colors.white, + ), + ), + Container( + width: certificateWidth * 0.15, + height: 1, + color: Colors.white.withOpacity(0.5), + margin: EdgeInsets.only(top: 3), + ), + Text( + "Tanda Tangan", + style: TextStyle( + fontSize: certificateWidth * 0.018, + color: Colors.white.withOpacity(0.7), + ), + ), + ], + ), + ], + ), + ], + ), + ), + + // Border decoration + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 2, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + + Future _findStudentInAllSchools(String? studentId) async { + if (studentId == null) return null; + + try { + // Get all schools + final schoolsCollection = + await FirebaseFirestore.instance.collection('schools').get(); + + // For each school, check if this student exists + for (var school in schoolsCollection.docs) { + final studentDoc = await FirebaseFirestore.instance + .collection('schools') + .doc(school.id) + .collection('students') + .doc(studentId) + .get(); + + if (studentDoc.exists) { + return studentDoc; + } + } + + return null; + } catch (e) { + debugPrint('Error finding student: $e'); + return null; + } + } +} + +// CustomPainter untuk membuat background pattern yang estetik +class CertificateBackgroundPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + // Background gradient overlay + final Rect fullRect = Rect.fromLTWH(0, 0, size.width, size.height); + final Paint gradientPaint = Paint() + ..shader = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withOpacity(0.05), + Colors.white.withOpacity(0.0), + Colors.white.withOpacity(0.07), + ], + stops: [0.0, 0.5, 1.0], + ).createShader(fullRect); + + canvas.drawRect(fullRect, gradientPaint); + + // Wavy pattern in the middle + final Path wavyPath = Path(); + final double amplitude = size.height * 0.02; + final double frequency = 0.1; + final double startY = size.height * 0.5; + + wavyPath.moveTo(0, startY); + for (double x = 0; x <= size.width; x += 1) { + double y = startY + amplitude * math.sin(frequency * x); + wavyPath.lineTo(x, y); + } + + canvas.drawPath( + wavyPath, + Paint() + ..color = Colors.white.withOpacity(0.1) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5); + + // Pattern 1: Abstract geometric shapes + // Diamond pattern + final double diamondSize = size.width * 0.04; + final double spacing = size.width * 0.15; + + for (double x = spacing; x < size.width; x += spacing) { + for (double y = spacing; y < size.height; y += spacing) { + // Only draw diamonds in certain areas to create a pattern + if ((x / spacing).round() % 3 == (y / spacing).round() % 3) { + final Path diamondPath = Path(); + diamondPath.moveTo(x, y - diamondSize); + diamondPath.lineTo(x + diamondSize, y); + diamondPath.lineTo(x, y + diamondSize); + diamondPath.lineTo(x - diamondSize, y); + diamondPath.close(); + + canvas.drawPath( + diamondPath, + Paint() + ..color = Colors.white.withOpacity(0.04) + ..style = PaintingStyle.fill); + + canvas.drawPath( + diamondPath, + Paint() + ..color = Colors.white.withOpacity(0.08) + ..style = PaintingStyle.stroke + ..strokeWidth = 0.5); + } + } + } + + // Pattern 2: Light rays emanating from center + final Paint rayPaint = Paint() + ..color = Colors.white.withOpacity(0.07) + ..style = PaintingStyle.stroke + ..strokeWidth = 0.8; + + final double centerX = size.width / 2; + final double centerY = size.height / 2; + + for (int i = 0; i < 24; i++) { + final double angle = (i * math.pi / 12); + final double length = math.max(size.width, size.height) * 0.6; + + canvas.drawLine( + Offset(centerX, centerY), + Offset(centerX + math.cos(angle) * length, + centerY + math.sin(angle) * length), + rayPaint); + } + + // Pattern 3: Subtle circle patterns + for (int i = 0; i < 30; i++) { + double radius = + math.Random().nextDouble() * size.width * 0.025 + size.width * 0.005; + double x = math.Random().nextDouble() * size.width; + double y = math.Random().nextDouble() * size.height; + + // Create glowing effect with multiple circles + for (int j = 0; j < 3; j++) { + double glowRadius = radius * (1 + j * 0.5); + double opacity = 0.02 / (j + 1); + + canvas.drawCircle( + Offset(x, y), + glowRadius, + Paint() + ..color = Colors.white.withOpacity(opacity) + ..style = PaintingStyle.fill); + } + + canvas.drawCircle( + Offset(x, y), + radius, + Paint() + ..color = Colors.white.withOpacity(0.05) + ..style = PaintingStyle.fill); + } + + // Decorative border with ornate corners + final double cornerSize = size.width * 0.12; + final double borderWidth = 2.0; + + // Create a fancy corner design + void drawOrnateCorner(double startX, double startY, double endX, + double endY, double controlX, double controlY) { + final Path cornerPath = Path(); + cornerPath.moveTo(startX, startY); + cornerPath.quadraticBezierTo(controlX, controlY, endX, endY); + + canvas.drawPath( + cornerPath, + Paint() + ..color = Colors.white.withOpacity(0.25) + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth); + + // Add small circle decoration at corner point + canvas.drawCircle( + Offset(controlX, controlY), + borderWidth * 1.5, + Paint() + ..color = Colors.white.withOpacity(0.3) + ..style = PaintingStyle.fill); + } + + // Top left corner + drawOrnateCorner( + 0, cornerSize, cornerSize, 0, cornerSize * 0.3, cornerSize * 0.3); + + // Top right corner + drawOrnateCorner(size.width - cornerSize, 0, size.width, cornerSize, + size.width - cornerSize * 0.3, cornerSize * 0.3); + + // Bottom right corner + drawOrnateCorner( + size.width, + size.height - cornerSize, + size.width - cornerSize, + size.height, + size.width - cornerSize * 0.3, + size.height - cornerSize * 0.3); + + // Bottom left corner + drawOrnateCorner(cornerSize, size.height, 0, size.height - cornerSize, + cornerSize * 0.3, size.height - cornerSize * 0.3); + + // Add decorative elements at each corner + void drawCornerDecoration( + double x, double y, double size, double rotation) { + canvas.save(); + canvas.translate(x, y); + canvas.rotate(rotation); + + final Path decorPath = Path(); + decorPath.moveTo(0, -size); + decorPath.lineTo(size * 0.5, 0); + decorPath.lineTo(0, size); + decorPath.lineTo(-size * 0.5, 0); + decorPath.close(); + + canvas.drawPath( + decorPath, + Paint() + ..color = Colors.white.withOpacity(0.2) + ..style = PaintingStyle.fill); + + canvas.drawPath( + decorPath, + Paint() + ..color = Colors.white.withOpacity(0.3) + ..style = PaintingStyle.stroke + ..strokeWidth = 0.8); + + canvas.restore(); + } + + final double decorSize = cornerSize * 0.3; + + // Corner decorations + drawCornerDecoration(cornerSize * 0.5, cornerSize * 0.5, decorSize, 0); + drawCornerDecoration(size.width - cornerSize * 0.5, cornerSize * 0.5, + decorSize, math.pi * 0.5); + drawCornerDecoration(size.width - cornerSize * 0.5, + size.height - cornerSize * 0.5, decorSize, math.pi); + drawCornerDecoration(cornerSize * 0.5, size.height - cornerSize * 0.5, + decorSize, math.pi * 1.5); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +// Enhanced background painter for front certificate +class EnhancedCertificateBackgroundPainter extends CustomPainter { + final Color primaryColor; + final Color secondaryColor; + + EnhancedCertificateBackgroundPainter({ + required this.primaryColor, + required this.secondaryColor, + }); + + @override + void paint(Canvas canvas, Size size) { + // Base background color + final Paint bgPaint = Paint()..color = Colors.white; + canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), bgPaint); + + // Create a soft gradient background + final Rect rect = Rect.fromLTRB(0, 0, size.width, size.height); + final Gradient gradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + Colors.blue.shade50, + Colors.indigo.shade50, + Colors.white, + ], + stops: const [0.0, 0.3, 0.7, 1.0], + ); + + final Paint gradientPaint = Paint() + ..shader = gradient.createShader(rect) + ..style = PaintingStyle.fill; + + canvas.drawRect(rect, gradientPaint); + + // Draw elegant dot pattern + final dotPaint = Paint() + ..color = primaryColor.withOpacity(0.1) + ..style = PaintingStyle.fill; + + // Small dots pattern - create a grid of dots + for (int i = 0; i < size.width; i += 20) { + for (int j = 0; j < size.height; j += 20) { + // Draw tiny circles + canvas.drawCircle( + Offset(i.toDouble(), j.toDouble()), + 0.8, + dotPaint, + ); + } + } + + // Add decorative ribbons at the top + final ribbonPaint = Paint() + ..color = primaryColor.withOpacity(0.08) + ..style = PaintingStyle.fill; + + final topRibbonPath = Path(); + topRibbonPath.moveTo(0, 0); + topRibbonPath.lineTo(size.width, 0); + topRibbonPath.lineTo(size.width, 60); + topRibbonPath.quadraticBezierTo( + size.width * 0.75, 50, size.width * 0.5, 60); + topRibbonPath.quadraticBezierTo(size.width * 0.25, 70, 0, 60); + topRibbonPath.close(); + canvas.drawPath(topRibbonPath, ribbonPaint); + + // Add decorative ribbons at the bottom + final bottomRibbonPath = Path(); + bottomRibbonPath.moveTo(0, size.height); + bottomRibbonPath.lineTo(size.width, size.height); + bottomRibbonPath.lineTo(size.width, size.height - 60); + bottomRibbonPath.quadraticBezierTo(size.width * 0.75, size.height - 50, + size.width * 0.5, size.height - 60); + bottomRibbonPath.quadraticBezierTo( + size.width * 0.25, size.height - 70, 0, size.height - 60); + bottomRibbonPath.close(); + canvas.drawPath(bottomRibbonPath, ribbonPaint); + + // Draw decorative wave patterns + final wavePaint = Paint() + ..color = secondaryColor.withOpacity(0.08) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + // Multiple wave lines for richer effect + // Top waves + for (int offset = 0; offset < 3; offset++) { + final topWavePath = Path(); + topWavePath.moveTo(0, 100 + (offset * 15)); + + for (int i = 0; i < size.width / 40; i++) { + topWavePath.quadraticBezierTo( + (i * 40) + 20, + 90 + (offset * 15) + (i % 2 == 0 ? 5 : 0), + (i * 40) + 40, + 100 + (offset * 15), + ); + } + canvas.drawPath(topWavePath, wavePaint); + } + + // Bottom waves + for (int offset = 0; offset < 3; offset++) { + final bottomWavePath = Path(); + bottomWavePath.moveTo(0, size.height - 100 - (offset * 15)); + + for (int i = 0; i < size.width / 40; i++) { + bottomWavePath.quadraticBezierTo( + (i * 40) + 20, + size.height - 90 - (offset * 15) - (i % 2 == 0 ? 5 : 0), + (i * 40) + 40, + size.height - 100 - (offset * 15), + ); + } + canvas.drawPath(bottomWavePath, wavePaint); + } + + // Add decorative accent lines + final accentPaint = Paint() + ..color = primaryColor.withOpacity(0.2) + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + + // Add diagonal accent lines in corners + // Top left + canvas.drawLine( + Offset(0, 0), + Offset(size.width * 0.2, size.height * 0.2), + accentPaint, + ); + + // Top right + canvas.drawLine( + Offset(size.width, 0), + Offset(size.width * 0.8, size.height * 0.2), + accentPaint, + ); + + // Bottom left + canvas.drawLine( + Offset(0, size.height), + Offset(size.width * 0.2, size.height * 0.8), + accentPaint, + ); + + // Bottom right + canvas.drawLine( + Offset(size.width, size.height), + Offset(size.width * 0.8, size.height * 0.8), + accentPaint, + ); + + // Add decorative circular elements + final circlePaint = Paint() + ..style = PaintingStyle.stroke + ..color = primaryColor.withOpacity(0.15) + ..strokeWidth = 1.5; + + // Draw circles in the corners + canvas.drawCircle(Offset(0, 0), 100, circlePaint); + canvas.drawCircle(Offset(size.width, 0), 100, circlePaint); + canvas.drawCircle(Offset(0, size.height), 100, circlePaint); + canvas.drawCircle(Offset(size.width, size.height), 100, circlePaint); + + // Create radial gradient effect in corners for added depth + final cornerGradient = RadialGradient( + colors: [ + primaryColor.withOpacity(0.1), + Colors.white.withOpacity(0.0), + ], + ); + + final cornerPaint1 = Paint() + ..shader = cornerGradient + .createShader(Rect.fromCircle(center: Offset(0, 0), radius: 200)); + canvas.drawCircle(Offset(0, 0), 200, cornerPaint1); + + final cornerPaint2 = Paint() + ..shader = cornerGradient.createShader( + Rect.fromCircle(center: Offset(size.width, 0), radius: 200)); + canvas.drawCircle(Offset(size.width, 0), 200, cornerPaint2); + + final cornerPaint3 = Paint() + ..shader = cornerGradient.createShader( + Rect.fromCircle(center: Offset(0, size.height), radius: 200)); + canvas.drawCircle(Offset(0, size.height), 200, cornerPaint3); + + final cornerPaint4 = Paint() + ..shader = cornerGradient.createShader(Rect.fromCircle( + center: Offset(size.width, size.height), radius: 200)); + canvas.drawCircle(Offset(size.width, size.height), 200, cornerPaint4); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +// Corner Element Painter +class CornerElementPainter extends CustomPainter { + final Color primaryColor; + final Color secondaryColor; + + CornerElementPainter({ + required this.primaryColor, + this.secondaryColor = Colors.transparent, + }); + + @override + void paint(Canvas canvas, Size size) { + // Main outline + final outlinePaint = Paint() + ..color = primaryColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1.8; + + // Draw decorative corner element with more details + final path = Path(); + path.moveTo(0, size.height * 0.8); + path.lineTo(0, 0); + path.lineTo(size.width * 0.8, 0); + + // Add fancy corner flourish + path.moveTo(0, size.height * 0.4); + path.quadraticBezierTo( + size.width * 0.05, + size.height * 0.35, + size.width * 0.2, + size.height * 0.2, + ); + path.quadraticBezierTo( + size.width * 0.25, + size.height * 0.15, + size.width * 0.4, + 0, + ); + + // Add another decorative curve + path.moveTo(size.width * 0.2, 0); + path.quadraticBezierTo( + size.width * 0.15, + size.height * 0.15, + 0, + size.height * 0.3, + ); + + canvas.drawPath(path, outlinePaint); + + // Add decorative circles + final fillPaint = Paint() + ..color = primaryColor + ..style = PaintingStyle.fill; + + // Add main decorative circle + canvas.drawCircle( + Offset(size.width * 0.5, size.height * 0.5), + size.width * 0.05, + fillPaint, + ); + + // Add smaller decorative circles + canvas.drawCircle( + Offset(size.width * 0.3, size.height * 0.3), + size.width * 0.02, + fillPaint, + ); + + canvas.drawCircle( + Offset(size.width * 0.7, size.height * 0.3), + size.width * 0.02, + fillPaint, + ); + + canvas.drawCircle( + Offset(size.width * 0.3, size.height * 0.7), + size.width * 0.02, + fillPaint, + ); + + canvas.drawCircle( + Offset(size.width * 0.7, size.height * 0.7), + size.width * 0.02, + fillPaint, + ); + + // Add thin line flourishes + final thinPaint = Paint() + ..color = primaryColor.withOpacity(0.5) + ..style = PaintingStyle.stroke + ..strokeWidth = 0.8; + + // Diagonal line + canvas.drawLine( + Offset(0, 0), + Offset(size.width, size.height), + thinPaint, + ); + + // Cross lines + canvas.drawLine( + Offset(size.width * 0.3, size.height * 0.5), + Offset(size.width * 0.7, size.height * 0.5), + thinPaint, + ); + + canvas.drawLine( + Offset(size.width * 0.5, size.height * 0.3), + Offset(size.width * 0.5, size.height * 0.7), + thinPaint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +class DecorativeBorderPainter extends CustomPainter { + final Color primaryColor; + final Color secondaryColor; + final double borderWidth; + + DecorativeBorderPainter({ + required this.primaryColor, + required this.secondaryColor, + this.borderWidth = 3.0, + }); + + @override + void paint(Canvas canvas, Size size) { + final mainBorderPaint = Paint() + ..color = primaryColor + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth; + + // Outer border with fancy corners + final outerPath = Path(); + + // Top-left corner + outerPath.moveTo(size.width * 0.05, 0); + outerPath.lineTo(size.width * 0.95, 0); + + // Top-right corner with flourish + outerPath.quadraticBezierTo(size.width, 0, size.width, size.height * 0.05); + outerPath.lineTo(size.width, size.height * 0.95); + + // Bottom-right corner with flourish + outerPath.quadraticBezierTo( + size.width, size.height, size.width * 0.95, size.height); + outerPath.lineTo(size.width * 0.05, size.height); + + // Bottom-left corner with flourish + outerPath.quadraticBezierTo(0, size.height, 0, size.height * 0.95); + outerPath.lineTo(0, size.height * 0.05); + + // Back to top-left with flourish + outerPath.quadraticBezierTo(0, 0, size.width * 0.05, 0); + + canvas.drawPath(outerPath, mainBorderPaint); + + // Inner border + final innerBorderPaint = Paint() + ..color = secondaryColor.withOpacity(0.7) + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth * 0.5; + + final padding = borderWidth * 4; + final innerRect = Rect.fromLTRB( + padding, padding, size.width - padding, size.height - padding); + + // Draw rounded rect for inner border + canvas.drawRRect( + RRect.fromRectAndRadius(innerRect, Radius.circular(padding * 0.5)), + innerBorderPaint); + + // Add decorative corners + final cornerSize = padding * 1.5; + + // Draw corner accents + _drawCornerAccent( + canvas, Offset(padding, padding), cornerSize, primaryColor); + _drawCornerAccent( + canvas, Offset(size.width - padding, padding), cornerSize, primaryColor, + angle: 1.57); + _drawCornerAccent( + canvas, + Offset(size.width - padding, size.height - padding), + cornerSize, + primaryColor, + angle: 3.14); + _drawCornerAccent(canvas, Offset(padding, size.height - padding), + cornerSize, primaryColor, + angle: 4.71); + + // Add subtle gradient overlay to the borders + final gradientPaint = Paint() + ..shader = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + primaryColor.withOpacity(0.1), + secondaryColor.withOpacity(0.1), + primaryColor.withOpacity(0.1), + ], + stops: const [0.0, 0.5, 1.0], + ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)) + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth * 2; + + // Draw gradient overlay along borders + canvas.drawPath(outerPath, gradientPaint); + } + + void _drawCornerAccent( + Canvas canvas, Offset position, double size, Color color, + {double angle = 0}) { + canvas.save(); + canvas.translate(position.dx, position.dy); + + if (angle != 0) { + canvas.rotate(angle); + } + + final accentPaint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth * 0.3; + + final accentPath = Path(); + accentPath.moveTo(0, size * 0.5); + accentPath.lineTo(0, 0); + accentPath.lineTo(size * 0.5, 0); + + // Add flourish + accentPath.moveTo(size * 0.2, 0); + accentPath.quadraticBezierTo(size * 0.1, size * 0.1, 0, size * 0.2); + + canvas.drawPath(accentPath, accentPaint); + + // Add small dot decoration + final dotPaint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + canvas.drawCircle( + Offset(size * 0.3, size * 0.3), borderWidth * 0.4, dotPaint); + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +// Decorative Edges Painter - not using this anymore to avoid visual clutter +class DecorativeEdgesPainter extends CustomPainter { + final Color primaryColor; + final Color secondaryColor; + + DecorativeEdgesPainter({ + required this.primaryColor, + required this.secondaryColor, + }); + + @override + void paint(Canvas canvas, Size size) { + // Not implementing to reduce visual elements + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} + +class CertificateBack extends StatefulWidget { + final RecommendationItem recommendation; + + const CertificateBack({ + Key? key, + required this.recommendation, + }) : super(key: key); + + @override + State createState() => _CertificateBackState(); +} + +class _CertificateBackState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + + // Setup animations + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ); + + _fadeAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeIn, + ); + + // Start animations + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment(0.0, -0.4), + radius: 1.5, + colors: [ + Colors.blue.shade700, + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 15, + spreadRadius: 3, + ), + ], + ), + child: Stack( + children: [ + // Background with lighter pattern + Positioned.fill( + child: CustomPaint( + painter: CertificateBackgroundPainter(), + ), + ), + // Border + Positioned.fill( + child: Container( + margin: const EdgeInsets.all(24), + decoration: BoxDecoration( + border: Border.all( + width: 3, + color: const Color(0xFFd9dbe6), + ), + borderRadius: BorderRadius.circular(4), + ), + ), + ), + + // Content + Padding( + padding: const EdgeInsets.fromLTRB(50.0, 50.0, 50.0, 30.0), + child: FadeTransition( + opacity: _fadeAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + 'DETAIL REKOMENDASI', + style: GoogleFonts.montserrat( + fontSize: 22, + fontWeight: FontWeight.bold, + color: const Color(0xFFd9dbe6), + letterSpacing: 2, + ), + ), + ), + const SizedBox(height: 8), + Center( + child: Container( + width: 150, + height: 3, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFd9dbe6), Color(0xFF57C5B6)], + ), + borderRadius: BorderRadius.circular(1.5), + ), + ), + ), + const SizedBox(height: 20), + + // Recommendation title + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 0.5, + ), + ), + child: Text( + widget.recommendation.title, + style: GoogleFonts.montserrat( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 20), + + // Scroll view for content to prevent overflow + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + + // Two columns layout for careers and majors + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Careers + Expanded( + child: _buildDetailSection( + 'Karir yang Cocok:', + widget.recommendation.careers, + const Icon(Icons.work, + color: Color(0xFFd9dbe6)), + ), + ), + const SizedBox(width: 30), + // Majors + Expanded( + child: _buildDetailSection( + 'Jurusan yang Cocok:', + widget.recommendation.majors, + const Icon(Icons.school, + color: Color(0xFFd9dbe6)), + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Recommended courses and universities if available + if (widget.recommendation.recommendedCourses != + null || + widget.recommendation.recommendedUniversities != + null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.recommendation.recommendedCourses != + null) + Expanded( + child: _buildDetailSection( + 'Mata Kuliah yang Cocok:', + widget.recommendation.recommendedCourses!, + const Icon(Icons.menu_book, + color: Color(0xFFd9dbe6)), + ), + ), + if (widget.recommendation.recommendedCourses != + null && + widget.recommendation + .recommendedUniversities != + null) + const SizedBox(width: 30), + if (widget.recommendation + .recommendedUniversities != + null) + Expanded( + child: _buildDetailSection( + 'Universitas yang Cocok:', + widget.recommendation + .recommendedUniversities!, + const Icon(Icons.account_balance, + color: Color(0xFFd9dbe6)), + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Footer + Center( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFd9dbe6).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: + const Color(0xFFd9dbe6).withOpacity(0.3), + ), + ), + child: Text( + 'Hasil analisis ini disusun dari jawaban dan minat yang kamu berikan.\n' + 'Gunakan info ini untuk membantu merencanakan masa depanmu dengan lebih baik.', + textAlign: TextAlign.center, + style: GoogleFonts.montserrat( + fontSize: 12, + fontStyle: FontStyle.italic, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + + // Decorative corners + Positioned( + top: 35, + left: 35, + child: _buildDetailCorner(), + ), + Positioned( + top: 35, + right: 35, + child: Transform.rotate( + angle: 1.57, + child: _buildDetailCorner(), + ), + ), + Positioned( + bottom: 35, + right: 35, + child: Transform.rotate( + angle: 3.14, + child: _buildDetailCorner(), + ), + ), + Positioned( + bottom: 35, + left: 35, + child: Transform.rotate( + angle: 4.71, + child: _buildDetailCorner(), + ), + ), + ], + ), + ); + } + + // Helper to simplify rules text + List _simplifyRules(List rules) { + List simplifiedRules = []; + + for (String rule in rules) { + // Replace common complex phrases with simpler alternatives + String simplified = rule + .replaceAll('direkomendasikan', 'cocok') + .replaceAll('berdasarkan analisis', 'karena') + .replaceAll('memiliki ketertarikan yang tinggi', 'tertarik') + .replaceAll('memiliki kemampuan yang baik', 'kamu mampu') + .replaceAll('menunjukkan minat yang kuat', 'kamu suka') + .replaceAll('berdasarkan jawaban anda', 'dari jawabanmu') + .replaceAll('mempunyai potensi untuk', 'bisa') + .replaceAll('sangat sesuai dengan', 'cocok dengan'); + + // Make first letter uppercase if needed + if (simplified.isNotEmpty) { + simplified = simplified[0].toUpperCase() + simplified.substring(1); + } + + simplifiedRules.add(simplified); + } + + return simplifiedRules; + } + + Widget _buildDetailSection(String title, List items, Icon icon) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + icon, + const SizedBox(width: 6), + Expanded( + child: Text( + title, + style: GoogleFonts.montserrat( + fontSize: 15, + fontWeight: FontWeight.w600, + color: const Color(0xFFd9dbe6), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + ...items + .map((item) => Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '• ', + style: GoogleFonts.montserrat( + fontSize: 14, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Expanded( + child: Text( + item, + overflow: TextOverflow.visible, + style: GoogleFonts.montserrat( + fontSize: 13, + color: Colors.white, + height: 1.4, + ), + ), + ), + ], + ), + )) + .toList(), + ], + ); + } + + Widget _buildDetailCorner() { + return Container( + width: 24, + height: 24, + decoration: BoxDecoration( + border: Border( + left: BorderSide(width: 3, color: const Color(0xFFd9dbe6)), + top: BorderSide(width: 3, color: const Color(0xFFd9dbe6)), + ), + ), + ); + } +} + +class FloatingParticlesPainter extends CustomPainter { + final List particles; + final double animation; // 0.0 to 1.0 + + FloatingParticlesPainter({ + required this.particles, + required this.animation, + }); + + @override + void paint(Canvas canvas, Size size) { + for (var particle in particles) { + // Calculate current position with animation + // Make particles float upward and wiggle a bit + final y = (particle.position.dy - (animation * particle.speed)) % 1.0; + final wiggle = + math.sin(animation * 2 * math.pi + particle.position.dx * 10) * 0.02; + final x = (particle.position.dx + wiggle) % 1.0; + + // Draw the particle + final paint = Paint() + ..color = particle.color + ..style = PaintingStyle.fill; + + canvas.drawCircle( + Offset(x * size.width, y * size.height), + particle.size, + paint, + ); + } + } + + @override + bool shouldRepaint(FloatingParticlesPainter oldDelegate) { + return oldDelegate.animation != animation; + } +} + +class CertificateReccomendationPage extends StatefulWidget { + final RecommendationResult result; + final String rawMessage; + const CertificateReccomendationPage({ + required this.result, + this.rawMessage = '', + Key? key, + }) : super(key: key); + + @override + State createState() => + _CertificateReccomendationPageState(); +} + +class _CertificateReccomendationPageState + extends State with TickerProviderStateMixin { + final GlobalKey _certificateFrontKey = GlobalKey(); + final GlobalKey _certificateBackKey = GlobalKey(); + final PageController _pageController = PageController(); + + bool _isGenerating = false; + bool _isRevealed = false; + int _currentPage = 0; + String _certificateId = ''; + + // A4 Landscape dimensions in pixels (assuming 96 DPI) + // 297mm x 210mm + final double _a4Width = 1123; // 297mm at 96 DPI + final double _a4Height = 794; // 210mm at 96 DPI + + late AnimationController _pageIndicatorController; + late AnimationController _floatingParticlesController; + + // For additional floating particles effect + final List _particles = []; + + @override + void initState() { + super.initState(); + _generateCertificateId(); + + _pageIndicatorController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + + _floatingParticlesController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 6000), + )..repeat(); + + _pageController.addListener(() { + // Update the page indicator animation + if (_pageController.page != null) { + _pageIndicatorController.value = _pageController.page!; + } + + // Update current page for UI state + if (_pageController.page != null && + _pageController.page!.round() != _currentPage) { + setState(() { + _currentPage = _pageController.page!.round(); + }); + } + }); + + // Initialize floating particles + _initializeParticles(); + } + + void _initializeParticles() { + // Generate 15 random particles + for (int i = 0; i < 15; i++) { + _particles.add( + ParticleModel( + position: Offset( + math.Random().nextDouble(), + math.Random().nextDouble(), + ), + speed: 0.2 + math.Random().nextDouble() * 0.4, // between 0.2 and 0.6 + size: 3 + math.Random().nextDouble() * 7, // between 3 and 10 + color: const Color(0xFF57C5B6).withOpacity( + 0.1 + math.Random().nextDouble() * 0.3, // between 0.1 and 0.4 + ), + ), + ); + } + } + + // Generate a unique certificate ID + void _generateCertificateId() { + final now = DateTime.now(); + final timestamp = now.millisecondsSinceEpoch.toString().substring(6); + _certificateId = 'CERT-$timestamp'; + } + + @override + void dispose() { + _pageController.dispose(); + _pageIndicatorController.dispose(); + _floatingParticlesController.dispose(); + super.dispose(); + } + + // Capture the certificate using its RenderRepaintBoundary + Future _captureCertificate(GlobalKey key) async { + try { + // Ensure the build is complete + await Future.delayed(const Duration(milliseconds: 500)); + + final context = key.currentContext; + if (context == null) { + return null; + } + + final renderObject = context.findRenderObject(); + if (renderObject == null || !renderObject.attached) { + return null; + } + + final boundary = renderObject as RenderRepaintBoundary; + final image = await boundary.toImage(pixelRatio: 3.0); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + + if (byteData != null) { + return byteData.buffer.asUint8List(); + } + return null; + } catch (e) { + debugPrint('Error capturing certificate: $e'); + return null; + } + } + + // Save both certificate front and back to gallery + Future _saveCertificate() async { + setState(() { + _isGenerating = true; + }); + + try { + final timestamp = DateTime.now().millisecondsSinceEpoch; + final title = + widget.result.recommendations.first.title.replaceAll("|", "_"); + bool frontSuccess = false; + bool backSuccess = false; + + // First capture front of certificate (go to page 0) + if (_currentPage != 0) { + await _pageController.animateToPage(0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut); + await Future.delayed(const Duration(milliseconds: 500)); + } + + final frontBytes = await _captureCertificate(_certificateFrontKey); + if (frontBytes != null) { + final resultFront = await ImageGallerySaverPlus.saveImage(frontBytes, + quality: 100, name: "certificate_front_${title}_$timestamp"); + frontSuccess = resultFront['isSuccess'] ?? false; + } + + // Then capture back of certificate (go to page 1) + await _pageController.animateToPage(1, + duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + await Future.delayed(const Duration(milliseconds: 500)); + + final backBytes = await _captureCertificate(_certificateBackKey); + if (backBytes != null) { + final resultBack = await ImageGallerySaverPlus.saveImage(backBytes, + quality: 100, name: "certificate_back_${title}_$timestamp"); + backSuccess = resultBack['isSuccess'] ?? false; + } + + // Show result + String message; + Color backgroundColor; + + if (frontSuccess && backSuccess) { + message = 'Kedua sisi sertifikat berhasil disimpan di galeri'; + backgroundColor = Colors.green; + } else if (frontSuccess) { + message = 'Hanya bagian depan sertifikat yang berhasil disimpan'; + backgroundColor = Colors.orange; + } else if (backSuccess) { + message = 'Hanya bagian belakang sertifikat yang berhasil disimpan'; + backgroundColor = Colors.orange; + } else { + message = 'Gagal menyimpan sertifikat'; + backgroundColor = Colors.red; + } + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: backgroundColor, + behavior: SnackBarBehavior.floating, + ), + ); + } catch (e) { + debugPrint('Error in saving certificate: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Terjadi kesalahan saat menyimpan sertifikat'), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + ), + ); + } + } finally { + setState(() { + _isGenerating = false; + }); + } + } + + // Share certificate + Future _shareCertificate() async { + try { + setState(() { + _isGenerating = true; + }); + + final tempDir = await getTemporaryDirectory(); + final List filesToShare = []; + + // First capture front of certificate (go to page 0) + if (_currentPage != 0) { + await _pageController.animateToPage(0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut); + await Future.delayed(const Duration(milliseconds: 500)); + } + + final frontBytes = await _captureCertificate(_certificateFrontKey); + if (frontBytes != null) { + final frontPath = '${tempDir.path}/certificate_front.png'; + final frontFile = File(frontPath); + await frontFile.writeAsBytes(frontBytes); + filesToShare.add(XFile(frontPath)); + } + + // Then capture back of certificate (go to page 1) + await _pageController.animateToPage(1, + duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + await Future.delayed(const Duration(milliseconds: 500)); + + final backBytes = await _captureCertificate(_certificateBackKey); + if (backBytes != null) { + final backPath = '${tempDir.path}/certificate_back.png'; + final backFile = File(backPath); + await backFile.writeAsBytes(backBytes); + filesToShare.add(XFile(backPath)); + } + + if (filesToShare.isNotEmpty) { + // Share files + await Share.shareXFiles(filesToShare, + text: 'Sertifikat Hasil Analisis Minat dan Bakat'); + } else { + _showErrorSnackBar('Gagal membuat sertifikat untuk dibagikan'); + } + } catch (e) { + _showErrorSnackBar('Terjadi kesalahan: ${e.toString()}'); + } finally { + setState(() { + _isGenerating = false; + }); + } + } + + void _showErrorSnackBar(String message) { + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + ), + ); + } + + @override + Widget build(BuildContext context) { + final recommendation = widget.result.recommendations.first; + + return Scaffold( + appBar: AppBar( + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0.0, 0.5), + end: Offset.zero, + ).animate(animation), + child: child, + ), + ); + }, + child: Text( + _currentPage == 0 + ? 'Sertifikat Hasil Analisis' + : 'Detail Rekomendasi', + key: ValueKey(_currentPage), + style: TextStyle( + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + ), + ), + backgroundColor: Colors.indigo.shade800, + foregroundColor: Colors.white, + elevation: 0, + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.blue.shade700, + Colors.indigo.shade800, + Colors.blue.shade700, + ], + stops: [0.0, 0.5, 1.0], + ), + ), + child: Stack( + children: [ + // Background decorative elements + Positioned.fill( + child: CustomPaint( + painter: EnhancedBackgroundPainter(), + ), + ), + + // Floating particles animation + Positioned.fill( + child: AnimatedBuilder( + animation: _floatingParticlesController, + builder: (context, child) { + return CustomPaint( + painter: FloatingParticlesPainter( + particles: _particles, + animation: _floatingParticlesController.value, + ), + size: MediaQuery.of(context).size, + ); + }, + ), + ), + + // Content + Column( + children: [ + // Page indicator + Container( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildPageIndicator(0), + const SizedBox(width: 8), + _buildPageIndicator(1), + ], + ), + ), + + // Main content + Expanded( + child: !_isRevealed + ? _buildRevealContainer() + : _buildPageView(recommendation), + ), + + // Action buttons + _buildActionButtons(), + ], + ), + ], + ), + ), + ); + } + + Widget _buildRevealContainer() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Pre-reveal animation + Container( + width: 220, + height: 220, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade100, + Colors.white, + ], + ), + boxShadow: [ + BoxShadow( + color: Colors.blue.shade200.withOpacity(0.3), + blurRadius: 15, + spreadRadius: 5, + ), + BoxShadow( + color: Colors.indigo.shade200.withOpacity(0.2), + blurRadius: 20, + spreadRadius: 0, + offset: Offset(10, 10), + ), + ], + ), + child: Center( + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 1500), + curve: Curves.easeInOut, + builder: (context, value, child) { + return Transform.scale( + scale: 0.5 + (value * 0.5), + child: Opacity( + opacity: value, + child: Stack( + alignment: Alignment.center, + children: [ + // Glow effect behind the certificate + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.blue.shade400 + .withOpacity(0.2 * value), + blurRadius: 30 * value, + spreadRadius: 10 * value, + ), + ], + ), + ), + + // Medal icon with gradient + ShaderMask( + shaderCallback: (bounds) => LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade700, + Colors.indigo.shade800, + Colors.blue.shade700, + ], + ).createShader(bounds), + child: Icon( + Icons.workspace_premium, + size: 105, + color: Colors.white, + ), + ), + + // Shine effect + AnimatedBuilder( + animation: _floatingParticlesController, + builder: (context, child) { + return Positioned( + top: 25 + + (10 * + math.sin( + _floatingParticlesController.value * + math.pi * + 2)), + right: 25 + + (5 * + math.cos( + _floatingParticlesController.value * + math.pi * + 2)), + child: Container( + width: 15, + height: 15, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(0.9), + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity(0.5), + blurRadius: 5, + spreadRadius: 2, + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: _isFormOpened + ? null + : () { + setState(() { + _isRevealed = true; + }); + _launchFormURL(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo.shade900, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 8, + shadowColor: Colors.indigo.withOpacity(0.5), + // Apply a disabled style when the form has been opened + disabledBackgroundColor: Colors.grey.shade400, + disabledForegroundColor: Colors.grey.shade700, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _isRevealed ? Icons.check_circle : Icons.visibility, + size: 22, + ), + SizedBox(width: 8), + Text( + _isFormOpened ? 'Form Telah Dibuka' : 'Tampilkan Sertifikat', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ) + ], + ), + ); + } + + bool _isFormOpened = false; + + Future _launchFormURL() async { + setState(() { + _isFormOpened = true; + }); + } + + Widget _buildPageView(RecommendationItem recommendation) { + return PageView( + controller: _pageController, + physics: const PagePhysics(), + children: [ + // Front certificate + Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: FittedBox( + fit: BoxFit.contain, + child: SizedBox( + width: _a4Width, + height: _a4Height, + child: MagicRevealAnimation( + child: RepaintBoundary( + key: _certificateFrontKey, + child: CertificateFront( + recommendation: recommendation, + certificateId: _certificateId, + ), + ), + ), + ), + ), + ), + ), + ), + + // Back certificate + Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: FittedBox( + fit: BoxFit.contain, + child: SizedBox( + width: _a4Width, + height: _a4Height, + child: RepaintBoundary( + key: _certificateBackKey, + child: CertificateBack( + recommendation: recommendation, + ), + ), + ), + ), + ), + ), + ), + ], + ); + } + + Widget _buildPageIndicator(int pageIndex) { + final isActive = _currentPage == pageIndex; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: isActive ? 24.0 : 12.0, + height: 8.0, + decoration: BoxDecoration( + color: isActive + ? Colors.blue.shade800 + : Colors.indigo.shade900.withOpacity(0.3), + borderRadius: BorderRadius.circular(4.0), + boxShadow: isActive + ? [ + BoxShadow( + color: Colors.blue.shade800.withOpacity(0.3), + blurRadius: 4, + spreadRadius: 1, + ), + ] + : null, + ), + ); + } + + Widget _buildActionButtons() { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + decoration: BoxDecoration( + color: Colors.transparent, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, -3), + ), + ], + ), + child: SafeArea( + child: !_isRevealed + ? const SizedBox.shrink() // Hide buttons before reveal + : CertificateActions( + onSave: () { + _saveCertificate(); + }, + onShare: () { + _shareCertificate(); + }, + )), + ); + } +} + +class CertificateActions extends StatefulWidget { + final Function() onSave; + final Function() onShare; + + const CertificateActions({ + Key? key, + required this.onSave, + required this.onShare, + }) : super(key: key); + + @override + State createState() => _CertificateActionsState(); +} + +class _CertificateActionsState extends State { + bool _isRevealed = + true; // Assuming certificate is already revealed in your case + bool _isGenerating = false; + bool _hasCompletedUAT = false; + + // UAT form URL + final Uri uatFormUrl = Uri.parse( + 'https://docs.google.com/forms/d/e/1FAIpQLSd9JjQS66LHkHOcbpiyZj7yObHTPZf3Sjhc0UAytjyPMExfcw/viewform?fbzx=-371784723675406593'); + + // Function to launch the UAT form with countdown + Future _showUATDialog() async { + int countdown = 5; + + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setState) { + // Start the countdown timer + Timer.periodic(const Duration(seconds: 1), (timer) { + if (countdown > 0) { + setState(() { + countdown--; + }); + } else { + timer.cancel(); + _launchUATForm(); + } + }); + + return AlertDialog( + title: const Text( + "Kuisioner UAT", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.indigo, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Apakah Anda sudah mengisi kuisioner UAT (User Acceptance Test)? Kuisioner ini penting untuk membantu menilai apakah aplikasi dapat berjalan dengan baik.", + style: TextStyle(fontSize: 16), + ), + const SizedBox(height: 20), + LinearProgressIndicator( + value: (3 - countdown) / 3, + backgroundColor: Colors.grey.shade200, + valueColor: AlwaysStoppedAnimation(Colors.indigo), + ), + const SizedBox(height: 8), + Text( + "Membuka form dalam $countdown detik...", + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Tutup"), + ), + ElevatedButton( + onPressed: () { + _launchUATForm(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + ), + child: const Text("Buka Sekarang"), + ), + ], + ); + }, + ); + }, + ); + } + + // Function to launch the UAT form + Future _launchUATForm() async { + if (!await launchUrl(uatFormUrl, mode: LaunchMode.externalApplication)) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Tidak dapat membuka form kuisioner')), + ); + } + } else { + // Mark as completed so user can save after returning to app + setState(() { + _hasCompletedUAT = true; + }); + } + } + + // Modified save function that checks for UAT completion + void _saveCertificate() { + if (!_hasCompletedUAT) { + _showUATDialog(); + return; + } + // Proceed with original save functionality + setState(() { + _isGenerating = true; + }); + + // Call the actual save function passed from parent + widget.onSave(); + + // Reset generating state after a delay to simulate processing + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() { + _isGenerating = false; + }); + // Improved Snackbar with better styling + Get.snackbar( + "Berhasil", + "Sertifikat berhasil disimpan", + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.green.shade600, + colorText: Colors.white, + margin: const EdgeInsets.all(16), + borderRadius: 8, + duration: const Duration(seconds: 3), + icon: const Icon( + Icons.check_circle, + color: Colors.white, + ), + boxShadows: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ], + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + dismissDirection: DismissDirection.horizontal, + ); + } + }); + } + + // Original share function + void _shareCertificate() { + setState(() { + _isGenerating = true; + }); + + // Call the actual share function passed from parent + widget.onShare(); + + // Reset generating state after a delay to simulate processing + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() { + _isGenerating = false; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, -3), + ), + ], + ), + child: SafeArea( + child: !_isRevealed + ? const SizedBox.shrink() // Hide buttons before reveal + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _isGenerating ? null : _saveCertificate, + icon: _hasCompletedUAT + ? const Icon(Icons.save) + : const Icon(Icons.assignment_outlined), + label: Text(_isGenerating + ? 'Menyimpan...' + : (_hasCompletedUAT ? 'Simpan' : 'Isi UAT & Simpan')), + style: ElevatedButton.styleFrom( + backgroundColor: _hasCompletedUAT + ? Colors.blue.shade800 + : Colors.orange.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 4, + shadowColor: _hasCompletedUAT + ? Colors.blue.shade300 + : Colors.orange.shade300, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: _isGenerating ? null : _shareCertificate, + icon: const Icon(Icons.share), + label: Text(_isGenerating ? 'Membagikan...' : 'Bagikan'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo.shade800, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 4, + shadowColor: Colors.indigo.shade300, + ), + ), + ), + ], + ), + ), + ); + } +} + +class EnhancedBackgroundPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + // Background subtle elements + + // Soft curves + final Path curvePath1 = Path(); + curvePath1.moveTo(0, size.height * 0.1); + curvePath1.quadraticBezierTo(size.width * 0.2, size.height * 0.05, + size.width * 0.4, size.height * 0.1); + + canvas.drawPath( + curvePath1, + Paint() + ..color = Colors.blue.shade100.withOpacity(0.3) + ..style = PaintingStyle.stroke + ..strokeWidth = 15); + + final Path curvePath2 = Path(); + curvePath2.moveTo(size.width, size.height * 0.5); + curvePath2.quadraticBezierTo(size.width * 0.7, size.height * 0.6, + size.width * 0.5, size.height * 0.5); + + canvas.drawPath( + curvePath2, + Paint() + ..color = Colors.indigo.shade100.withOpacity(0.3) + ..style = PaintingStyle.stroke + ..strokeWidth = 15); + + // Subtle circles + final random = math.Random(123); // Fixed seed for consistency + for (int i = 0; i < 20; i++) { + final double x = random.nextDouble() * size.width; + final double y = random.nextDouble() * size.height; + final double radius = random.nextDouble() * 30 + 5; + + canvas.drawCircle( + Offset(x, y), + radius, + Paint() + ..color = (i % 2 == 0 ? Colors.blue : Colors.indigo) + .withOpacity(0.03 + (random.nextDouble() * 0.02)) + ..style = PaintingStyle.fill); + } + + // Abstract geometric elements in the corners + final double cornerSize = 100; + + // Top right + final Path diamondPath = Path(); + final double diamondX = size.width - cornerSize * 0.5; + final double diamondY = cornerSize * 0.5; + final double diamondSize = 30; + + diamondPath.moveTo(diamondX, diamondY - diamondSize); + diamondPath.lineTo(diamondX + diamondSize, diamondY); + diamondPath.lineTo(diamondX, diamondY + diamondSize); + diamondPath.lineTo(diamondX - diamondSize, diamondY); + diamondPath.close(); + + canvas.drawPath( + diamondPath, + Paint() + ..color = Colors.blue.shade300.withOpacity(0.1) + ..style = PaintingStyle.fill); + + // Bottom left + final double triangleX = cornerSize * 0.5; + final double triangleY = size.height - cornerSize * 0.5; + final double triangleSize = 40; + + final Path trianglePath = Path(); + trianglePath.moveTo(triangleX, triangleY - triangleSize); + trianglePath.lineTo(triangleX + triangleSize, triangleY + triangleSize); + trianglePath.lineTo(triangleX - triangleSize, triangleY + triangleSize); + trianglePath.close(); + + canvas.drawPath( + trianglePath, + Paint() + ..color = Colors.indigo.shade300.withOpacity(0.1) + ..style = PaintingStyle.fill); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +// Custom page physics for better swipe experience +class PagePhysics extends ScrollPhysics { + const PagePhysics({ScrollPhysics? parent}) : super(parent: parent); + + @override + PagePhysics applyTo(ScrollPhysics? ancestor) { + return PagePhysics(parent: buildParent(ancestor)); + } + + @override + SpringDescription get spring => const SpringDescription( + mass: 80, + stiffness: 100, + damping: 1, + ); +} + +// Model for floating particles +class ParticleModel { + Offset position; // Value between 0.0 and 1.0 for screen relative positioning + final double speed; + final double size; + final Color color; + + ParticleModel({ + required this.position, + required this.speed, + required this.size, + required this.color, + }); +} + +// Background painter for detail page +class DetailBackgroundPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + // Base background color + final Paint bgPaint = Paint()..color = Colors.white; + canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), bgPaint); + + // Create a soft gradient background + final Rect rect = Rect.fromLTRB(0, 0, size.width, size.height); + final Gradient gradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFFF8FDFF), + const Color(0xFFF0F7F8), + const Color(0xFFF8FDFF), + ], + stops: const [0.0, 0.5, 1.0], + ); + + final Paint gradientPaint = Paint() + ..shader = gradient.createShader(rect) + ..style = PaintingStyle.fill; + + canvas.drawRect(rect, gradientPaint); + + // Draw subtle dot pattern + final dotPaint = Paint() + ..color = const Color(0xFFd9dbe6).withOpacity(0.05) + ..style = PaintingStyle.fill; + + // Dot pattern + for (int i = 0; i < size.width; i += 30) { + for (int j = 0; j < size.height; j += 30) { + // Draw small dots + canvas.drawCircle( + Offset(i.toDouble(), j.toDouble()), + 1, + dotPaint, + ); + } + } + + // Add subtle waves at top and bottom + final wavePaint = Paint() + ..color = const Color(0xFF57C5B6).withOpacity(0.05) + ..style = PaintingStyle.fill; + + // Top waves + final topWavePath = Path(); + topWavePath.moveTo(0, 0); + topWavePath.lineTo(0, 70); + + for (int i = 0; i < (size.width / 80).ceil(); i++) { + topWavePath.quadraticBezierTo( + (i * 80) + 40, + 40, + (i * 80) + 80, + 70, + ); + } + + topWavePath.lineTo(size.width, 0); + topWavePath.close(); + + // Bottom waves + final bottomWavePath = Path(); + bottomWavePath.moveTo(0, size.height); + bottomWavePath.lineTo(0, size.height - 70); + + for (int i = 0; i < (size.width / 80).ceil(); i++) { + bottomWavePath.quadraticBezierTo( + (i * 80) + 40, + size.height - 40, + (i * 80) + 80, + size.height - 70, + ); + } + + bottomWavePath.lineTo(size.width, size.height); + bottomWavePath.close(); + + canvas.drawPath(topWavePath, wavePaint); + canvas.drawPath(bottomWavePath, wavePaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} diff --git a/lib/app/views/student/feature/recomendation_screen/view/page_recmendation_detail_screen.dart b/lib/app/views/student/feature/recomendation_screen/view/page_recmendation_detail_screen.dart new file mode 100644 index 0000000..1eb035c --- /dev/null +++ b/lib/app/views/student/feature/recomendation_screen/view/page_recmendation_detail_screen.dart @@ -0,0 +1,1052 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/app/views/about/page_about.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:firebase_core/firebase_core.dart'; +import 'package:intl/intl.dart' as intl; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:url_launcher/url_launcher.dart'; + +class _DetailTabController extends GetxController { + // Current tab index (0: Recommendations, 1: Answers, 2: Rules) + final RxInt currentTab = 0.obs; + + void setTab(int index) { + currentTab.value = index; + } +} + +class RecommendationDetailPage extends StatelessWidget { + final Map data; + final String documentId; + + const RecommendationDetailPage({ + Key? key, + required this.data, + required this.documentId, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + // Extract basic data + final questionMode = data['questionMode'] ?? 'Tidak diketahui'; + final timestamp = data['timestamp'] as Timestamp?; + final formattedDate = timestamp != null + ? intl.DateFormat('dd MMMM yyyy, HH:mm').format(timestamp.toDate()) + : 'Tidak ada tanggal'; + final totalQuestions = data['totalQuestions'] ?? 0; + final answeredQuestions = data['answeredQuestions'] ?? 0; + + // Controller for tab view + final tabController = Get.put(_DetailTabController()); + + return Scaffold( + body: SafeArea( + child: Column( + children: [ + // Custom app bar with gradient + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + questionMode.contains('Karir') + ? Colors.blue.shade800 + : Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: Column( + children: [ + // Top bar with back button and actions + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 16, 0), + child: Row( + children: [ + IconButton( + icon: + const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Get.back(), + ), + Expanded( + child: Text( + questionMode, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.share, color: Colors.white), + onPressed: () => _shareResults(), + ), + IconButton( + icon: const Icon(Icons.delete_outline, + color: Colors.white), + onPressed: () => _confirmDelete(context), + ), + ], + ), + ), + + // Date and stats info + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Date row + Row( + children: [ + Icon( + Icons.calendar_today, + color: Colors.white.withOpacity(0.7), + size: 16, + ), + const SizedBox(width: 8), + Text( + formattedDate, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 14, + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Stats cards + Row( + children: [ + Expanded( + child: _buildStatCard( + icon: Icons.question_answer_rounded, + title: 'Pertanyaan', + value: '$answeredQuestions/$totalQuestions', + color: Colors.white.withOpacity(0.15), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + icon: Icons.lightbulb_outline, + title: 'Rekomendasi', + value: (data['recommendations'] as List?) + ?.length + .toString() ?? + '0', + color: Colors.white.withOpacity(0.15), + ), + ), + ], + ), + ], + ), + ), + + // Tab bar + Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Obx(() => Row( + children: [ + _buildTabButton( + title: 'Rekomendasi', + isActive: tabController.currentTab.value == 0, + onTap: () => tabController.setTab(0), + ), + const SizedBox(width: 16), + _buildTabButton( + title: 'Jawaban', + isActive: tabController.currentTab.value == 1, + onTap: () => tabController.setTab(1), + ), + const SizedBox(width: 16), + _buildTabButton( + title: 'Rules', + isActive: tabController.currentTab.value == 2, + onTap: () => tabController.setTab(2), + ), + ], + )), + ), + ), + ], + ), + ), + + // Tab content + Expanded( + child: Container( + color: Colors.white, + child: Obx(() { + switch (tabController.currentTab.value) { + case 0: + return _buildRecommendationsTab(); + case 1: + return _buildAnswersTab(); + case 2: + return _buildRulesTab(); + default: + return _buildRecommendationsTab(); + } + }), + ), + ), + ], + ), + ), + ); + } + + Widget _buildStatCard({ + required IconData icon, + required String title, + required String value, + required Color color, + }) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 12, + ), + ), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildTabButton({ + required String title, + required bool isActive, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + Text( + title, + style: TextStyle( + color: isActive ? Colors.indigo.shade800 : Colors.grey.shade500, + fontWeight: isActive ? FontWeight.bold : FontWeight.normal, + ), + ), + const SizedBox(height: 8), + Container( + height: 3, + width: 40, + decoration: BoxDecoration( + color: isActive ? Colors.indigo.shade800 : Colors.transparent, + borderRadius: BorderRadius.circular(10), + ), + ), + ], + ), + ); + } + + // RECOMMENDATION TAB + Widget _buildRecommendationsTab() { + final recommendations = data['recommendations'] as List? ?? []; + + if (recommendations.isEmpty) { + return _buildEmptyState( + icon: Icons.lightbulb_outline, + message: 'Tidak ada rekomendasi yang tersedia', + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: recommendations.length, + itemBuilder: (context, index) { + final recommendation = recommendations[index]; + final title = recommendation['title'] ?? 'Tidak ada judul'; + final score = recommendation['score'] ?? 0; + final careers = recommendation['careers'] as List? ?? []; + final majors = recommendation['majors'] as List? ?? []; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Recommendation header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: + index == 0 ? Colors.amber.shade50 : Colors.grey.shade50, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + border: Border( + bottom: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: index == 0 ? Colors.amber : Colors.grey, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: index == 0 + ? Colors.amber.shade100 + : Colors.grey.shade200, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + 'Skor: $score', + style: TextStyle( + color: index == 0 + ? Colors.amber.shade900 + : Colors.grey.shade700, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + ), + + // Careers + if (careers.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Karir:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + const SizedBox(height: 8), + ...careers + .map((career) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.work_outline, + size: 16, + color: Colors.orange.shade400, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + career.toString(), + style: const TextStyle( + color: Colors.black87, + ), + ), + ), + ], + ), + )) + .toList(), + ], + ), + ), + if (majors.isNotEmpty) + Divider(color: Colors.grey.shade200, height: 1), + ], + + // Majors + if (majors.isNotEmpty) + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Jurusan Terkait:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + const SizedBox(height: 8), + ...majors + .map((major) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.school_outlined, + size: 16, + color: Colors.green.shade400, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + major.toString(), + style: const TextStyle( + color: Colors.black87, + ), + ), + ), + ], + ), + )) + .toList(), + ], + ), + ), + ], + ), + ); + }, + ); + } + + // ANSWERS TAB + Widget _buildAnswersTab() { + final userAnswers = data['userAnswers'] as List? ?? []; + + if (userAnswers.isEmpty) { + return _buildEmptyState( + icon: Icons.question_answer_outlined, + message: 'Tidak ada jawaban yang tersedia', + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: userAnswers.length, + itemBuilder: (context, index) { + final answer = userAnswers[index]; + final questionId = answer['questionId'] ?? ''; + final question = answer['question'] ?? ''; + final userAnswer = answer['answer']; + final programName = answer['programName'] ?? ''; + final minatKey = answer['minatKey'] ?? ''; + + String answerText; + Color answerColor; + IconData answerIcon; + + if (userAnswer == true) { + answerText = 'Ya'; + answerColor = Colors.green.shade800; + answerIcon = Icons.check_circle; + } else if (userAnswer == false) { + answerText = 'Tidak'; + answerColor = Colors.red.shade800; + answerIcon = Icons.cancel; + } else { + answerText = 'Tidak Dijawab'; + answerColor = Colors.grey.shade800; + answerIcon = Icons.help; + } + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + border: Border.all(color: Colors.grey.shade200), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Question ID and category + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.indigo.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + questionId, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + '$programName - $minatKey', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Question text + Text( + question, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + + const SizedBox(height: 12), + + // Answer + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: userAnswer == true + ? Colors.green.shade50 + : userAnswer == false + ? Colors.red.shade50 + : Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: userAnswer == true + ? Colors.green.shade200 + : userAnswer == false + ? Colors.red.shade200 + : Colors.grey.shade200, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + answerIcon, + size: 14, + color: answerColor, + ), + const SizedBox(width: 6), + Text( + 'Jawaban: $answerText', + style: TextStyle( + color: answerColor, + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + // RULES TAB + Widget _buildRulesTab() { + final workingMemory = data['workingMemory'] as List? ?? []; + final recommendations = data['recommendations'] as List? ?? []; + + if (workingMemory.isEmpty && recommendations.isEmpty) { + return _buildEmptyState( + icon: Icons.code, + message: 'Tidak ada rules yang tersedia', + ); + } + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + // Working memory + if (workingMemory.isNotEmpty) ...[ + Text( + 'Working Memory:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: workingMemory + .map((memory) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.indigo.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + memory.toString(), + style: TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: Colors.indigo.shade800, + ), + ), + )) + .toList(), + ), + ), + const SizedBox(height: 24), + ], + + // Rules per recommendation + if (recommendations.isNotEmpty) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Rules by Recommendation:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + // Copy button + IconButton( + onPressed: () => _copyRulesToClipboard(), + icon: Icon(Icons.copy, color: Colors.indigo.shade600, size: 20), + tooltip: 'Salin Rules', + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + ), + ], + ), + const SizedBox(height: 8), + ...recommendations.asMap().entries.map((entry) { + final index = entry.key; + final recommendation = entry.value; + final title = recommendation['title'] ?? 'Tidak ada judul'; + final rules = recommendation['rules'] as List? ?? []; + + if (rules.isEmpty) return const SizedBox.shrink(); + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Recommendation header + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: index == 0 + ? Colors.amber.shade50 + : Colors.grey.shade50, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + border: Border( + bottom: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: index == 0 ? Colors.amber : Colors.grey, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + ), + ], + ), + ), + + // Rules + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: rules + .map((rule) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: + Border.all(color: Colors.grey.shade200), + ), + child: Text( + rule.toString(), + style: TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: Colors.grey.shade800, + ), + ), + ), + )) + .toList(), + ), + ), + ], + ), + ); + }).toList(), + ], + ], + ); + } + + // Empty state widget + Widget _buildEmptyState({ + required IconData icon, + required String message, + }) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 64, + color: Colors.grey.shade300, + ), + const SizedBox(height: 16), + Text( + message, + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + // Helper functions + void _shareResults() { + // Format a shareable text + final buffer = StringBuffer(); + + // Basic info + final questionMode = data['questionMode'] ?? 'Tidak diketahui'; + final timestamp = data['timestamp'] as Timestamp?; + final formattedDate = timestamp != null + ? intl.DateFormat('dd MMMM yyyy, HH:mm').format(timestamp.toDate()) + : 'Tidak ada tanggal'; + + buffer.writeln('Hasil Rekomendasi - $questionMode'); + buffer.writeln('Tanggal: $formattedDate'); + buffer.writeln(''); + + // Recommendations + final recommendations = data['recommendations'] as List? ?? []; + if (recommendations.isNotEmpty) { + buffer.writeln('TOP REKOMENDASI:'); + for (int i = 0; i < recommendations.length; i++) { + final rec = recommendations[i]; + final title = rec['title'] ?? 'Tidak ada judul'; + final score = rec['score'] ?? 0; + buffer.writeln('${i + 1}. $title (Skor: $score)'); + } + } + + final shareText = buffer.toString(); + + // Share the text + Clipboard.setData(ClipboardData(text: shareText)); + Get.snackbar( + 'Disalin', + 'Hasil rekomendasi telah disalin ke clipboard', + backgroundColor: Colors.green.shade100, + colorText: Colors.green.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } + + void _copyRulesToClipboard() { + // Format all rules as text + final buffer = StringBuffer(); + + // Working memory + final workingMemory = data['workingMemory'] as List? ?? []; + if (workingMemory.isNotEmpty) { + buffer.writeln('WORKING MEMORY:'); + buffer.writeln(workingMemory.join(', ')); + buffer.writeln(''); + } + + // Rules by recommendation + final recommendations = data['recommendations'] as List? ?? []; + if (recommendations.isNotEmpty) { + buffer.writeln('RULES BY RECOMMENDATION:'); + for (int i = 0; i < recommendations.length; i++) { + final rec = recommendations[i]; + final title = rec['title'] ?? 'Tidak ada judul'; + final rules = rec['rules'] as List? ?? []; + + if (rules.isNotEmpty) { + buffer.writeln('${i + 1}. $title:'); + for (final rule in rules) { + buffer.writeln(' $rule'); + } + buffer.writeln(''); + } + } + } + + final rulesText = buffer.toString(); + + // Copy to clipboard + Clipboard.setData(ClipboardData(text: rulesText)); + Get.snackbar( + 'Disalin', + 'Rules telah disalin ke clipboard', + backgroundColor: Colors.green.shade100, + colorText: Colors.green.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } + + void _confirmDelete(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Hapus Riwayat'), + content: const Text( + 'Apakah Anda yakin ingin menghapus riwayat rekomendasi ini?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Batal'), + ), + ElevatedButton.icon( + onPressed: () { + Navigator.pop(context); + _deleteRecommendation(); + }, + icon: const Icon(Icons.delete), + label: const Text('Hapus'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + ), + ], + ), + ); + } + + void _deleteRecommendation() async { + try { + final prefs = await SharedPreferences.getInstance(); + + String? schoolId = prefs.getString('school_id'); + + // Check if we have the schoolId + if (schoolId == null || schoolId!.isEmpty) { + // Try to get schoolId from SharedPreferences if not provided + final prefs = await SharedPreferences.getInstance(); + schoolId = prefs.getString('school_id'); + + if (schoolId == null || schoolId!.isEmpty) { + Get.snackbar( + 'Error', + 'Tidak dapat menemukan data sekolah', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + return; + } + } + + // Delete from school's recommendation_history subcollection + await FirebaseFirestore.instance + .collection('schools') + .doc(schoolId) + .collection('recommendation_history') + .doc(documentId) + .delete(); + + // Show success message and go back + Get.back(); + Get.snackbar( + 'Berhasil', + 'Riwayat rekomendasi telah dihapus', + backgroundColor: Colors.green.shade100, + colorText: Colors.green.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } catch (e) { + Get.snackbar( + 'Error', + 'Gagal menghapus riwayat: ${e.toString()}', + backgroundColor: Colors.red.shade100, + colorText: Colors.red.shade800, + snackPosition: SnackPosition.BOTTOM, + ); + } + } +} diff --git a/lib/app/views/student/feature/recomendation_screen/view/page_recomendation_screen.dart b/lib/app/views/student/feature/recomendation_screen/view/page_recomendation_screen.dart new file mode 100644 index 0000000..4e12463 --- /dev/null +++ b/lib/app/views/student/feature/recomendation_screen/view/page_recomendation_screen.dart @@ -0,0 +1,5564 @@ +import 'dart:convert'; +import 'dart:ui'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/app/views/about/page_about.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:forward_chaining_man_app/app/views/splash_screen/page/page_splash_screen.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/recomendation_screen/page_cetak_sertifikat.dart'; +import 'package:forward_chaining_man_app/app/views/student/model/data_student.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:firebase_core/firebase_core.dart'; +import 'package:intl/intl.dart' as intl; + +import 'package:url_launcher/url_launcher.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:fl_chart/fl_chart.dart'; + +class RecommendationResultsScreen extends StatefulWidget { + final RecommendationResult result; + final String rawMessage; + + const RecommendationResultsScreen({ + required this.result, + this.rawMessage = '', + Key? key, + }) : super(key: key); + + @override + State createState() => + _RecommendationResultsScreenState(); +} + +class _RecommendationResultsScreenState + extends State with TickerProviderStateMixin { + // Controller untuk berbagai animasi + late AnimationController _swipeController; + late AnimationController _swipePromptController; + late AnimationController _introController; + late AnimationController _cardController; + late AnimationController _confettiController; + late List _itemControllers; + late AnimationController _networkController; + // Animasi + late Animation _swipeAnimation; + late Animation _fadeAnimation; + late Animation _scaleAnimation; + + // State untuk tracking progres interaksi + bool _hasRevealed = false; + bool _hasConfetti = false; + double _swipeProgress = 0.0; + int _currentPage = 0; + final PageController _pageController = PageController(); + + // Warna untuk peringkat + final Color _goldColor = const Color.fromARGB(255, 29, 255, 120); + final Color _silverColor = const Color(0xFFC0C0C0); + final Color _bronzeColor = const Color(0xFFCD7F32); + + // Emoji untuk setiap peringkat + final List _rankEmojis = ['🏆', '🥈', '🥉']; + + // Gradient warna + final List _gradientColors = [ + Colors.blue.shade800, + Colors.indigo.shade900, + ]; + + @override + void initState() { + super.initState(); + + // Separate slow controller for network animations - 15 seconds and repeat + _networkController = AnimationController( + duration: const Duration(milliseconds: 15000), + vsync: this, + ); + + // Inisialisasi controller animasi + _swipeController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + + // Controller khusus untuk animasi prompt swipe + _swipePromptController = AnimationController( + duration: const Duration(milliseconds: 2500), + vsync: this, + )..repeat(reverse: true); + + _introController = AnimationController( + duration: const Duration(milliseconds: 1800), + vsync: this, + )..forward(); + + _cardController = AnimationController( + duration: const Duration( + milliseconds: 1200), // Lebih lama untuk efek lebih halus + vsync: this, + ); + + _confettiController = AnimationController( + duration: const Duration( + milliseconds: 4000), // Lebih lama untuk efek lebih lengkap + vsync: this, + ); + + // Inisialisasi controller untuk setiap rekomendasi + _itemControllers = List.generate( + math.min(3, widget.result.recommendations.length), + (index) => AnimationController( + duration: Duration( + milliseconds: + 800 + (index * 300)), // Lebih lama untuk efek yang lebih jelas + vsync: this, + ), + ); + + // Animasi swipe + _swipeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _swipeController, + curve: Curves.easeOutBack, + ), + ); + + // Animasi fade + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _introController, + curve: const Interval(0.3, 1.0, curve: Curves.easeOut), + ), + ); + + // Animasi scale + _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation( + parent: _introController, + curve: const Interval(0.3, 1.0, curve: Curves.easeOutBack), + ), + ); + } + + @override + void dispose() { + _swipeController.dispose(); + _swipePromptController.dispose(); + _introController.dispose(); + _cardController.dispose(); + _confettiController.dispose(); + for (var controller in _itemControllers) { + controller.dispose(); + } + _pageController.dispose(); + super.dispose(); + } + + void _onSwipeComplete() { + setState(() { + _hasRevealed = true; + }); + _cardController.forward(); + + // Mulai animasi confetti setelah kartu muncul + Future.delayed(const Duration(milliseconds: 400), () { + if (mounted) { + setState(() { + _hasConfetti = true; + }); + _confettiController.forward(); + } + }); + + // Animasikan item rekomendasi secara berurutan dengan jeda yang lebih pendek + for (int i = 0; i < _itemControllers.length; i++) { + Future.delayed(Duration(milliseconds: 150 + (i * 200)), () { + if (mounted) { + _itemControllers[i].forward(); + } + }); + } + } + +// Color helpers for medal colors + Color _getMedalColor(int index) { + switch (index) { + case 0: + return Color.fromARGB(255, 0, 255, 140); // Gold + case 1: + return Color.fromARGB(255, 238, 213, 73); // Silver + case 2: + return Color.fromARGB(255, 255, 75, 39); // Bronze + default: + return Colors.grey; // Fallback + } + } + +// Label yang sesuai dengan medali + String _getMedalLabel(int index) { + switch (index) { + case 0: + return 'Sangat Direkomendasikan'; + case 1: + return 'Direkomendasikan'; + case 2: + return 'Kurang Direkomendasikan'; + default: + return 'Tidak Direkomendasikan'; + } + } + +// Emoji yang sesuai dengan medali + String _getMedalEmoji(int index) { + switch (index) { + case 0: + return '🥇'; + case 1: + return '🥈'; + case 2: + return '🥉'; + default: + return '🏅'; + } + } + + IconData _getMedalIcon(int index) { + return Icons.workspace_premium; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: _gradientColors, + ), + ), + child: Stack( + children: [ + if (_hasRevealed) + Positioned( + top: 10, + left: 0, + right: 0, + child: SafeArea(child: _buildPageIndicator()), + ), + + // Header + + // Konten utama (berbeda berdasarkan status revealed) + _hasRevealed + ? SafeArea( + child: _buildRevealedContent(), + bottom: false, + left: false, + ) + : _buildInitialContent(), + + // Partikel confetti (hanya muncul setelah swipe) + if (_hasConfetti) _buildConfetti(), + // Indikator halaman + ], + ), + ), + ); + } + + Widget _buildInitialContent() { + return GestureDetector( + // Deteksi swipe gesture untuk interaksi yang lebih baik + onVerticalDragUpdate: (details) { + if (!_hasRevealed) { + setState(() { + // Hitung kemajuan swipe berdasarkan jarak + _swipeProgress -= details.primaryDelta! / 200.0; + _swipeProgress = _swipeProgress.clamp(0.0, 1.0); + + // Update controller animasi swipe berdasarkan kemajuan + _swipeController.value = _swipeProgress; + }); + + // Jika swipe hampir selesai, otomatis selesaikan + if (_swipeProgress > 0.7 && !_hasRevealed) { + _swipeController + .forward(from: _swipeController.value) + .then((_) => _onSwipeComplete()); + } + } + }, + onVerticalDragEnd: (details) { + if (!_hasRevealed) { + if (_swipeProgress > 0.3) { + // Jika swipe sudah cukup jauh, selesaikan + _swipeController + .forward(from: _swipeController.value) + .then((_) => _onSwipeComplete()); + } else { + // Jika belum cukup jauh, kembalikan + _swipeController.reverse(from: _swipeController.value); + setState(() { + _swipeProgress = 0.0; + }); + } + } + }, + child: FadeTransition( + opacity: _fadeAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Stack( + alignment: Alignment.center, + children: [ + // Panel animasi untuk feedback visual saat swipe + Positioned.fill( + child: AnimatedBuilder( + animation: _swipeController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + -MediaQuery.of(context).size.height * + _swipeController.value), + child: Container( + color: Colors.white.withOpacity(0.1), + ), + ); + }, + ), + ), + + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Icon panah animasi dengan efek lebih menonjol + AnimatedBuilder( + animation: _swipePromptController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + -20 * _swipePromptController.value, + ), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity( + 0.2 * _swipePromptController.value), + blurRadius: 20 * _swipePromptController.value, + spreadRadius: + 5 * _swipePromptController.value, + ), + ], + ), + child: Icon( + Icons.keyboard_arrow_up_rounded, + color: Colors.white, + size: 60 + (10 * _swipePromptController.value), + ), + ), + ); + }, + ), + const SizedBox(height: 30), + ShaderMask( + shaderCallback: (bounds) { + return LinearGradient( + colors: [Colors.white, Colors.white.withOpacity(0.8)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(bounds); + }, + child: const Text( + 'Swipe Ke Atas', + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(30), + border: + Border.all(color: Colors.white.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: const Text( + 'Untuk Melihat Hasil Rekomendasimu', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + const SizedBox(height: 40), + // Animated hand icon + AnimatedBuilder( + animation: _swipePromptController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + -10 * + math.sin( + _swipePromptController.value * math.pi), + ), + child: Opacity( + opacity: 0.5 + + (0.5 * + math.sin(_swipePromptController.value * + math.pi)), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.touch_app, + color: Colors.white, + size: 32, + ), + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildRevealedContent() { + return AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + (1 - _cardController.value) * + MediaQuery.of(context).size.height * + 0.5, + ), + child: Opacity( + opacity: _cardController.value, + child: PageView( + controller: _pageController, + onPageChanged: (index) { + setState(() { + _currentPage = index; + }); + }, + children: [ + _buildRecommendationPage(), + _buildDetailPage(), + _buildExplanationPage(), + ], + ), + ), + ); + }, + ); + } + + Widget _buildRecommendationPage() { + final recommendations = widget.result.recommendations; + final topRecommendations = recommendations.length > 3 + ? recommendations.sublist(0, 3) + : recommendations; + + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 40), + Row( + children: [ + Expanded( + child: Text( + 'Hasil Rekomendasimu! 🎉', + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + shadows: [ + Shadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Transform.rotate( + angle: _cardController.value * math.pi * 2, + child: const Icon( + Icons.auto_awesome, + color: Colors.yellow, + size: 32, + ), + ); + }, + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Berdasarkan jawabanmu di kuesioner, kami menemukan minat yang paling cocok untukmu', + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + ), + ), + const SizedBox(height: 24), + + // Top 3 Recommendations in a Row +// Top 3 Recommendations in a Row + // Top 3 Recommendations in a Row (fully responsive) + Container( + margin: EdgeInsets.only(bottom: 16), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: BouncingScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: List.generate( + math.min(3, topRecommendations.length), + (index) { + final recommendation = topRecommendations[index]; + final parts = recommendation.title.split('|'); + final minatName = parts.length > 1 ? parts[1] : parts[0]; + final programName = parts[0]; + + // Get medal color + final Color medalColor = _getMedalColor(index); + + // Warna aksen untuk variasi + final Color accentColor = index == 0 + ? Color.fromARGB(255, 108, 247, 84) // Gold accent + : index == 1 + ? Color.fromARGB( + 255, 225, 193, 75) // Silver accent + : Color(0xFFE59866); // Bronze accent + + return AnimatedBuilder( + animation: _itemControllers[index], + builder: (context, child) { + final progress = math.min(1.0, + math.max(0.0, _itemControllers[index].value)); + final scale = 0.8 + (0.2 * progress); + + return Transform.scale( + scale: scale, + child: Opacity( + opacity: progress, + child: Container( + width: 220, + height: + 450, // Fix height here - all cards will have the same height + margin: EdgeInsets.only( + right: 16, top: 4, bottom: 4), + child: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => + _buildDetailBottomSheet( + recommendation, index), + ); + }, + child: Stack( + children: [ + // Main card with gradient border + Card( + elevation: 8, + shadowColor: + medalColor.withOpacity(0.6), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(20), + ), + child: Container( + height: + 450, // Fix height here too for consistency + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(20), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + Colors.white, + ], + ), + boxShadow: [ + BoxShadow( + color: + medalColor.withOpacity(0.3), + spreadRadius: 0, + blurRadius: 15, + offset: Offset(0, 8), + ) + ], + border: Border.all( + width: 2, + color: medalColor, + ), + ), + child: ClipRRect( + borderRadius: + BorderRadius.circular(20), + child: Stack( + children: [ + // Animated background patterns + Positioned.fill( + child: CustomPaint( + painter: + AnimatedBackgroundPainter( + color: medalColor, + accentColor: accentColor, + animationValue: + _itemControllers[ + index] + .value, + ), + ), + ), + + // Blurred overlay for better readability + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10, sigmaY: 10), + child: Container( + color: Colors.white + .withOpacity(0.8), + ), + ), + ), + + // Medal icon + Positioned( + top: -5, + left: -5, + child: Container( + height: 60, + width: 60, + child: CustomPaint( + painter: + MedalBadgePainter( + color: medalColor, + accentColor: + accentColor, + emoji: _getMedalEmoji( + index), + ), + ), + ), + ), + + // Content - made consistent for all cards + Container( + padding: EdgeInsets.fromLTRB( + 16, 30, 16, 16), + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .center, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + // Top section + Column( + children: [ + // Medal banner with shimmer effect + ShimmerText( + text: _getMedalLabel( + index) + .toUpperCase(), + baseColor: medalColor + .withOpacity( + 0.7), + highlightColor: + accentColor, + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight + .bold, + letterSpacing: + 1.2, + ), + ), + ], + ), + + // Middle section (circular score) + Container( + width: 80, + height: 80, + decoration: + BoxDecoration( + shape: + BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: medalColor + .withOpacity( + 0.3), + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: Stack( + children: [ + // Animated circular progress + Positioned.fill( + child: + TweenAnimationBuilder< + double>( + tween: Tween< + double>( + begin: 0, + end: recommendation + .score / + 100), + duration: Duration( + milliseconds: + 1500), + curve: Curves + .easeOutCubic, + builder: + (context, + value, + child) { + return CustomPaint( + painter: + CircularScorePainter( + progress: + value, + color: + medalColor, + strokeWidth: + 6, + ), + ); + }, + ), + ), + + // Center text + Center( + child: Column( + mainAxisSize: + MainAxisSize + .min, + children: [ + Text( + '${recommendation.score}%', + style: + TextStyle( + fontSize: + 22, + fontWeight: + FontWeight + .bold, + color: + medalColor, + ), + ), + Text( + 'COCOK', + style: + TextStyle( + fontSize: + 10, + fontWeight: + FontWeight + .bold, + color: Colors + .grey + .shade600, + letterSpacing: + 1, + ), + ), + ], + ), + ), + ], + ), + ), + + // Program & Minat name + Container( + width: double.infinity, + padding: EdgeInsets + .symmetric( + vertical: 8), + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + Text( + programName, + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight + .w500, + color: Colors + .grey + .shade600, + letterSpacing: + 0.5, + ), + textAlign: + TextAlign + .center, + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + SizedBox(height: 6), + Container( + padding: EdgeInsets + .symmetric( + horizontal: + 8, + vertical: + 2), + decoration: + BoxDecoration( + gradient: + LinearGradient( + colors: [ + medalColor + .withOpacity( + 0.1), + accentColor + .withOpacity( + 0.1) + ], + begin: Alignment + .centerLeft, + end: Alignment + .centerRight, + ), + borderRadius: + BorderRadius + .circular( + 4), + ), + child: Text( + minatName, + style: + TextStyle( + fontSize: 16, + fontWeight: + FontWeight + .bold, + color: + medalColor, + ), + textAlign: + TextAlign + .center, + maxLines: 2, + overflow: + TextOverflow + .ellipsis, + ), + ), + ], + ), + ), + + // Preview content with icons - same height for all + Container( + height: + 80, // Fixed height for this section + child: Row( + children: [ + if (recommendation + .careers + .isNotEmpty) + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + Container( + padding: + EdgeInsets.all( + 8), + decoration: + BoxDecoration( + color: medalColor + .withOpacity(0.1), + shape: BoxShape + .circle, + ), + child: + Icon( + Icons + .work, + color: + medalColor, + size: + 16, + ), + ), + SizedBox( + height: + 6), + Text( + recommendation + .careers + .first, + style: + TextStyle( + fontSize: + 11, + color: Colors + .black87, + ), + textAlign: + TextAlign + .center, + maxLines: + 2, + overflow: + TextOverflow + .ellipsis, + ), + ], + ), + ), + SizedBox(width: 10), + if (recommendation + .majors + .isNotEmpty) + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + Container( + padding: + EdgeInsets.all( + 8), + decoration: + BoxDecoration( + color: medalColor + .withOpacity(0.1), + shape: BoxShape + .circle, + ), + child: + Icon( + Icons + .school, + color: + medalColor, + size: + 16, + ), + ), + SizedBox( + height: + 6), + Text( + recommendation + .majors + .first, + style: + TextStyle( + fontSize: + 11, + color: Colors + .black87, + ), + textAlign: + TextAlign + .center, + maxLines: + 2, + overflow: + TextOverflow + .ellipsis, + ), + ], + ), + ), + ], + ), + ), + + // Button section + Container( + margin: EdgeInsets.only( + top: 10), + decoration: + BoxDecoration( + gradient: + LinearGradient( + colors: [ + medalColor, + accentColor + ], + begin: Alignment + .topLeft, + end: Alignment + .bottomRight, + ), + borderRadius: + BorderRadius + .circular(20), + boxShadow: [ + BoxShadow( + color: medalColor + .withOpacity( + 0.4), + blurRadius: 10, + offset: + Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors + .transparent, + child: InkWell( + onTap: () { + HapticFeedback + .lightImpact(); + showModalBottomSheet( + context: + context, + isScrollControlled: + true, + backgroundColor: + Colors + .transparent, + builder: (context) => + _buildDetailBottomSheet( + recommendation, + index), + ); + }, + borderRadius: + BorderRadius + .circular( + 20), + splashColor: Colors + .white + .withOpacity( + 0.2), + highlightColor: + Colors.white + .withOpacity( + 0.1), + child: Container( + padding: EdgeInsets + .symmetric( + horizontal: + 20, + vertical: + 10), + child: Row( + mainAxisSize: + MainAxisSize + .min, + children: [ + Text( + 'Lihat Detail', + style: + TextStyle( + color: Colors + .white, + fontWeight: + FontWeight + .bold, + fontSize: + 14, + ), + ), + SizedBox( + width: 8), + Icon( + Icons + .arrow_forward, + color: Colors + .white, + size: 14, + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + + // Glass reflection overlay + Positioned( + top: 0, + left: 0, + right: 0, + height: 80, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: + Alignment.topCenter, + end: Alignment + .bottomCenter, + colors: [ + Colors.white + .withOpacity(0.3), + Colors.white + .withOpacity(0.1), + Colors.white + .withOpacity(0), + ], + stops: [0.0, 0.5, 1.0], + ), + borderRadius: + BorderRadius.only( + topLeft: + Radius.circular(20), + topRight: + Radius.circular(20), + ), + ), + ), + ), + + // Floating sparkles + if (_itemControllers[index] + .value > + 0.7) + ...List.generate( + 5, + (i) => Positioned( + top: 10 + + (i * + 30 * + (i % 2 + 1)), + right: 10 + + (i * + 15 * + ((i + 1) % 2)), + child: AnimatedBuilder( + animation: + _itemControllers[ + index], + builder: + (context, child) { + final sparkleAnim = math + .sin(_itemControllers[ + index] + .value * + (5 + i) * + math.pi); + return Opacity( + opacity: 0.4 + + (sparkleAnim + .abs() * + 0.6), + child: Transform + .rotate( + angle: + sparkleAnim * + 0.3, + child: Icon( + Icons.star, + color: + accentColor, + size: + 8 + (i * 2), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ), + ), + ), + ), + const SizedBox(height: 32), + ...List.generate( + math.min(3, topRecommendations.length), + (index) { + final recommendation = topRecommendations[index]; + final parts = recommendation.title.split('|'); + final minatName = parts.length > 1 ? parts[1] : parts[0]; + final programName = parts[0]; + + // Get medal color + final Color medalColor = _getMedalColor(index); + + // Segunda cor para os padrões e destaque + final Color accentColor = index == 0 + ? Color.fromARGB(255, 11, 185, 124) // Gold accent + : index == 1 + ? Color(0xFFE59866) // Silver accent + : Color(0xFFE59866); // Bronze accent + + return AnimatedBuilder( + animation: _itemControllers[index], + builder: (context, child) { + final progress = math.min( + 1.0, math.max(0.0, _itemControllers[index].value)); + final adjustedProgress = + Curves.easeOutCubic.transform(progress); + final scale = 0.95 + (0.05 * adjustedProgress); + + return Transform.translate( + offset: Offset( + (1.0 - adjustedProgress) * 50, + 0, + ), + child: Transform.scale( + scale: scale, + child: Opacity( + opacity: adjustedProgress, + child: Container( + margin: EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: Card( + elevation: 8, + shadowColor: medalColor.withOpacity(0.6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + Colors.white, + ], + ), + boxShadow: [ + BoxShadow( + color: medalColor.withOpacity(0.3), + spreadRadius: 0, + blurRadius: 15, + offset: Offset(0, 8), + ) + ], + border: Border.all( + width: 1.5, + color: medalColor, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Stack( + children: [ + // Animated background patterns + Positioned.fill( + child: AnimatedBuilder( + animation: _itemControllers[index], + builder: (context, child) { + return CustomPaint( + painter: + AnimatedBackgroundPainter( + color: medalColor, + accentColor: accentColor, + animationValue: + _itemControllers[index] + .value, + ), + ); + }, + ), + ), + + // Blurred overlay for better readability + Positioned.fill( + child: ClipRRect( + borderRadius: + BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10, sigmaY: 10), + child: Container( + color: + Colors.white.withOpacity(0.8), + ), + ), + ), + ), + + Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(20), + child: InkWell( + onTap: () { + HapticFeedback.lightImpact(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: + Colors.transparent, + builder: (context) => + _buildDetailBottomSheet( + recommendation, index), + ); + }, + splashColor: + medalColor.withOpacity(0.1), + highlightColor: + medalColor.withOpacity(0.05), + borderRadius: + BorderRadius.circular(20), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // Header with medal banner + Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + vertical: 14, + horizontal: 16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + medalColor, + accentColor, + ], + ), + boxShadow: [ + BoxShadow( + color: medalColor + .withOpacity(0.3), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + programName, + style: TextStyle( + fontSize: 12, + color: Colors + .white + .withOpacity( + 0.9), + letterSpacing: + 0.5, + ), + maxLines: 1, + overflow: + TextOverflow + .ellipsis, + ), + SizedBox(height: 4), + ShimmerText( + text: minatName, + baseColor: + Colors.white, + highlightColor: + Colors.white + .withOpacity( + 0.7), + style: TextStyle( + fontSize: 18, + fontWeight: + FontWeight + .bold, + letterSpacing: + 0.5, + ), + ), + ], + ), + ), + Container( + padding: + EdgeInsets.symmetric( + horizontal: 10, + vertical: 6), + decoration: BoxDecoration( + color: Colors.white + .withOpacity(0.15), + borderRadius: + BorderRadius + .circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black + .withOpacity( + 0.1), + blurRadius: 4, + offset: + Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: + MainAxisSize.min, + children: [ + Text( + _getMedalEmoji( + index), + style: TextStyle( + fontSize: 16), + ), + SizedBox(width: 6), + ShimmerText( + text: + _getMedalLabel( + index), + baseColor: + Colors.white, + highlightColor: + accentColor + .withOpacity( + 0.8), + style: TextStyle( + fontWeight: + FontWeight + .bold, + fontSize: 12, + letterSpacing: 1, + ), + ), + ], + ), + ), + ], + ), + ), + + // Content + Padding( + padding: + const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // Score with circular indicator + Row( + children: [ + Container( + width: 60, + height: 60, + decoration: + BoxDecoration( + shape: + BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: medalColor + .withOpacity( + 0.2), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: Stack( + children: [ + // Animated circular progress + Positioned.fill( + child: + TweenAnimationBuilder< + double>( + tween: Tween< + double>( + begin: 0, + end: recommendation + .score / + 100), + duration: Duration( + milliseconds: + 1500), + curve: Curves + .easeOutCubic, + builder: + (context, + value, + child) { + return CustomPaint( + painter: + CircularScorePainter( + progress: + value, + color: + medalColor, + strokeWidth: + 5, + ), + ); + }, + ), + ), + + // Center text + Center( + child: Column( + mainAxisSize: + MainAxisSize + .min, + children: [ + Text( + '${recommendation.score}%', + style: + TextStyle( + fontSize: + 18, + fontWeight: + FontWeight.bold, + color: + medalColor, + ), + ), + Text( + 'Cocok', + style: + TextStyle( + fontSize: + 9, + fontWeight: + FontWeight.bold, + color: Colors + .grey + .shade600, + letterSpacing: + 0.5, + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox(width: 16), + Expanded( + child: Container( + padding: EdgeInsets + .symmetric( + horizontal: + 14, + vertical: + 10), + decoration: + BoxDecoration( + gradient: + LinearGradient( + begin: Alignment + .topLeft, + end: Alignment + .bottomRight, + colors: [ + medalColor + .withOpacity( + 0.1), + accentColor + .withOpacity( + 0.1), + ], + ), + borderRadius: + BorderRadius + .circular( + 12), + border: + Border.all( + color: medalColor + .withOpacity( + 0.3), + ), + ), + child: Text( + 'Kesesuaian tinggi dengan minat dan kemampuan Anda', + style: TextStyle( + fontSize: 13, + fontWeight: + FontWeight + .w500, + color: medalColor + .withOpacity( + 0.8), + ), + ), + ), + ), + ], + ), + + SizedBox(height: 20), + + // Careers and Majors with enhanced styling + Row( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + // Careers section + Expanded( + child: + _buildEnhancedInfoSection( + icon: Icons.work, + title: 'Karir', + items: + recommendation + .careers, + color: medalColor, + accentColor: + accentColor, + ), + ), + SizedBox(width: 16), + // Majors section + Expanded( + child: + _buildEnhancedInfoSection( + icon: Icons.school, + title: 'Jurusan', + items: + recommendation + .majors, + color: medalColor, + accentColor: + accentColor, + ), + ), + ], + ), + + // View Details button + Center( + child: Container( + margin: EdgeInsets.only( + top: 20), + decoration: + BoxDecoration( + gradient: + LinearGradient( + colors: [ + medalColor, + accentColor + ], + begin: Alignment + .topLeft, + end: Alignment + .bottomRight, + ), + borderRadius: + BorderRadius + .circular(20), + boxShadow: [ + BoxShadow( + color: medalColor + .withOpacity( + 0.4), + blurRadius: 10, + offset: + Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors + .transparent, + child: InkWell( + onTap: () { + HapticFeedback + .lightImpact(); + showModalBottomSheet( + context: + context, + isScrollControlled: + true, + backgroundColor: + Colors + .transparent, + builder: (context) => + _buildDetailBottomSheet( + recommendation, + index), + ); + }, + borderRadius: + BorderRadius + .circular( + 20), + splashColor: Colors + .white + .withOpacity( + 0.2), + highlightColor: + Colors.white + .withOpacity( + 0.1), + child: Padding( + padding: EdgeInsets + .symmetric( + horizontal: + 20, + vertical: + 12), + child: Row( + mainAxisSize: + MainAxisSize + .min, + children: [ + Icon( + Icons + .info_outline, + color: Colors + .white, + size: 16, + ), + SizedBox( + width: 8), + Text( + 'Lihat Detail Lengkap', + style: + TextStyle( + color: Colors + .white, + fontWeight: + FontWeight + .bold, + letterSpacing: + 0.5, + ), + ), + SizedBox( + width: 8), + Icon( + Icons + .arrow_forward, + color: Colors + .white, + size: 14, + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + + // Glass reflection overlay + Positioned( + top: 0, + left: 0, + right: 0, + height: 60, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withOpacity(0.3), + Colors.white.withOpacity(0.1), + Colors.white.withOpacity(0), + ], + stops: [0.0, 0.5, 1.0], + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + ), + ), + + // Floating sparkles + if (_itemControllers[index].value > 0.7) + ...List.generate( + 3, + (i) => Positioned( + top: 40 + (i * 40 * (i % 2 + 1)), + right: + 20 + (i * 20 * ((i + 1) % 2)), + child: AnimatedBuilder( + animation: + _itemControllers[index], + builder: (context, child) { + final sparkleAnim = math.sin( + _itemControllers[index] + .value * + (5 + i) * + math.pi); + return Opacity( + opacity: 0.4 + + (sparkleAnim.abs() * 0.6), + child: Transform.rotate( + angle: sparkleAnim * 0.3, + child: Icon( + Icons.star, + color: accentColor, + size: 8 + (i * 2), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ); + }, + ); + }, + ), + Container( + height: 800, + child: ScholarshipWidget( + assetPath: 'assets/data_beasiswa.json', + primaryColor: Color(0xFF1565C0), // Dark blue + accentColor: Color(0xFF42A5F5), // Medium blue + backgroundColor: Colors.white, + textColor: Colors.black87, + ), + ), + + // Swipe indicator yang lebih mencolok + Center( + child: Column( + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon( + Icons.swipe, + color: Colors.white, + size: 18, + ), + SizedBox(width: 8), + Text( + 'Swipe untuk melihat detail lebih lanjut', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + AnimatedBuilder( + animation: _swipePromptController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 8 * + math.sin( + _swipePromptController.value * math.pi * 2), + 0, + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + size: 20, + ), + ); + }, + ), + ], + ), + ), + + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildDetailCard(RecommendationItem recommendation, int index) { + final parts = recommendation.title.split('|'); + final programName = parts[0]; + final minatName = parts.length > 1 ? parts[1] : ''; + + // List emoji yang menarik untuk tiap kategori + final careerEmojis = ['👨‍💼', '👩‍💻', '👩‍🏫', '👨‍🔬', '👩‍⚕️', '👨‍🚀']; + final majorEmojis = ['📚', '🎓', '🔬', '💻', '🎨', '📊']; + + // Pilih emoji secara random tapi konsisten untuk tiap kategori + final random = math.Random(recommendation.title.hashCode); + final careerEmoji = careerEmojis[random.nextInt(careerEmojis.length)]; + final majorEmoji = majorEmojis[random.nextInt(majorEmojis.length)]; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: _getMedalColor(index).withOpacity(0.3), + blurRadius: 15, + offset: const Offset(0, 4), + ), + ], + border: Border.all( + color: _getMedalColor(index).withOpacity(0.5), + width: 1.5, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan warna medali dan animasi + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + _getMedalColor(index).withOpacity(0.2), + Colors.white.withOpacity(0.9), + ], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + border: Border( + bottom: BorderSide( + color: _getMedalColor(index).withOpacity(0.3), + width: 1, + ), + ), + ), + child: Row( + children: [ + AnimatedBuilder( + animation: _itemControllers[index], + builder: (context, child) { + return Transform.rotate( + angle: math.sin(_itemControllers[index].value * 6) * 0.1, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: _getMedalColor(index).withOpacity(0.2), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: _getMedalColor(index).withOpacity(0.3), + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + child: Text( + _getMedalEmoji(index), + style: const TextStyle(fontSize: 20), + ), + ), + ); + }, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + programName, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + Text( + minatName, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade900, + ), + ), + ], + ), + ), + // Badge skor dengan animasi pulse + AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Transform.scale( + scale: 1.0 + + 0.1 * math.sin(_cardController.value * 6 * math.pi), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.blue.shade100, + _getMedalColor(index).withOpacity(0.3), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: _getMedalColor(index).withOpacity(0.3), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.star, + color: _getMedalColor(index), + size: 16, + ), + const SizedBox(width: 4), + Text( + "${recommendation.score}%", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.indigo.shade900, + fontSize: 14, + ), + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), + + // Content dengan emoji untuk setiap bagian + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Karir dengan emoji + _buildInfoSectionWithEmoji( + 'Karir/Profesi Terkait:', + recommendation.careers, + careerEmoji, + Colors.orange, + animate: true, + ), + + const SizedBox(height: 16), + + // Jurusan dengan emoji + _buildInfoSectionWithEmoji( + 'Jurusan yang Disarankan:', + recommendation.majors, + majorEmoji, + Colors.green, + animate: true, + ), + + // Additional sections if available + if (recommendation.recommendedCourses != null && + recommendation.recommendedCourses!.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildInfoSectionWithEmoji( + 'Mata Kuliah Rekomendasi:', + recommendation.recommendedCourses!, + '📝', + Colors.purple, + animate: true, + ), + ], + + if (recommendation.recommendedUniversities != null && + recommendation.recommendedUniversities!.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildInfoSectionWithEmoji( + 'Universitas Rekomendasi:', + recommendation.recommendedUniversities!, + '🏛️', + Colors.blue, + animate: true, + ), + ], + + // RIASEC compatibility if available + if (recommendation.riasecCompatibility != null) ...[ + const SizedBox(height: 16), + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.purple.shade50, + shape: BoxShape.circle, + border: Border.all(color: Colors.purple.shade200), + ), + child: Text( + '🧠', + style: TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 8), + Text( + 'Kesesuaian RIASEC:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.grey.shade800, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.purple.shade50, + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.purple.shade200), + ), + child: Text( + '${(recommendation.riasecCompatibility! * 100).round()}%', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.purple.shade700, + fontSize: 12, + ), + ), + ), + ], + ), + ], + + if (recommendation.matchingRiasecCareers != null && + recommendation.matchingRiasecCareers!.isNotEmpty) ...[ + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(left: 26), + child: Text( + 'Karir RIASEC yang Cocok:', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(left: 26), + child: Wrap( + spacing: 6, + runSpacing: 6, + children: + recommendation.matchingRiasecCareers!.map((career) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.purple.shade50, + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.purple.shade200), + ), + child: Text( + career, + style: TextStyle( + fontSize: 12, + color: Colors.purple.shade700, + ), + ), + ); + }).toList(), + ), + ), + ], + + // Tombol detail dengan animasi + Padding( + padding: const EdgeInsets.only(top: 16), + child: Center( + child: AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Transform.scale( + scale: 1.0 + + 0.05 * + math.sin(_cardController.value * 4 * math.pi), + child: ElevatedButton.icon( + onPressed: () { + // Implementasi untuk menampilkan dialog atau halaman detail lebih lanjut + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => _buildDetailBottomSheet( + recommendation, index), + ); + }, + icon: Icon( + Icons.visibility, + color: Colors.white, + size: 18, + ), + label: Text( + 'Lihat Detail', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: _getMedalColor(index), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 5, + shadowColor: + _getMedalColor(index).withOpacity(0.5), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildInfoSectionWithEmoji( + String title, List items, String emoji, MaterialColor color, + {bool animate = false}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + animate + ? AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Transform.rotate( + angle: + math.sin(_cardController.value * 6 * math.pi) * 0.1, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: color.shade50, + shape: BoxShape.circle, + border: Border.all(color: color.shade200), + boxShadow: [ + BoxShadow( + color: color.shade200.withOpacity(0.5), + blurRadius: 4, + spreadRadius: 1, + ), + ], + ), + child: Text( + emoji, + style: const TextStyle(fontSize: 16), + ), + ), + ); + }, + ) + : Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: color.shade50, + shape: BoxShape.circle, + border: Border.all(color: color.shade200), + ), + child: Text( + emoji, + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: color.shade800, + ), + ), + ], + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(left: 26), + child: Wrap( + spacing: 6, + runSpacing: 6, + children: items.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + + return animate + ? AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + // Delay berdasarkan indeks untuk efek staggered + final delay = index * 0.1; + double opacity = 0.0; + + if (_cardController.value > delay) { + opacity = math.min( + 1.0, (_cardController.value - delay) / 0.3); + } + + return Opacity( + opacity: opacity, + child: Transform.translate( + offset: Offset( + (1.0 - opacity) * 20, + 0, + ), + child: child, + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: color.shade50, + borderRadius: BorderRadius.circular(30), + border: Border.all(color: color.shade200), + boxShadow: [ + BoxShadow( + color: color.shade100.withOpacity(0.5), + blurRadius: 3, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + item, + style: TextStyle( + fontSize: 12, + color: color.shade700, + fontWeight: FontWeight.w500, + ), + ), + ), + ) + : Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: color.shade50, + borderRadius: BorderRadius.circular(30), + border: Border.all(color: color.shade200), + ), + child: Text( + item, + style: TextStyle( + fontSize: 12, + color: color.shade700, + ), + ), + ); + }).toList(), + ), + ), + ], + ); + } + + Widget _buildDetailBottomSheet(RecommendationItem recommendation, int index) { + final parts = recommendation.title.split('|'); + final programName = parts[0]; + final minatName = parts.length > 1 ? parts[1] : ''; + + // Medal color and accent color (following design system from the first UI) + final Color medalColor = _getMedalColor(index); + final Color accentColor = index == 0 + ? Color(0xFFF7D154) // Gold accent + : index == 1 + ? Color(0xFFB3B6B7) // Silver accent + : Color(0xFFE59866); // Bronze accent + + return DraggableScrollableSheet( + initialChildSize: 0.85, + minChildSize: 0.5, + maxChildSize: 0.95, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: Stack( + children: [ + // Background subtle patterns + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + child: CustomPaint( + painter: AnimatedBackgroundPainter( + color: medalColor, + accentColor: accentColor, + animationValue: 0.5, // Fixed value for subtle animation + ), + ), + ), + ), + + // Blurred overlay for better readability + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Container( + color: Colors.white.withOpacity(0.95), + ), + ), + ), + ), + + Column( + children: [ + // Handle bar + Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 40, + height: 5, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + medalColor.withOpacity(0.5), + accentColor.withOpacity(0.5), + ], + ), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: medalColor.withOpacity(0.2), + blurRadius: 4, + spreadRadius: 1, + ), + ], + ), + ), + + // Medal icon + header + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + medalColor.withOpacity(0.15), + Colors.white.withOpacity(0.5), + ], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + medalColor.withOpacity(0.3), + accentColor.withOpacity(0.3), + ], + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: medalColor.withOpacity(0.3), + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + child: Text( + _getMedalEmoji(index), + style: const TextStyle(fontSize: 24), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + programName, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + letterSpacing: 0.5, + ), + ), + ShimmerText( + text: minatName, + baseColor: medalColor, + highlightColor: accentColor, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade900, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + medalColor.withOpacity(0.3), + accentColor.withOpacity(0.3), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: medalColor.withOpacity(0.3), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.star, + color: medalColor, + size: 16, + ), + const SizedBox(width: 4), + Text( + "${recommendation.score}%", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.indigo.shade900, + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + ), + + // Divider + Container( + height: 1, + margin: EdgeInsets.symmetric(horizontal: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + medalColor.withOpacity(0.3), + accentColor.withOpacity(0.3), + Colors.transparent, + ], + stops: [0.0, 0.3, 0.7, 1.0], + ), + ), + ), + + // Content + Expanded( + child: ListView( + controller: scrollController, + padding: const EdgeInsets.all(20), + children: [ + // Explanation + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + medalColor.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: + Border.all(color: medalColor.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: medalColor.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 0, + offset: Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + medalColor.withOpacity(0.2), + accentColor.withOpacity(0.2) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: Icon( + Icons.lightbulb_outline, + color: medalColor, + size: 18, + ), + ), + const SizedBox(width: 8), + Text( + 'Tentang ${minatName}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: medalColor, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Hasil analisis menunjukkan bahwa kamu memiliki kecocokan yang signifikan dengan bidang minat ini. Skor ${recommendation.score}% menggambarkan tingkat kesesuaian minatmu berdasarkan jawaban kuesioner.', + style: TextStyle( + color: Colors.black87, + height: 1.5, + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Career section with improved styling but same content + _buildDetailEnhancedInfoSection( + title: 'Karir dan Profesi', + items: recommendation.careers, + emoji: '👨‍💼', + color: medalColor, + accentColor: accentColor, + ), + + const SizedBox(height: 24), + + // Majors section with improved styling but same content + _buildDetailEnhancedInfoSection( + title: 'Jurusan yang Disarankan', + items: recommendation.majors, + emoji: '🎓', + color: medalColor, + accentColor: accentColor, + ), + + const SizedBox(height: 24), + + // Additional sections if available (maintaining exact same conditional logic) + if (recommendation.recommendedCourses != null && + recommendation.recommendedCourses!.isNotEmpty) ...[ + _buildDetailEnhancedInfoSection( + title: 'Mata Kuliah Rekomendasi', + items: recommendation.recommendedCourses!, + emoji: '📝', + color: medalColor, + accentColor: accentColor, + ), + const SizedBox(height: 24), + ], + + if (recommendation.recommendedUniversities != null && + recommendation + .recommendedUniversities!.isNotEmpty) ...[ + _buildDetailEnhancedInfoSection( + title: 'Universitas Rekomendasi', + items: recommendation.recommendedUniversities!, + emoji: '🏛️', + color: medalColor, + accentColor: accentColor, + ), + const SizedBox(height: 24), + ], + + // Rules section (why this recommendation) - preserving structure but enhancing style + if (recommendation.rules.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + accentColor.withOpacity(0.2), + medalColor.withOpacity(0.2) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + border: Border.all( + color: accentColor.withOpacity(0.3)), + ), + child: const Text( + '💡', + style: TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 8), + ShimmerText( + text: 'Mengapa Ini Direkomendasikan?', + baseColor: accentColor, + highlightColor: medalColor, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(left: 26), + child: Text( + 'Berdasarkan jawabanmu, kami menemukan kecocokan dengan minat ini karena:', + style: TextStyle( + color: Colors.grey.shade700, + fontSize: 14, + height: 1.5, + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 26), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: math.min( + 5, + recommendation.rules + .length), // Batasi ke 5 aturan saja + itemBuilder: (context, index) { + final rule = recommendation.rules[index]; + // Extract just the question text + final questionTextMatch = + RegExp(r'\[Pertanyaan: "(.*?)"\]') + .firstMatch(rule); + final questionText = + questionTextMatch?.group(1) ?? ''; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + accentColor.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + accentColor.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: accentColor.withOpacity(0.1), + blurRadius: 6, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + medalColor.withOpacity(0.2), + accentColor.withOpacity(0.2) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: Icon( + Icons.check, + color: medalColor, + size: 14, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + questionText, + style: const TextStyle( + fontSize: 14, + height: 1.4, + ), + ), + ), + ], + ), + ); + }, + ), + ), + + // "Show more" button if there are more than 5 rules + ], + ), + + // RIASEC placeholder with enhanced style but same content + const SizedBox(height: 24), + + const SizedBox(height: 40), + + // Action button with enhanced styling + Container( + margin: EdgeInsets.symmetric(horizontal: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [medalColor, accentColor], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: medalColor.withOpacity(0.4), + blurRadius: 15, + spreadRadius: 0, + offset: Offset(0, 6), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(30), + child: InkWell( + onTap: () { + Navigator.pop(context); + HapticFeedback.mediumImpact(); + }, + borderRadius: BorderRadius.circular(30), + splashColor: Colors.white.withOpacity(0.2), + highlightColor: Colors.white.withOpacity(0.1), + child: Container( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: Text( + 'Kembali', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + letterSpacing: 0.5, + ), + ), + ), + ), + ), + ), + ), + SizedBox(height: 20), + ], + ), + ), + ], + ), + + // Glass reflection overlay at top + Positioned( + top: 0, + left: 0, + right: 0, + height: 60, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withOpacity(0.3), + Colors.white.withOpacity(0.1), + Colors.white.withOpacity(0), + ], + stops: [0.0, 0.5, 1.0], + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + +// Enhanced info section specifically for the detail bottom sheet + Widget _buildDetailEnhancedInfoSection({ + required String title, + required List items, + required String emoji, + required Color color, + required Color accentColor, + }) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + color.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: color.withOpacity(0.3)), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 0, + offset: Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + color.withOpacity(0.2), + accentColor.withOpacity(0.2) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Text( + emoji, + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 8), + ShimmerText( + text: title, + baseColor: color, + highlightColor: accentColor, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 12), + ...items + .map((item) => Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + color.withOpacity(0.03), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.2)), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.05), + blurRadius: 6, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + color.withOpacity(0.2), + accentColor.withOpacity(0.2) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: Icon( + Icons.check, + color: color, + size: 14, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + item, + style: const TextStyle( + fontSize: 14, + height: 1.4, + ), + ), + ), + ], + ), + )) + .toList(), + ], + ), + ); + } + + Widget _buildDetailPage() { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 40), + Row( + children: [ + Expanded( + child: Text( + 'Analisis Terperinci 📊', + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + shadows: [ + Shadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + // Spinning gear animation + AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Transform.rotate( + angle: _cardController.value * math.pi * 2, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.analytics, + color: Colors.white, + size: 24, + ), + ), + ); + }, + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Lihat bagaimana rekomendasi ini didapatkan berdasarkan jawabanmu', + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + ), + ), + const SizedBox(height: 24), + + // Chart + _buildScoreChart(), + + const SizedBox(height: 24), + + // // Working Memory + // Container( + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(16), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.1), + // blurRadius: 10, + // offset: const Offset(0, 4), + // ), + // ], + // ), + // child: ExpansionTile( + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(16), + // ), + // leading: Container( + // padding: const EdgeInsets.all(8), + // decoration: BoxDecoration( + // color: Colors.blue.shade50, + // shape: BoxShape.circle, + // ), + // child: Text( + // '🧠', + // style: TextStyle(fontSize: 16), + // ), + // ), + // title: const Text( + // 'Data Jawaban (Working Memory)', + // style: TextStyle( + // fontWeight: FontWeight.bold, + // ), + // ), + // childrenPadding: const EdgeInsets.all(16), + // children: [ + // Text( + // 'Berikut adalah daftar fakta-fakta yang digunakan dalam proses analisis:', + // style: TextStyle( + // fontSize: 14, + // color: Colors.grey.shade700, + // ), + // ), + // const SizedBox(height: 12), + // Container( + // width: double.infinity, + // padding: const EdgeInsets.all(12), + // decoration: BoxDecoration( + // color: Colors.grey.shade100, + // borderRadius: BorderRadius.circular(8), + // border: Border.all(color: Colors.grey.shade300), + // ), + // child: Wrap( + // spacing: 8, + // runSpacing: 8, + // children: widget.result.workingMemory.map((fact) { + // final isYes = fact.contains('=Yes'); + // return Container( + // padding: const EdgeInsets.symmetric( + // horizontal: 8, vertical: 4), + // decoration: BoxDecoration( + // gradient: LinearGradient( + // colors: isYes + // ? [ + // Colors.green.shade50, + // Colors.green.shade100 + // ] + // : [Colors.red.shade50, Colors.red.shade100], + // begin: Alignment.topLeft, + // end: Alignment.bottomRight, + // ), + // borderRadius: BorderRadius.circular(6), + // border: Border.all( + // color: isYes + // ? Colors.green.shade300 + // : Colors.red.shade300, + // ), + // boxShadow: [ + // BoxShadow( + // color: isYes + // ? Colors.green.withOpacity(0.1) + // : Colors.red.withOpacity(0.1), + // blurRadius: 3, + // offset: const Offset(0, 1), + // ), + // ], + // ), + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Icon( + // isYes ? Icons.check_circle : Icons.cancel, + // color: isYes + // ? Colors.green.shade600 + // : Colors.red.shade600, + // size: 12, + // ), + // const SizedBox(width: 4), + // Text( + // fact, + // style: TextStyle( + // fontSize: 12, + // color: isYes + // ? Colors.green.shade800 + // : Colors.red.shade800, + // fontWeight: FontWeight.w500, + // ), + // ), + // ], + // ), + // ); + // }).toList(), + // ), + // ), + // ], + // ), + // ), + + const SizedBox(height: 24), + + // Steps explanation + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + shape: BoxShape.circle, + ), + child: Text( + '⚙️', + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 8), + const Text( + 'Proses Analisis', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + const Text( + 'Sistem ini menggunakan metode Forward Chaining untuk menentukan rekomendasi paling cocok untukmu. Berikut caranya:', + style: TextStyle(fontSize: 14), + ), + const SizedBox(height: 16), + + // Animasi steps yang muncul satu per satu + AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Column( + children: [ + _buildAnimatedAnalysisStep( + '1', + 'Jawaban kuesioner diubah menjadi fakta', + 'Sistem mengumpulkan semua jawaban "Ya" dan "Tidak" dari kuesioner yang kamu isi.', + Colors.blue, + _cardController.value > 0.2 + ? math.min( + 1.0, (_cardController.value - 0.2) / 0.2) + : 0.0, + ), + _buildAnimatedAnalysisStep( + '2', + 'Fakta dicocokkan dengan aturan', + 'Setiap jawaban "Ya" akan menambah skor pada minat yang sesuai.', + Colors.purple, + _cardController.value > 0.3 + ? math.min( + 1.0, (_cardController.value - 0.3) / 0.2) + : 0.0, + ), + _buildAnimatedAnalysisStep( + '3', + 'Skor dihitung untuk setiap minat', + 'Sistem menghitung persentase kecocokan berdasarkan total bobot.', + Colors.orange, + _cardController.value > 0.4 + ? math.min( + 1.0, (_cardController.value - 0.4) / 0.2) + : 0.0, + ), + _buildAnimatedAnalysisStep( + '4', + 'Hasil diurutkan', + 'Rekomendasi ditampilkan berdasarkan skor tertinggi.', + Colors.green, + _cardController.value > 0.5 + ? math.min( + 1.0, (_cardController.value - 0.5) / 0.2) + : 0.0, + isLast: true, + ), + ], + ); + }, + ), + ], + ), + ), + + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildExplanationPage() { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 40), + Row( + children: [ + Expanded( + child: Text( + 'Apa Artinya Ini? 🤔', + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + shadows: [ + Shadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + // Thinking emoji animation + AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + 5 * math.sin(_cardController.value * 3 * math.pi), + ), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Text( + '🤔', + style: const TextStyle(fontSize: 20), + ), + ), + ); + }, + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Penjelasan sederhana tentang hasil analisis dan apa yang bisa kamu lakukan selanjutnya', + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + ), + ), + const SizedBox(height: 24), + + // Explanation card with animated points + AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + // Hitung progress animasi untuk masing-masing poin + final point1Opacity = _cardController.value > 0.2 + ? math.min(1.0, (_cardController.value - 0.2) / 0.2) + : 0.0; + final point2Opacity = _cardController.value > 0.3 + ? math.min(1.0, (_cardController.value - 0.3) / 0.2) + : 0.0; + final point3Opacity = _cardController.value > 0.4 + ? math.min(1.0, (_cardController.value - 0.4) / 0.2) + : 0.0; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white, Colors.blue.shade50], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: Colors.blue.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.amber.shade100, + shape: BoxShape.circle, + ), + child: Text( + '💡', + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 8), + const Text( + 'Apa Arti Hasil Ini?', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + const Text( + 'Hasil analisis ini menunjukkan bidang minat yang paling sesuai denganmu berdasarkan jawaban yang kamu berikan dalam kuesioner.', + style: TextStyle(fontSize: 14), + ), + const SizedBox(height: 16), + _buildAnimatedExplanationPoint( + 'Persentase yang tinggi menunjukkan kecocokan yang lebih baik antara minatmu dan bidang tersebut.', + Icons.bar_chart, + Colors.blue, + point1Opacity, + ), + _buildAnimatedExplanationPoint( + 'Rekomendasi karir dan jurusan dapat membantumu untuk merencanakan pendidikan dan masa depan.', + Icons.school, + Colors.green, + point2Opacity, + ), + _buildAnimatedExplanationPoint( + 'Kamu bisa mendiskusikan hasil ini dengan guru BK, orang tua, atau konselor untuk mendapat saran lebih lanjut.', + Icons.people, + Colors.purple, + point3Opacity, + ), + ], + ), + ); + }, + ), + + const SizedBox(height: 24), + + // What next card + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white, Colors.green.shade50], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: Colors.green.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.indigo.shade100, + shape: BoxShape.circle, + ), + child: Text( + '🚀', + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 8), + const Text( + 'Langkah Selanjutnya', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Animated steps with path connector + AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Column( + children: [ + _buildAnimatedNextStep( + 'Jelajahi', + 'Cari tahu lebih banyak tentang jurusan dan karir yang direkomendasikan.', + Icons.explore, + Colors.blue, + _cardController.value > 0.2 + ? math.min( + 1.0, (_cardController.value - 0.2) / 0.2) + : 0.0, + ), + _buildAnimatedNextStep( + 'Diskusikan', + 'Bicarakan hasil ini dengan guru, orang tua, atau konselor.', + Icons.chat, + Colors.orange, + _cardController.value > 0.3 + ? math.min( + 1.0, (_cardController.value - 0.3) / 0.2) + : 0.0, + ), + _buildAnimatedNextStep( + 'Refleksikan', + 'Pikirkan apakah rekomendasi ini sesuai dengan apa yang kamu inginkan.', + Icons.self_improvement, + Colors.purple, + _cardController.value > 0.4 + ? math.min( + 1.0, (_cardController.value - 0.4) / 0.2) + : 0.0, + ), + _buildAnimatedNextStep( + 'Rencanakan', + 'Buat rencana pendidikan dan karir berdasarkan minatmu.', + Icons.checklist, + Colors.green, + _cardController.value > 0.5 + ? math.min( + 1.0, (_cardController.value - 0.5) / 0.2) + : 0.0, + isLast: true, + ), + ], + ); + }, + ), + ], + ), + ), + + const SizedBox(height: 24), + + // RIASEC info card with animated gradient + + const SizedBox(height: 32), + + // Share button with floating animation + Center( + child: AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + 4 * math.sin(_cardController.value * 3 * math.pi), + ), + child: ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CertificateReccomendationPage( + result: widget + .result, // Gunakan DateTime.now().toString() atau format yang diinginkan + ), + ), + ); + }, + icon: const Icon(Icons.share), + label: const Text( + 'Cetak Sertifikat', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.indigo.shade900, + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 5, + shadowColor: Colors.black.withOpacity(0.3), + ), + ), + ); + }, + ), + ), + + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildScoreChart() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: Colors.blue.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade50, + shape: BoxShape.circle, + ), + child: Text( + '📊', + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 8), + const Text( + 'Grafik Kesesuaian Minat', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 4), + const Text( + 'Berdasarkan jawaban kuesionermu', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + const SizedBox(height: 20), + SizedBox( + height: 220, + child: AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: 100, + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final recommendation = + widget.result.recommendations[groupIndex]; + final parts = recommendation.title.split('|'); + final minatName = + parts.length > 1 ? parts[1] : parts[0]; + + return BarTooltipItem( + '$minatName\n', + const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: '${rod.toY.round()}%', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + }, + ), + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index < 0 || + index >= widget.result.recommendations.length) { + return const SizedBox.shrink(); + } + final parts = widget + .result.recommendations[index].title + .split('|'); + String label = + parts.length > 1 ? parts[1] : parts[0]; + if (label.length > 8) { + label = '${label.substring(0, 6)}...'; + } + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: index < 3 + ? _getMedalColor(index) + : Colors.grey.shade700, + ), + ), + ); + }, + reservedSize: 30, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + if (value == 0) return const SizedBox(); + return Padding( + padding: const EdgeInsets.only(right: 4), + child: Text( + '${value.toInt()}%', + style: const TextStyle( + color: Colors.grey, + fontSize: 10, + ), + ), + ); + }, + reservedSize: 30, + interval: 25, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: FlGridData( + show: true, + horizontalInterval: 25, + getDrawingHorizontalLine: (value) => FlLine( + color: Colors.grey.shade200, + strokeWidth: 1, + dashArray: [5, 5], + ), + ), + borderData: FlBorderData(show: false), + barGroups: widget.result.recommendations + .asMap() + .entries + .map((entry) { + final index = entry.key; + final item = entry.value; + + // Animasi bar dengan cardController + final barProgress = _cardController.value * + 1.3; // Sedikit lebih cepat dari nilai controller + final barHeight = math.min(1.0, barProgress) * item.score; + + // Set warna berdasarkan peringkat + Color barColor; + if (index == 0) { + barColor = _goldColor; + } else if (index == 1) { + barColor = _silverColor; + } else if (index == 2) { + barColor = _bronzeColor; + } else { + barColor = Colors.grey; + } + + // Animasi spark di puncak bar + final bool showSpark = barProgress >= 0.95 && index < 3; + + return BarChartGroupData( + x: index, + barRods: [ + BarChartRodData( + toY: barHeight, + color: barColor, + width: 20, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + backDrawRodData: BackgroundBarChartRodData( + show: true, + toY: 100, + color: Colors.grey.shade200, + ), + rodStackItems: showSpark + ? [ + BarChartRodStackItem( + barHeight - 5, + barHeight + 2, + Colors.white.withOpacity(math.sin( + _cardController.value * + 10 * + math.pi) * + 0.5 + + 0.5), + ), + ] + : [], + ), + ], + ); + }).toList(), + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildAnimatedAnalysisStep( + String number, + String title, + String description, + MaterialColor color, + double progress, { + bool isLast = false, + }) { + return Opacity( + opacity: progress, + child: Transform.translate( + offset: Offset((1.0 - progress) * 50, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: color.shade50, + shape: BoxShape.circle, + border: Border.all(color: color.shade300), + boxShadow: [ + BoxShadow( + color: color.shade100.withOpacity(0.5), + blurRadius: 4, + spreadRadius: 1, + ), + ], + ), + child: Center( + child: Text( + number, + style: TextStyle( + color: color.shade700, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: color.shade700, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + if (!isLast) + Container( + margin: const EdgeInsets.only(top: 8, bottom: 8), + height: 24, + width: 2, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color.shade300, color.shade100], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildAnimatedExplanationPoint( + String text, IconData icon, MaterialColor color, double progress) { + return Opacity( + opacity: progress, + child: Transform.translate( + offset: Offset((1.0 - progress) * 50, 0), + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: color.shade50, + shape: BoxShape.circle, + border: Border.all(color: color.shade200), + boxShadow: [ + BoxShadow( + color: color.shade100.withOpacity(0.5), + blurRadius: 4, + spreadRadius: 1, + ), + ], + ), + child: Icon( + icon, + color: color.shade600, + size: 14, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade800, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildAnimatedNextStep( + String title, + String description, + IconData icon, + MaterialColor color, + double progress, { + bool isLast = false, + }) { + return Opacity( + opacity: progress, + child: Transform.translate( + offset: Offset((1.0 - progress) * 50, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + final pulseScale = + 1.0 + 0.1 * math.sin(_cardController.value * 6 * math.pi); + return Transform.scale( + scale: pulseScale, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.shade50, + shape: BoxShape.circle, + border: Border.all(color: color.shade200), + boxShadow: [ + BoxShadow( + color: color.shade100.withOpacity(0.5 * pulseScale), + blurRadius: 4 * pulseScale, + spreadRadius: 1 * pulseScale, + ), + ], + ), + child: Icon( + icon, + color: color.shade600, + size: 18, + ), + ), + ); + }, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: color.shade700, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + if (!isLast) const SizedBox(height: 16), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildAnimatedRiasecItem( + String letter, String fullName, MaterialColor color, double progress) { + final effectiveProgress = math.max(0.0, math.min(1.0, progress)); + + return Expanded( + child: Opacity( + opacity: effectiveProgress, + child: Transform.translate( + offset: Offset((1.0 - effectiveProgress) * 30, 0), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + children: [ + AnimatedBuilder( + animation: _cardController, + builder: (context, child) { + return Transform.rotate( + angle: effectiveProgress > 0.5 + ? math.sin(_cardController.value * 5 * math.pi) * 0.1 + : 0, + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: color.shade100, + shape: BoxShape.circle, + boxShadow: effectiveProgress > 0.7 + ? [ + BoxShadow( + color: color.shade200.withOpacity(0.5), + blurRadius: 4, + spreadRadius: 1, + ), + ] + : null, + ), + child: Center( + child: Text( + letter, + style: TextStyle( + color: color.shade700, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ), + ); + }, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + fullName, + style: TextStyle( + fontSize: 12, + color: color.shade800, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildPageIndicator() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(3, (index) { + return GestureDetector( + onTap: () { + _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(horizontal: 4), + width: _currentPage == index ? 24 : 10, + height: 8, + decoration: BoxDecoration( + color: _currentPage == index + ? Colors.white + : Colors.white.withOpacity(0.4), + borderRadius: BorderRadius.circular(4), + boxShadow: _currentPage == index + ? [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 4, + spreadRadius: 1, + ), + ] + : null, + ), + ), + ); + }), + ); + } + + Widget _buildConfetti() { + return AnimatedBuilder( + animation: _confettiController, + builder: (context, child) { + final particles = []; + final random = math.Random(42); // Fixed seed for consistent generation + + // Generate particles dinamis + for (int i = 0; i < 100; i++) { + final size = random.nextDouble() * 10 + 5; + + // Colors based on medal colors plus some extras + final List particleColors = [ + _goldColor, + _silverColor, + _bronzeColor, + Colors.red.shade400, + Colors.blue.shade400, + Colors.green.shade400, + Colors.purple.shade400, + Colors.pink.shade300, + ]; + + final color = particleColors[random.nextInt(particleColors.length)] + .withOpacity(0.8); + + // Posisi awal (dari tengah atas layar dengan spread) + final screenWidth = MediaQuery.of(context).size.width; + final initialX = screenWidth * 0.5 + + (random.nextDouble() - 0.5) * screenWidth * 0.8; + final initialY = -size * 2; + + // Faktor waktu (0.0 - 1.0) + final time = _confettiController.value; + + // Velocity dengan variasi + final vx = (random.nextDouble() - 0.5) * 300; + final vy = random.nextDouble() * 400 + 200; + + // Posisi saat ini dengan gravitasi + final x = initialX + vx * time; + final y = initialY + + vy * time + + 400 * time * time; // Dengan akselerasi gravitasi + + // Rotasi dan randomness + final rotation = random.nextDouble() * 360 * math.pi / 180; + final rotationSpeed = (random.nextDouble() - 0.5) * 4; + final currentRotation = rotation + rotationSpeed * time * 10; + + // Ukuran sesuai dengan fase animasi + final sizeMultiplier = 1.0; + + // Opacity untuk fade out - PERBAIKAN: Pastikan nilai opacity selalu di antara 0.0 dan 1.0 + double opacity = 1.0; + if (time > 0.7) { + // Hitung opacity dan pastikan dalam range yang valid + opacity = math.max(0.0, math.min(1.0, 1.0 - (time - 0.7) / 0.3)); + } + + // Bentuk partikel (circle, square, star/custom) + final shape = random.nextInt(3); + + // Jangan tampilkan partikel yang sudah keluar layar + if (y > MediaQuery.of(context).size.height || + x < 0 || + x > screenWidth) continue; + + // Tambahkan ke daftar partikel + particles.add( + Positioned( + left: x, + top: y, + child: Transform.rotate( + angle: currentRotation, + child: Opacity( + opacity: opacity, + child: shape == 0 + ? Container( + // Circle + width: size * sizeMultiplier, + height: size * sizeMultiplier, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ) + : shape == 1 + ? Container( + // Square + width: size * sizeMultiplier, + height: size * sizeMultiplier, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2), + ), + ) + : CustomPaint( + // Star + size: Size(size * 1.2 * sizeMultiplier, + size * 1.2 * sizeMultiplier), + painter: StarPainter(color: color), + ), + ), + ), + ), + ); + } + + return Stack( + children: particles, + ); + }, + ); + } + + Widget _buildSimpleInfoSection({ + required String title, + required IconData icon, + required List items, + required Color color, + }) { + // Menampilkan maksimal 2 item + final displayItems = items.take(2).toList(); + final hasMore = items.length > 2; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Row( + children: [ + Icon( + icon, + color: color, + size: 14, + ), + const SizedBox(width: 4), + Text( + title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + const SizedBox(height: 4), + // Items dalam Column dengan ukuran terbatas + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Daftar item + for (var item in displayItems) + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + "• $item", + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade700, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + // Indikator "lainnya" + if (hasMore) + Text( + "+ ${items.length - 2} lainnya...", + style: TextStyle( + fontSize: 10, + fontStyle: FontStyle.italic, + color: Colors.grey.shade500, + ), + ), + ], + ), + ], + ); + } + +// Tambahkan fungsi helper untuk label dan warna + String _getCompatibilityLabel(int index) { + switch (index) { + case 0: + return 'Sangat Direkomendasikan'; + case 1: + return 'Direkomendasikan'; + case 2: + return 'Kurang Direkomendasikan'; + default: + return 'Cukup'; + } + } + + Color _getCompatibilityColor(int index) { + switch (index) { + case 0: + return Colors.blue.shade700; + case 1: + return Colors.teal.shade600; + case 2: + return Colors.green.shade600; + default: + return Colors.grey.shade600; + } + } + + String _getCompatibilityEmoji(int index) { + switch (index) { + case 0: + return '⭐'; + case 1: + return '✨'; + case 2: + return '👍'; + default: + return '✓'; + } + } + + Widget _buildCompactInfoItem({ + required IconData icon, + required String label, + required String content, + required Color color, + }) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + icon, + size: 14, + color: color, + ), + SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + Text( + content, + style: TextStyle( + fontSize: 12, + color: Colors.black87, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ); + } + +// Helper untuk section info yang responsif + Widget _buildResponsiveInfoSection({ + required IconData icon, + required String title, + required List items, + required Color color, + }) { + // Tentukan berapa item yang ditampilkan berdasarkan jumlah item + final int itemsToShow = items.length > 2 ? 2 : items.length; + final bool hasMore = items.length > itemsToShow; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Icon( + icon, + size: 16, + color: color, + ), + SizedBox(width: 6), + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + SizedBox(height: 6), + + // Items + ...items.take(itemsToShow).map((item) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "• ", + style: TextStyle( + fontWeight: FontWeight.bold, + color: color, + ), + ), + Expanded( + child: Text( + item, + style: TextStyle( + fontSize: 12, + color: Colors.black87, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + }).toList(), + + // "More" indicator if needed + if (hasMore) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + "+ ${items.length - itemsToShow} lainnya", + style: TextStyle( + fontSize: 11, + fontStyle: FontStyle.italic, + color: color, + ), + ), + ), + ], + ); + } + + Widget _buildEnhancedInfoSection({ + required IconData icon, + required String title, + required List items, + required Color color, + required Color accentColor, + }) { + return Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.1), + blurRadius: 8, + offset: Offset(0, 3), + ), + ], + border: Border.all( + color: color.withOpacity(0.2), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + color.withOpacity(0.2), + accentColor.withOpacity(0.2) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: color, + size: 16, + ), + ), + SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: color, + letterSpacing: 0.5, + ), + ), + ], + ), + SizedBox(height: 12), + ...items + .take(3) + .map((item) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.arrow_right, + size: 16, + color: color.withOpacity(0.7), + ), + SizedBox(width: 4), + Expanded( + child: Text( + item, + style: TextStyle( + fontSize: 13, + color: Colors.black87, + ), + ), + ), + ], + ), + )) + .toList(), + if (items.length > 3) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + '+${items.length - 3} lainnya', + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } +} + +// Animated Background Painter +class AnimatedBackgroundPainter extends CustomPainter { + final Color color; + final Color accentColor; + final double animationValue; + + AnimatedBackgroundPainter({ + required this.color, + required this.accentColor, + required this.animationValue, + }); + + @override + void paint(Canvas canvas, Size size) { + final random = math.Random(42); + + // Draw floating circles + for (int i = 0; i < 12; i++) { + final paint = Paint() + ..color = color.withOpacity(0.05 + (i % 3) * 0.03) + ..style = PaintingStyle.fill; + + final baseX = random.nextDouble() * size.width; + final baseY = random.nextDouble() * size.height; + final baseRadius = 5.0 + random.nextDouble() * 20; + + // Add animated movement + final offsetX = math.sin((animationValue * 2 + i) * math.pi) * 10; + final offsetY = math.cos((animationValue * 2 + i) * math.pi) * 10; + + canvas.drawCircle( + Offset(baseX + offsetX, baseY + offsetY), + baseRadius, + paint, + ); + } + + // Draw thin decorative lines + for (int i = 0; i < 8; i++) { + final linePaint = Paint() + ..color = accentColor.withOpacity(0.1) + ..style = PaintingStyle.stroke + ..strokeWidth = 0.5; + + final startX = random.nextDouble() * size.width; + final startY = random.nextDouble() * size.height; + final angle = random.nextDouble() * math.pi * 2; + final len = 30.0 + random.nextDouble() * 50; + + // Animate line rotation + final rotatedAngle = angle + (animationValue * math.pi / 8); + + canvas.drawLine( + Offset(startX, startY), + Offset( + startX + math.cos(rotatedAngle) * len, + startY + math.sin(rotatedAngle) * len, + ), + linePaint, + ); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} + +// Medal Badge Painter +class MedalBadgePainter extends CustomPainter { + final Color color; + final Color accentColor; + final String emoji; + + MedalBadgePainter({ + required this.color, + required this.accentColor, + required this.emoji, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + // Draw corner badge + final path = Path(); + path.moveTo(0, 0); + path.lineTo(size.width, 0); + path.lineTo(0, size.height); + path.close(); + + canvas.drawPath(path, paint); + + // Draw emoji text + final textPainter = TextPainter( + text: TextSpan( + text: emoji, + style: TextStyle( + fontSize: 18, + ), + ), + textDirection: TextDirection.ltr, + ); + + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + size.width * 0.2 - textPainter.width / 2, + size.height * 0.2 - textPainter.height / 2, + ), + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +// Circular Score Painter +class CircularScorePainter extends CustomPainter { + final double progress; + final Color color; + final double strokeWidth; + + CircularScorePainter({ + required this.progress, + required this.color, + this.strokeWidth = 4.0, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = math.min(size.width, size.height) / 2 - strokeWidth / 2; + + // Background circle + final bgPaint = Paint() + ..color = Colors.grey.shade200 + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth; + + canvas.drawCircle(center, radius, bgPaint); + + // Progress arc + final progressPaint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round; + + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + -math.pi / 2, // Start from top + progress * 2 * math.pi, // Convert progress to radians + false, + progressPaint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + if (oldDelegate is CircularScorePainter) { + return oldDelegate.progress != progress; + } + return true; + } +} + +// Shimmer Text Widget +class ShimmerText extends StatefulWidget { + final String text; + final Color baseColor; + final Color highlightColor; + final TextStyle style; + + const ShimmerText({ + Key? key, + required this.text, + required this.baseColor, + required this.highlightColor, + required this.style, + }) : super(key: key); + + @override + _ShimmerTextState createState() => _ShimmerTextState(); +} + +class _ShimmerTextState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 1500), + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return ShaderMask( + shaderCallback: (bounds) => LinearGradient( + colors: [ + widget.baseColor, + widget.highlightColor, + widget.baseColor, + ], + stops: [ + 0.0, + _controller.value, + 1.0, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + transform: GradientRotation(_controller.value * math.pi * 2), + ).createShader(bounds), + child: Text( + widget.text, + style: widget.style.copyWith(color: Colors.white), + ), + ); + }, + ); + } +} + +class LightPatternPainter extends CustomPainter { + final Color color; + final int seed; + + LightPatternPainter({required this.color, required this.seed}); + + @override + void paint(Canvas canvas, Size size) { + final random = math.Random(seed); + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 0.5; + + // Gambar pola dot ringan + const int dotCount = 30; + for (int i = 0; i < dotCount; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + + canvas.drawCircle( + Offset(x, y), + random.nextDouble() * 1.5 + 0.5, + paint, + ); + } + + // Gambar garis dekoratif ringan + for (int i = 0; i < 3; i++) { + final startX = random.nextDouble() * size.width; + final startY = random.nextDouble() * size.height; + final endX = startX + (random.nextDouble() - 0.5) * 30; + final endY = startY + (random.nextDouble() - 0.5) * 30; + + canvas.drawLine( + Offset(startX, startY), + Offset(endX, endY), + paint, + ); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +// Custom painter untuk partikel bintang +class StarPainter extends CustomPainter { + final Color color; + + StarPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final Paint paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + final double centerX = size.width / 2; + final double centerY = size.height / 2; + final double radius = size.width / 2; + + final path = Path(); + + for (int i = 0; i < 5; i++) { + final double outerAngle = i * math.pi * 2 / 5 - math.pi / 2; + final double innerAngle = outerAngle + math.pi / 5; + + final double outerX = centerX + radius * math.cos(outerAngle); + final double outerY = centerY + radius * math.sin(outerAngle); + + final double innerX = centerX + radius * 0.4 * math.cos(innerAngle); + final double innerY = centerY + radius * 0.4 * math.sin(innerAngle); + + if (i == 0) { + path.moveTo(outerX, outerY); + } else { + path.lineTo(outerX, outerY); + } + + path.lineTo(innerX, innerY); + } + + path.close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } +} + +class Scholarship { + final String nama; + final String penyelenggara; + final List jenjang; + final List bidangStudi; + final String deskripsi; + final List persyaratan; + final String deadline; + final String link; + + Scholarship({ + required this.nama, + required this.penyelenggara, + required this.jenjang, + required this.bidangStudi, + required this.deskripsi, + required this.persyaratan, + required this.deadline, + required this.link, + }); + + factory Scholarship.fromJson(Map json) { + return Scholarship( + nama: json['nama'], + penyelenggara: json['penyelenggara'], + jenjang: List.from(json['jenjang']), + bidangStudi: List.from(json['bidang_studi']), + deskripsi: json['deskripsi'], + persyaratan: List.from(json['persyaratan']), + deadline: json['deadline'], + link: json['link'], + ); + } +} + +class ScholarshipWidget extends StatefulWidget { + final String assetPath; + final Color primaryColor; + final Color accentColor; + final Color backgroundColor; + final Color textColor; + + const ScholarshipWidget({ + Key? key, + required this.assetPath, + this.primaryColor = const Color(0xFF2196F3), // Default blue + this.accentColor = const Color(0xFF64B5F6), // Lighter blue + this.backgroundColor = Colors.white, + this.textColor = Colors.black87, + }) : super(key: key); + + @override + _ScholarshipWidgetState createState() => _ScholarshipWidgetState(); +} + +class _ScholarshipWidgetState extends State { + List scholarships = []; + bool isLoading = true; + String? searchQuery; + String? selectedFilter; + final List filterOptions = ["Semua", "S1", "S2", "S3"]; + + @override + void initState() { + super.initState(); + loadScholarships(); + } + + Future loadScholarships() async { + try { + // Use package:flutter/services.dart to directly load the asset + final String jsonString = + await rootBundle.rootBundle.loadString(widget.assetPath); + final dynamic jsonData = jsonDecode(jsonString); + + setState(() { + scholarships = (jsonData['beasiswa'] as List) + .map((item) => Scholarship.fromJson(item)) + .toList(); + isLoading = false; + }); + } catch (e) { + setState(() { + isLoading = false; + }); + print('Error loading scholarships: $e'); + } + } + + List getFilteredScholarships() { + if (searchQuery == null && selectedFilter == null || + selectedFilter == "Semua") { + return scholarships; + } + + return scholarships.where((scholarship) { + bool matchesSearch = searchQuery == null || + scholarship.nama.toLowerCase().contains(searchQuery!.toLowerCase()) || + scholarship.penyelenggara + .toLowerCase() + .contains(searchQuery!.toLowerCase()) || + scholarship.deskripsi + .toLowerCase() + .contains(searchQuery!.toLowerCase()); + + bool matchesFilter = selectedFilter == null || + selectedFilter == "Semua" || + scholarship.jenjang.contains(selectedFilter); + + return matchesSearch && matchesFilter; + }).toList(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text( + 'Beasiswa Tersedia', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + _buildSearchAndFilter(), + SizedBox(height: 8), + if (isLoading) + Center( + child: CircularProgressIndicator( + color: widget.primaryColor, + ), + ) + else + _buildScholarshipList(), + ], + ); + } + + Widget _buildSearchAndFilter() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Container( + color: Colors.white, + child: TextField( + onChanged: (value) { + setState(() { + searchQuery = value.isEmpty ? null : value; + }); + }, + decoration: InputDecoration( + hintText: 'Cari beasiswa...', + prefixIcon: Icon(Icons.search, color: widget.primaryColor), + filled: true, + fillColor: widget.accentColor.withOpacity(0.1), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: widget.primaryColor), + ), + ), + ), + ), + SizedBox(height: 8), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: filterOptions.map((filter) { + bool isSelected = selectedFilter == filter || + (selectedFilter == null && filter == "Semua"); + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: FilterChip( + label: Text( + filter, + style: TextStyle( + color: isSelected ? Colors.blue : widget.textColor, + ), + ), + selected: isSelected, + onSelected: (selected) { + setState(() { + selectedFilter = selected ? filter : null; + }); + }, + backgroundColor: widget.backgroundColor, + selectedColor: widget.primaryColor, + checkmarkColor: Colors.blue, + ), + ); + }).toList(), + ), + ), + ], + ), + ); + } + + Widget _buildScholarshipList() { + final filteredList = getFilteredScholarships(); + + if (filteredList.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + children: [ + Icon(Icons.search_off, size: 64, color: widget.accentColor), + SizedBox(height: 16), + Text( + 'Tidak ada beasiswa yang sesuai dengan pencarian Anda', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: widget.textColor, + ), + ), + ], + ), + ), + ); + } + + return Expanded( + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16), + itemCount: filteredList.length, + itemBuilder: (context, index) { + final scholarship = filteredList[index]; + return Card( + margin: EdgeInsets.only(bottom: 16), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: ExpansionTile( + tilePadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + childrenPadding: EdgeInsets.all(16), + title: Text( + scholarship.nama, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: widget.primaryColor, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 4), + Text(scholarship.penyelenggara), + SizedBox(height: 4), + Row( + children: [ + ...scholarship.jenjang.map((level) => Padding( + padding: const EdgeInsets.only(right: 4.0), + child: Chip( + label: Text( + level, + style: TextStyle( + fontSize: 10, + color: Colors.white, + ), + ), + backgroundColor: widget.primaryColor, + padding: EdgeInsets.all(0), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + )), + ], + ), + ], + ), + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + scholarship.deskripsi, + style: TextStyle( + fontSize: 14, + color: widget.textColor, + ), + ), + SizedBox(height: 12), + Text( + 'Bidang Studi:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: widget.primaryColor, + ), + ), + Wrap( + spacing: 4, + children: scholarship.bidangStudi + .map((field) => Chip( + label: Text( + field, + style: TextStyle( + fontSize: 12, + color: widget.textColor, + ), + ), + backgroundColor: + widget.accentColor.withOpacity(0.2), + )) + .toList(), + ), + SizedBox(height: 12), + Text( + 'Persyaratan:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: widget.primaryColor, + ), + ), + SizedBox(height: 4), + ...scholarship.persyaratan.map((requirement) => Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.check_circle, + size: 16, + color: widget.accentColor, + ), + SizedBox(width: 8), + Expanded( + child: Text( + requirement, + style: TextStyle( + fontSize: 14, + color: widget.textColor, + ), + ), + ), + ], + ), + )), + SizedBox(height: 12), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 16, + color: widget.accentColor, + ), + SizedBox(width: 8), + Text( + 'Deadline: ${scholarship.deadline}', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: widget.textColor, + ), + ), + ], + ), + SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + // Here you would open the link, using url_launcher package + print('Opening link: ${scholarship.link}'); + }, + child: Text('Lihat Detail'), + style: ElevatedButton.styleFrom( + backgroundColor: widget.primaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/app/views/student/feature/recomendation_screen/view/page_recomendation_screen_history.dart b/lib/app/views/student/feature/recomendation_screen/view/page_recomendation_screen_history.dart new file mode 100644 index 0000000..0556dfc --- /dev/null +++ b/lib/app/views/student/feature/recomendation_screen/view/page_recomendation_screen_history.dart @@ -0,0 +1,728 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/app/views/about/page_about.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/recomendation_screen/view/page_recmendation_detail_screen.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:firebase_core/firebase_core.dart'; +import 'package:intl/intl.dart' as intl; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:url_launcher/url_launcher.dart'; + +class RecommendationHistoryPage extends StatelessWidget { + const RecommendationHistoryPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final User? currentUser = FirebaseAuth.instance.currentUser; + + return Scaffold( + appBar: AppBar( + title: const Text('Riwayat Rekomendasi'), + backgroundColor: Colors.indigo.shade800, + foregroundColor: Colors.white, + elevation: 0, + actions: [ + IconButton( + onPressed: () { + // Refresh action + Get.snackbar( + 'Refresh', + 'Menyegarkan data...', + backgroundColor: Colors.indigo.shade100, + colorText: Colors.indigo.shade800, + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 1), + ); + }, + icon: const Icon(Icons.refresh), + ), + ], + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.indigo.shade800, + Colors.blue.shade800, + ], + ), + ), + child: SafeArea( + child: FutureBuilder( + // First get the school ID + future: () async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('school_id') ?? ''; + }(), + builder: (context, schoolSnapshot) { + if (schoolSnapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ); + } + + if (schoolSnapshot.hasError || + !schoolSnapshot.hasData || + schoolSnapshot.data!.isEmpty) { + // If we can't get school ID, search in all schools + return _buildAllSchoolsHistoryView(currentUser?.uid); + } + + final schoolId = schoolSnapshot.data!; + + // Now use the school ID to query the subcollection + return StreamBuilder( + stream: FirebaseFirestore.instance + .collection('schools') + .doc(schoolId) + .collection('recommendation_history') + .where('userId', isEqualTo: currentUser?.uid) + .orderBy('timestamp', descending: true) + .snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ); + } + + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, + color: Colors.white, size: 48), + const SizedBox(height: 16), + Text( + 'Error: ${snapshot.error}', + style: const TextStyle(color: Colors.white), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.history, + color: Colors.white.withOpacity(0.7), size: 64), + const SizedBox(height: 24), + const Text( + 'Belum ada riwayat rekomendasi', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'Mulai aplikasi untuk mendapatkan rekomendasi karir atau kuliah', + style: TextStyle(color: Colors.white70), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () { + Get.back(); // Go back to previous screen + }, + icon: const Icon(Icons.arrow_back), + label: const Text('Kembali'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.indigo.shade800, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ), + ); + } + + // Group by date for better organization + final Map> groupedHistory = {}; + + for (var doc in snapshot.data!.docs) { + final data = doc.data() as Map; + final timestamp = data['timestamp'] as Timestamp?; + + if (timestamp != null) { + final date = intl.DateFormat('yyyy-MM-dd') + .format(timestamp.toDate()); + if (!groupedHistory.containsKey(date)) { + groupedHistory[date] = []; + } + groupedHistory[date]!.add(doc); + } else { + final date = 'Tidak ada tanggal'; + if (!groupedHistory.containsKey(date)) { + groupedHistory[date] = []; + } + groupedHistory[date]!.add(doc); + } + } + + // Sort dates in descending order + final sortedDates = groupedHistory.keys.toList() + ..sort((a, b) => b.compareTo(a)); + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: sortedDates.length, + itemBuilder: (context, dateIndex) { + final date = sortedDates[dateIndex]; + final docs = groupedHistory[date]!; + + // Format the date for display + String formattedDate; + try { + final DateTime parsedDate = DateTime.parse(date); + formattedDate = + intl.DateFormat('EEEE, d MMMM yyyy', 'id_ID') + .format(parsedDate); + } catch (e) { + formattedDate = date; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Date header + Padding( + padding: const EdgeInsets.only(left: 8, bottom: 8), + child: Row( + children: [ + Icon( + Icons.calendar_today, + size: 16, + color: Colors.amber.shade300, + ), + const SizedBox(width: 8), + Text( + formattedDate, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + ), + + // History items for this date with schoolId passed + ...docs + .map((doc) => _buildHistoryItem(doc, schoolId)), + + // Add space between date groups + const SizedBox(height: 16), + ], + ); + }, + ); + }, + ); + }, + ), + ), + ), + ); + } + + Widget _buildHistoryItem(DocumentSnapshot doc, String schoolId) { + final data = doc.data() as Map; + + // Extract data + final timestamp = data['timestamp'] as Timestamp?; + final time = timestamp != null + ? intl.DateFormat('HH:mm').format(timestamp.toDate()) + : ''; + final questionMode = data['questionMode'] ?? 'Tidak diketahui'; + + // Get top recommendation if available + String topRecommendation = 'Tidak ada rekomendasi'; + String secondRecommendation = ''; + List recommendationsList = []; + + if (data['recommendations'] != null && + (data['recommendations'] as List).isNotEmpty) { + recommendationsList = data['recommendations'] as List; + if (recommendationsList.isNotEmpty) { + topRecommendation = + recommendationsList[0]['title'] ?? 'Tidak ada judul'; + if (recommendationsList.length > 1) { + secondRecommendation = recommendationsList[1]['title'] ?? ''; + } + } + } + + // Calculate answer percentage + final totalQuestions = data['totalQuestions'] ?? 0; + final answeredQuestions = data['answeredQuestions'] ?? 0; + final double answerPercentage = + totalQuestions > 0 ? (answeredQuestions / totalQuestions * 100) : 0; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withOpacity(0.1), + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + // Navigate to detail page with school ID + Get.to(() => RecommendationDetailPage( + data: data, + documentId: doc.id, + )); + }, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Mode icon + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: questionMode.contains('Karir') + ? Colors.orange.withOpacity(0.2) + : Colors.green.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + questionMode.contains('Karir') + ? Icons.work_outline + : Icons.school_outlined, + color: Colors.white, + size: 22, + ), + ), + const SizedBox(width: 12), + + // Content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with type and time + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + questionMode, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + time, + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 12, + ), + ), + ], + ), + + const SizedBox(height: 8), + + // Progress bar + Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: LinearProgressIndicator( + value: answerPercentage / 100, + backgroundColor: + Colors.white.withOpacity(0.1), + valueColor: AlwaysStoppedAnimation( + Colors.white.withOpacity(0.7), + ), + minHeight: 6, + ), + ), + ), + const SizedBox(width: 8), + Text( + '$answeredQuestions/$totalQuestions', + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 12, + ), + ), + ], + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Recommendations + const Text( + 'Rekomendasi Utama:', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + topRecommendation, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + + if (secondRecommendation.isNotEmpty) ...[ + const SizedBox(height: 8), + const Text( + 'Rekomendasi Lainnya:', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + secondRecommendation, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 14, + ), + ), + ], + + const SizedBox(height: 12), + + // Action button + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Detail', + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white.withOpacity(0.7), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildAllSchoolsHistoryView(String? userId) { + if (userId == null) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.history, color: Colors.white.withOpacity(0.7), size: 64), + const SizedBox(height: 24), + const Text( + 'Silakan login terlebih dahulu', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () { + Get.back(); // Go back to previous screen + }, + icon: const Icon(Icons.arrow_back), + label: const Text('Kembali'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.indigo.shade800, + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ), + ); + } + + return StreamBuilder( + stream: FirebaseFirestore.instance.collection('schools').snapshots(), + builder: (context, schoolsSnapshot) { + if (schoolsSnapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ); + } + + if (schoolsSnapshot.hasError || + !schoolsSnapshot.hasData || + schoolsSnapshot.data!.docs.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, color: Colors.white, size: 48), + const SizedBox(height: 16), + const Text( + 'Tidak dapat menemukan data sekolah', + style: TextStyle(color: Colors.white), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + // Use FutureBuilder to find the right school and its recommendation history + return FutureBuilder>>( + future: + _findUserHistoryAcrossSchools(userId, schoolsSnapshot.data!.docs), + builder: (context, historySnapshot) { + if (historySnapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ); + } + + if (historySnapshot.hasError || + !historySnapshot.hasData || + historySnapshot.data!.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.history, + color: Colors.white.withOpacity(0.7), size: 64), + const SizedBox(height: 24), + const Text( + 'Belum ada riwayat rekomendasi', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + const Text( + 'Mulai aplikasi untuk mendapatkan rekomendasi karir atau kuliah', + style: TextStyle(color: Colors.white70), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () { + Get.back(); // Go back to previous screen + }, + icon: const Icon(Icons.arrow_back), + label: const Text('Kembali'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.indigo.shade800, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ), + ); + } + + // Group all recommendations by date + final Map>> groupedHistory = {}; + historySnapshot.data!.forEach((schoolId, docs) { + for (var doc in docs) { + final data = doc.data() as Map; + final timestamp = data['timestamp'] as Timestamp?; + + String date; + if (timestamp != null) { + date = + intl.DateFormat('yyyy-MM-dd').format(timestamp.toDate()); + } else { + date = 'Tidak ada tanggal'; + } + + if (!groupedHistory.containsKey(date)) { + groupedHistory[date] = []; + } + + groupedHistory[date]!.add({'doc': doc, 'schoolId': schoolId}); + } + }); + + // Sort dates in descending order + final sortedDates = groupedHistory.keys.toList() + ..sort((a, b) => b.compareTo(a)); + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: sortedDates.length, + itemBuilder: (context, dateIndex) { + final date = sortedDates[dateIndex]; + final items = groupedHistory[date]!; + + // Format the date for display + String formattedDate; + try { + final DateTime parsedDate = DateTime.parse(date); + formattedDate = intl.DateFormat('EEEE, d MMMM yyyy', 'id_ID') + .format(parsedDate); + } catch (e) { + formattedDate = date; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Date header + Padding( + padding: const EdgeInsets.only(left: 8, bottom: 8), + child: Row( + children: [ + Icon( + Icons.calendar_today, + size: 16, + color: Colors.amber.shade300, + ), + const SizedBox(width: 8), + Text( + formattedDate, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + ), + + // History items for this date + ...items.map((item) => + _buildHistoryItem(item['doc'], item['schoolId'])), + + // Add space between date groups + const SizedBox(height: 16), + ], + ); + }, + ); + }, + ); + }, + ); + } + +// Make sure to update your _buildHistoryItem method to accept schoolId + +// Helper function to find user's recommendation history across all schools + Future>> _findUserHistoryAcrossSchools( + String userId, List schools) async { + Map> results = {}; + String? primarySchoolId; + + for (var schoolDoc in schools) { + try { + QuerySnapshot historySnapshot = await schoolDoc.reference + .collection('recommendation_history') + .where('userId', isEqualTo: userId) + .orderBy('timestamp', descending: true) + .get(); + + if (historySnapshot.docs.isNotEmpty) { + results[schoolDoc.id] = historySnapshot.docs; + + // If this is the first school with recommendations, save it as primary + if (primarySchoolId == null) { + primarySchoolId = schoolDoc.id; + + // Save the school ID for future use + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('school_id', primarySchoolId); + } + } + } catch (e) { + print('Error fetching history from school ${schoolDoc.id}: $e'); + } + } + + return results; + } +} diff --git a/lib/app/views/student/model/data_student.dart b/lib/app/views/student/model/data_student.dart new file mode 100644 index 0000000..dbadf7d --- /dev/null +++ b/lib/app/views/student/model/data_student.dart @@ -0,0 +1,189 @@ +/// Tambahan untuk Model Minat dengan RIASEC +class Minat { + final List pertanyaan; + final List> riasecType; // Baru: list tipe RIASEC + final List karir; + final List karir_riasec; // Baru: kode RIASEC untuk setiap karir + final List jurusanTerkait; + final List? rekomendasi_kursus; + final List? universitas_rekomendasi; + + Minat({ + required this.pertanyaan, + this.riasecType = const [], // Baru + required this.karir, + this.karir_riasec = const [], // Baru + required this.jurusanTerkait, + this.rekomendasi_kursus, + this.universitas_rekomendasi, + }); + + factory Minat.fromJson(Map json) { + return Minat( + pertanyaan: List.from(json['pertanyaan'] ?? []), + // Parsing untuk riasecType + riasecType: List>.from((json['riasecType'] ?? []) + .map((item) => Map.from(item))), + karir: List.from(json['karir'] ?? []), + // Parsing untuk karir_riasec + karir_riasec: List.from(json['karir_riasec'] ?? []), + jurusanTerkait: List.from(json['jurusan_terkait'] ?? []), + rekomendasi_kursus: List.from(json['rekomendasi_kursus'] ?? []), + universitas_rekomendasi: + List.from(json['universitas_rekomendasi'] ?? []), + ); + } +} + +/// Tambahan untuk ProgramStudi dengan RIASEC +class ProgramStudi { + final String name; + final String description; + final List categories; + final Map? riasec; // Baru: info RIASEC program studi + final Map minat; + + ProgramStudi({ + required this.name, + required this.description, + required this.categories, + this.riasec, // Baru + required this.minat, + }); + + factory ProgramStudi.fromJson(Map json) { + final Map rawMinat = json['minat'] ?? {}; + final Map minat = {}; + + rawMinat.forEach((key, value) { + minat[key] = Minat.fromJson(value); + }); + + return ProgramStudi( + name: json['name'] ?? '', + description: json['description'] ?? '', + categories: List.from(json['categories'] ?? []), + // Parsing untuk riasec + riasec: json['riasec'] != null + ? Map.from(json['riasec']) + : null, + minat: minat, + ); + } + + factory ProgramStudi.empty() { + return ProgramStudi( + name: '', + description: '', + categories: [], + riasec: null, + minat: {}, + ); + } +} + +/// Tambahan untuk QuestionItem dengan RIASEC +class QuestionItem { + final String id; + final String programName; + final String minatKey; + final String questionText; + final String rawQuestionText; + final int bobot; + final List? riasecTypes; // Baru: tipe RIASEC pertanyaan + final List? riasecBobot; // Baru: bobot RIASEC pertanyaan + bool? userAnswer; + + QuestionItem({ + required this.id, + required this.programName, + required this.minatKey, + required this.questionText, + required this.rawQuestionText, + required this.bobot, + this.riasecTypes, // Baru + this.riasecBobot, // Baru + this.userAnswer, + }); +} + +/// Model untuk hasil profil RIASEC +class RiasecProfile { + final Map scores; // Skor untuk masing-masing tipe (R,I,A,S,E,C) + final List dominantTypes; // Tipe dominan, misal ["R", "I", "C"] + final String code; // Kode RIASEC, misal "RIC" + final List matchingCareers; // Karir yang cocok berdasarkan RIASEC + + RiasecProfile({ + required this.scores, + required this.dominantTypes, + required this.code, + required this.matchingCareers, + }); +} + +/// Tambahan untuk RecommendationResult dengan RIASEC +class RecommendationResult { + final List workingMemory; + final List recommendations; + final RiasecProfile? riasecProfile; // Baru: profil RIASEC pengguna + + RecommendationResult({ + required this.workingMemory, + required this.recommendations, + this.riasecProfile, // Baru + }); +} + +/// Tambahan untuk RecommendationItem dengan kesesuaian RIASEC +class RecommendationItem { + final String title; + final int score; + final List careers; + final List majors; + final List rules; + final int index; + final List? recommendedCourses; + final List? recommendedUniversities; + final double? riasecCompatibility; // Baru: persentase kesesuaian RIASEC + final List? matchingRiasecCareers; // Baru: karir yang cocok RIASEC + + RecommendationItem({ + required this.title, + required this.score, + required this.careers, + required this.majors, + required this.rules, + required this.index, + this.recommendedCourses, + this.recommendedUniversities, + this.riasecCompatibility, // Baru + this.matchingRiasecCareers, // Baru + }); +} + +/// Fungsi ambil bobot [n] dari teks pertanyaan +int extractBobot(String pertanyaan) { + final regex = RegExp(r"\[(\d+)\]"); + final match = regex.firstMatch(pertanyaan); + if (match != null) { + return int.parse(match.group(1)!); + } + return 0; +} + +/// Fungsi buang [n] dari teks pertanyaan +String cleanPertanyaan(String pertanyaan) { + return pertanyaan.replaceAll(RegExp(r"\[\d+\]"), "").trim(); +} + +/// Representasi rule IF-THEN sederhana +class Rule { + final List ifFacts; + final void Function(Set wm) thenAction; + + Rule({ + required this.ifFacts, + required this.thenAction, + }); +} diff --git a/lib/app/views/student/page_student_dashboard.dart b/lib/app/views/student/page_student_dashboard.dart new file mode 100644 index 0000000..d3d38f7 --- /dev/null +++ b/lib/app/views/student/page_student_dashboard.dart @@ -0,0 +1,1254 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/app/controllers/developer_controller.dart'; +import 'package:forward_chaining_man_app/app/views/about/page_about.dart'; +import 'package:forward_chaining_man_app/app/views/developer/page/page_developer_viewer.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/quiz/view/page_select_major.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/recomendation_screen/view/page_recmendation_detail_screen.dart'; +import 'package:forward_chaining_man_app/app/views/student/feature/recomendation_screen/view/page_recomendation_screen_history.dart'; +import 'package:forward_chaining_man_app/app/views/student/model/data_student.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:intl/intl.dart' as intl; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'feature/quiz/view/page_select_economy.dart'; + +class DevDataViewerController extends GetxController { + final RxList programStudiKerja = [].obs; + final RxList programStudiKuliah = [].obs; + final RxString currentView = + 'overview'.obs; // overview, kerja, kuliah, rules, ugm + final RxBool isLoading = true.obs; + final RxString loadingError = ''.obs; + final RxList> rulesData = >[].obs; + + // Data for UGM tuition fees + final RxList> biayaKuliahD4UGM = + >[].obs; + final RxList> biayaKuliahS1UGM = + >[].obs; + + @override + void onInit() { + super.onInit(); + loadAllData(); + } + + /// Load all data for analysis + void loadAllData() async { + isLoading.value = true; + loadingError.value = ''; + + try { + // Load Kerja data + await loadProgramData(true, programStudiKerja); + + // Load Kuliah data + await loadProgramData(false, programStudiKuliah); + + // Load UGM tuition fee data + await loadUGMTuitionData(); + + // Generate sample rules for analysis + generateSampleRules(); + + isLoading.value = false; + } catch (e) { + loadingError.value = e.toString(); + isLoading.value = false; + } + } + + void setCurrentView(String view) { + currentView.value = view; + } + + /// Memuat data ProgramStudi dari file JSON (Sains + Teknik) tergantung Kerja/Kuliah + Future loadProgramData( + bool isKerja, RxList target) async { + // Tentukan file sains + final sainsFile = isKerja + ? 'assets/ipa_sains_kerja.json' + : 'assets/ipa_sains_kuliah.json'; + + // File teknik + final teknikFile = isKerja + ? 'assets/ipa_teknik_kerja.json' + : 'assets/ipa_teknik_kuliah.json'; + + // Baca JSON sains + final sainsString = await rootBundle.rootBundle.loadString(sainsFile); + final sainsMap = json.decode(sainsString) as Map; + + // Baca JSON teknik + final teknikString = await rootBundle.rootBundle.loadString(teknikFile); + final teknikMap = json.decode(teknikString) as Map; + + // Ubah ke list ProgramStudi + final programs = []; + // Parsing sains + for (var entry in sainsMap.entries) { + programs.add(ProgramStudi.fromJson(entry.value)); + } + // Parsing teknik + for (var entry in teknikMap.entries) { + programs.add(ProgramStudi.fromJson(entry.value)); + } + + target.value = programs; + } + + /// Load UGM tuition fee data + Future loadUGMTuitionData() async { + try { + // Load D4 data + final d4String = await rootBundle.rootBundle + .loadString('assets/biaya_kuliah_d4_ugm.json'); + final d4List = json.decode(d4String) as List; + biayaKuliahD4UGM.value = d4List.cast>(); + + // Load S1 data + final s1String = await rootBundle.rootBundle + .loadString('assets/biaya_kuliah_s1_ugm.json'); + final s1List = json.decode(s1String) as List; + biayaKuliahS1UGM.value = s1List.cast>(); + } catch (e) { + print('Error loading UGM data: $e'); + // Continue even if UGM data fails to load + } + } + + /// Generate sample rules untuk analisis + void generateSampleRules() { + final rules = >[]; + + // Flatten pertanyaan dari programStudiKerja untuk contoh rules + int counter = 1; + for (var prog in programStudiKerja) { + for (var minatEntry in prog.minat.entries) { + final minatKey = minatEntry.key; + final minatVal = minatEntry.value; + + for (var p in minatVal.pertanyaan) { + final bobot = extractBobot(p); + final cleaned = cleanPertanyaan(p); + final qId = 'Q$counter'; + counter++; + + rules.add({ + 'id': 'R$counter', + 'type': 'Forward Chaining Rule', + 'condition': 'IF $qId = Yes', + 'action': 'THEN Score("${prog.name}|$minatKey") += $bobot', + 'question': cleaned, + 'weight': bobot, + 'programName': prog.name, + 'minatKey': minatKey, + }); + } + } + } + + rulesData.value = rules; + } + + /// Get total question count + int getTotalQuestions() { + int count = 0; + + // Count questions from kerja + for (var prog in programStudiKerja) { + for (var minat in prog.minat.values) { + count += minat.pertanyaan.length; + } + } + + // Count questions from kuliah + for (var prog in programStudiKuliah) { + for (var minat in prog.minat.values) { + count += minat.pertanyaan.length; + } + } + + return count; + } + + /// Count total minat + int getTotalMinat() { + int kerjaMinat = + programStudiKerja.fold(0, (sum, prog) => sum + prog.minat.length); + int kuliahMinat = + programStudiKuliah.fold(0, (sum, prog) => sum + prog.minat.length); + return kerjaMinat + kuliahMinat; + } + + /// Count total jurusan + int getTotalJurusan() { + Set allJurusan = {}; + + // Collect unique jurusan from Kerja + for (var prog in programStudiKerja) { + for (var minat in prog.minat.values) { + allJurusan.addAll(minat.jurusanTerkait); + } + } + + // Collect unique jurusan from Kuliah + for (var prog in programStudiKuliah) { + for (var minat in prog.minat.values) { + allJurusan.addAll(minat.jurusanTerkait); + } + } + + return allJurusan.length; + } + + /// Count total karir + int getTotalKarir() { + Set allKarir = {}; + + // Collect unique karir from Kerja + for (var prog in programStudiKerja) { + for (var minat in prog.minat.values) { + allKarir.addAll(minat.karir); + } + } + + // Collect unique karir from Kuliah + for (var prog in programStudiKuliah) { + for (var minat in prog.minat.values) { + allKarir.addAll(minat.karir); + } + } + + return allKarir.length; + } +} + +class PageStudentDashboard extends StatelessWidget { + const PageStudentDashboard({Key? key}) : super(key: key); + Future _findStudentInAllSchools(String? userId) async { + if (userId == null) { + // Return an empty document that won't exist + return FirebaseFirestore.instance.collection('dummy').doc('dummy').get(); + } + + final schoolsSnapshot = + await FirebaseFirestore.instance.collection('schools').get(); + + for (var schoolDoc in schoolsSnapshot.docs) { + final studentDoc = + await schoolDoc.reference.collection('students').doc(userId).get(); + + if (studentDoc.exists) { + // Save the school ID for future use + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('school_id', schoolDoc.id); + + return studentDoc; + } + } + + // If student not found in any school, return an empty document + return FirebaseFirestore.instance.collection('dummy').doc('dummy').get(); + } + + @override + Widget build(BuildContext context) { + final User? currentUser = FirebaseAuth.instance.currentUser; + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + // Use a ListView as the main container instead of Column + SingleChildScrollView + child: SafeArea( + bottom: false, + child: ListView( + padding: + const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0), + children: [ + // Top bar with buttons + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Profile button + GestureDetector( + onTap: () { + Get.to(() => const ProfilePage()); + }, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + border: Border.all( + color: Colors.indigo.shade100, + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + currentUser?.photoURL != null + ? CircleAvatar( + radius: 14, + backgroundImage: + NetworkImage(currentUser!.photoURL!), + ) + : CircleAvatar( + radius: 14, + backgroundColor: Colors.indigo.shade100, + child: Icon( + Icons.person, + color: Colors.indigo.shade700, + size: 16, + ), + ), + const SizedBox(width: 6), + Text( + "Profil", + style: TextStyle( + color: Colors.indigo.shade700, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + + // About button + GestureDetector( + onTap: () { + Get.to(() => AboutPage()); + }, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + border: Border.all( + color: Colors.indigo.shade100, + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.info_outline_rounded, + color: Colors.indigo.shade700, + size: 18, + ), + const SizedBox(width: 6), + Text( + "Tentang", + style: TextStyle( + color: Colors.indigo.shade700, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + + const SizedBox(height: 20), + + // App Logo with Hero animation - centered + Center( + child: Hero( + tag: 'app_logo', + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: const Center( + child: Icon( + Icons.psychology, + size: 60, + color: Colors.indigo, + ), + ), + ), + ), + ), + + const SizedBox(height: 20), + + // App Title - centered + Center( + child: TweenAnimationBuilder( + tween: Tween(begin: 0.8, end: 1), + duration: const Duration(milliseconds: 800), + curve: Curves.easeOutQuad, + builder: (context, double value, child) { + return Transform.scale( + scale: value, + child: child, + ); + }, + child: Column( + children: [ + const Text( + 'EduGuide', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 4), + const Text( + 'Sistem Rekomendasi Karir & Kuliah', + style: TextStyle( + fontSize: 15, + color: Colors.white70, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // User welcome section + FutureBuilder( + // First, get the school ID from SharedPreferences + future: () async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('school_id') ?? ''; + }(), + builder: (context, schoolSnapshot) { + if (schoolSnapshot.connectionState == + ConnectionState.waiting) { + return const CircularProgressIndicator(); + } + + // Once we have the school ID (or not), proceed + String schoolId = schoolSnapshot.data ?? ''; + + return FutureBuilder( + future: schoolId.isNotEmpty + // If we have a school ID, try to get the student directly + ? FirebaseFirestore.instance + .collection('schools') + .doc(schoolId) + .collection('students') + .doc(currentUser?.uid) + .get() + // If no school ID, find the student in all schools + : _findStudentInAllSchools(currentUser?.uid), + builder: (context, snapshot) { + String userName = "Siswa"; + String userClass = ""; + + if (snapshot.hasData && snapshot.data!.exists) { + final userData = + snapshot.data!.data() as Map; + userName = userData['name'] ?? "Siswa"; + userClass = userData['class'] ?? ""; + } + + return Container( + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 20), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + children: [ + Icon( + Icons.waving_hand_rounded, + color: Colors.amber.shade300, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Hai, $userName!', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + if (userClass.isNotEmpty) + Text( + 'Kelas $userClass', + style: const TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + ), + const SizedBox(height: 16), + + // User Recommendation History + if (currentUser != null) ...[ + // Recommendation History Section + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(15), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.history, + color: Colors.amber.shade200, + size: 20, + ), + const SizedBox(width: 8), + const Text( + 'Riwayat Rekomendasi', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const Spacer(), + TextButton( + onPressed: () { + Get.to(() => const RecommendationHistoryPage()); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + backgroundColor: Colors.blue.withOpacity(0.2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text( + 'Lihat Semua', + style: TextStyle(fontSize: 12), + ), + ) + ], + ), + const SizedBox(height: 8), + SizedBox( + height: 170, // Fixed height for the history list + child: FutureBuilder( + // First get the school ID + future: () async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('school_id') ?? ''; + }(), + builder: (context, schoolSnapshot) { + if (schoolSnapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Colors.white70), + strokeWidth: 2, + ), + ); + } + + if (schoolSnapshot.hasError || + !schoolSnapshot.hasData || + schoolSnapshot.data!.isEmpty) { + // If we can't get school ID, search in all schools + return _buildAllSchoolsStreamBuilder( + currentUser.uid); + } + + final schoolId = schoolSnapshot.data!; + + // Now we have the school ID, use it to query the subcollection + return StreamBuilder( + stream: FirebaseFirestore.instance + .collection('schools') + .doc(schoolId) + .collection('recommendation_history') + .where('userId', isEqualTo: currentUser.uid) + .orderBy('timestamp', descending: true) + .limit(5) + .snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Colors.white70), + strokeWidth: 2, + ), + ); + } + + if (snapshot.hasError) { + return Center( + child: Text( + 'Error: ${snapshot.error}', + style: const TextStyle( + color: Colors.white70), + ), + ); + } + + if (!snapshot.hasData || + snapshot.data!.docs.isEmpty) { + return Container( + height: 170, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Text( + 'Belum ada riwayat rekomendasi.\nMulai aplikasi untuk mendapatkan rekomendasi.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white70, + fontSize: 13, + ), + ), + ), + ); + } + + return ListView.builder( + padding: EdgeInsets.zero, + itemCount: snapshot.data!.docs.length, + itemBuilder: (context, index) { + final doc = snapshot.data!.docs[index]; + final data = + doc.data() as Map; + + // Extract recommendation data + String questionMode = + data['questionMode'] ?? + 'Tidak diketahui'; + final timestamp = + data['timestamp'] as Timestamp?; + final formattedDate = timestamp != null + ? intl.DateFormat('dd/MM/yyyy HH:mm') + .format(timestamp.toDate()) + : 'Tidak ada tanggal'; + + // Get top recommendation if available + String topRecommendation = + 'Tidak ada rekomendasi'; + if (data['recommendations'] != null && + (data['recommendations'] as List) + .isNotEmpty) { + final recommendations = + data['recommendations'] as List; + if (recommendations.isNotEmpty) { + topRecommendation = recommendations[0] + ['title'] ?? + 'Tidak ada judul'; + } + } + + return GestureDetector( + onTap: () { + // Navigate to recommendation detail with school ID + Get.to(() => RecommendationDetailPage( + data: data, + documentId: doc.id, + )); + }, + child: Container( + margin: + const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: + BorderRadius.circular(12), + border: Border.all( + color: + Colors.white.withOpacity(0.1), + width: 1, + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: questionMode + .contains('Karir') + ? Colors.orange + .withOpacity(0.2) + : Colors.green + .withOpacity(0.2), + borderRadius: + BorderRadius.circular(8), + ), + child: Center( + child: Icon( + questionMode.contains('Karir') + ? Icons.work + : Icons.school, + color: questionMode + .contains('Karir') + ? Colors.orange.shade300 + : Colors.green.shade300, + size: 20, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + topRecommendation, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: + FontWeight.bold, + ), + maxLines: 1, + overflow: + TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + '$questionMode • $formattedDate', + style: TextStyle( + color: Colors.white + .withOpacity(0.7), + fontSize: 12, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: + Colors.white.withOpacity(0.5), + size: 20, + ), + ], + ), + ), + ); + }, + ); + }, + ); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + + // Main Content Area - Now directly in ListView, no nested scrolling + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 12), + // Welcome message with visually distinct styling + Center( + child: Text( + 'Selamat Datang!', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.indigo.shade800, + ), + ), + ), + const SizedBox(height: 12), + // Subtitle with improved styling + Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: const Text( + 'Aplikasi ini akan membantumu menemukan program studi dan karir yang paling sesuai dengan minatmu.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: Colors.black54, + height: 1.4, + ), + ), + ), + const SizedBox(height: 24), + + // Visual indicator to show content continues - arrow indicator + Center( + child: Icon( + Icons.keyboard_double_arrow_down, + color: Colors.indigo.shade200, + size: 28, + ), + ), + const SizedBox(height: 16), + + // User tips card with action indicator + Stack( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.blue.shade100, + width: 1, + ), + ), + child: Column( + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.lightbulb_outline, + size: 20, + color: Colors.blue.shade800, + ), + ), + const SizedBox(width: 12), + Text( + 'Tips Penggunaan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + ], + ), + const SizedBox(height: 12), + const Text( + 'Jawab pertanyaan dengan jujur untuk mendapatkan rekomendasi karir dan program studi yang paling sesuai dengan minat dan bakatmu.', + style: TextStyle( + fontSize: 13, + color: Colors.black54, + height: 1.4, + ), + ), + ], + ), + ), + // Subtle indicator to show this is important + Positioned( + top: 8, + right: 8, + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: Colors.blue.shade400, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + + const SizedBox(height: 24), + + // Action Buttons - with enhanced styling and visual cues + // Primary Button + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.indigo.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ElevatedButton( + onPressed: () => + Get.to(() => const MajorPreferencePage()), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo.shade800, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 16), + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.play_arrow_rounded, size: 24), + SizedBox(width: 12), + Text( + 'Mulai Aplikasi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + const SizedBox(height: 16), + + // Footer attribution + Center( + child: Text( + 'v1.0.0', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade400, + ), + ), + ), + const SizedBox(height: 8), + ], + ), + ), + + // Bottom spacing + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _buildAllSchoolsStreamBuilder(String userId) { + return StreamBuilder( + stream: FirebaseFirestore.instance.collection('schools').snapshots(), + builder: (context, schoolsSnapshot) { + if (schoolsSnapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white70), + strokeWidth: 2, + ), + ); + } + + if (schoolsSnapshot.hasError || + !schoolsSnapshot.hasData || + schoolsSnapshot.data!.docs.isEmpty) { + return Container( + height: 170, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Text( + 'Tidak dapat menemukan data sekolah', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white70, + fontSize: 13, + ), + ), + ), + ); + } + + // Use FutureBuilder to find the right school and its recommendation history + return FutureBuilder>( + future: _findUserRecommendations(userId, schoolsSnapshot.data!.docs), + builder: (context, recommendationsSnapshot) { + if (recommendationsSnapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white70), + strokeWidth: 2, + ), + ); + } + + if (recommendationsSnapshot.hasError || + !recommendationsSnapshot.hasData || + recommendationsSnapshot.data!.isEmpty) { + return Container( + height: 170, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Text( + 'Belum ada riwayat rekomendasi.\nMulai aplikasi untuk mendapatkan rekomendasi.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white70, + fontSize: 13, + ), + ), + ), + ); + } + + List recommendations = + recommendationsSnapshot.data!; + + // Sort recommendations by timestamp + recommendations.sort((a, b) { + final aTimestamp = + (a.data() as Map)['timestamp'] as Timestamp?; + final bTimestamp = + (b.data() as Map)['timestamp'] as Timestamp?; + if (aTimestamp == null) return 1; + if (bTimestamp == null) return -1; + return bTimestamp.compareTo(aTimestamp); + }); + + // Limit to 5 recommendations + if (recommendations.length > 5) { + recommendations = recommendations.sublist(0, 5); + } + + return ListView.builder( + padding: EdgeInsets.zero, + itemCount: recommendations.length, + itemBuilder: (context, index) { + final doc = recommendations[index]; + final data = doc.data() as Map; + final schoolId = doc.reference.parent.parent!.id; + + // Extract recommendation data + String questionMode = data['questionMode'] ?? 'Tidak diketahui'; + final timestamp = data['timestamp'] as Timestamp?; + final formattedDate = timestamp != null + ? intl.DateFormat('dd/MM/yyyy HH:mm') + .format(timestamp.toDate()) + : 'Tidak ada tanggal'; + + // Get top recommendation if available + String topRecommendation = 'Tidak ada rekomendasi'; + if (data['recommendations'] != null && + (data['recommendations'] as List).isNotEmpty) { + final recommendations = data['recommendations'] as List; + if (recommendations.isNotEmpty) { + topRecommendation = + recommendations[0]['title'] ?? 'Tidak ada judul'; + } + } + + return GestureDetector( + onTap: () { + // Save the found school ID for future use + SharedPreferences.getInstance().then((prefs) { + prefs.setString('school_id', schoolId); + }); + + // Navigate to recommendation detail + Get.to(() => RecommendationDetailPage( + data: data, + documentId: doc.id, + )); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withOpacity(0.1), + width: 1, + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: questionMode.contains('Karir') + ? Colors.orange.withOpacity(0.2) + : Colors.green.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Icon( + questionMode.contains('Karir') + ? Icons.work + : Icons.school, + color: questionMode.contains('Karir') + ? Colors.orange.shade300 + : Colors.green.shade300, + size: 20, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + topRecommendation, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + '$questionMode • $formattedDate', + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 12, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + color: Colors.white.withOpacity(0.5), + size: 20, + ), + ], + ), + ), + ); + }, + ); + }, + ); + }, + ); + } + +// Helper function to find user recommendations across all schools + Future> _findUserRecommendations( + String userId, List schools) async { + List results = []; + + for (var schoolDoc in schools) { + try { + QuerySnapshot recommendationsSnapshot = await schoolDoc.reference + .collection('recommendation_history') + .where('userId', isEqualTo: userId) + .orderBy('timestamp', descending: true) + .limit(5) + .get(); + + if (recommendationsSnapshot.docs.isNotEmpty) { + results.addAll(recommendationsSnapshot.docs); + + // Save the school ID for future use if we found recommendations + if (recommendationsSnapshot.docs.isNotEmpty) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('school_id', schoolDoc.id); + } + } + } catch (e) { + print('Error fetching recommendations from school ${schoolDoc.id}: $e'); + } + } + + return results; + } +} diff --git a/lib/app/views/teacher/teacher_page.dart b/lib/app/views/teacher/teacher_page.dart new file mode 100644 index 0000000..7b60bb1 --- /dev/null +++ b/lib/app/views/teacher/teacher_page.dart @@ -0,0 +1,770 @@ +import 'package:flutter/material.dart'; +import 'package:forward_chaining_man_app/app/views/dashboard_teacher.dart'; +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:intl/intl.dart'; +import 'package:fl_chart/fl_chart.dart'; + +class StudentDashboardPage extends StatelessWidget { + final TeacherDashboardController controller = + Get.put(TeacherDashboardController()); + + StudentDashboardPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Dashboard Siswa'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () => controller.loadStudentResults(), + ), + ], + ), + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStatisticCards(), + const SizedBox(height: 24), + _buildCharts(), + const SizedBox(height: 24), + _buildFilters(), + const SizedBox(height: 16), + _buildStudentList(), + ], + ), + ); + }), + ); + } + + Widget _buildStatisticCards() { + return Row( + children: [ + Expanded( + child: _buildStatCard( + 'Total Siswa', + controller.stats['totalStudents'].toString(), + Colors.blue, + Icons.people, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + 'Rekomendasi Karir', + controller.stats['careerRecommendations'].toString(), + Colors.orange, + Icons.work, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + 'Rekomendasi Studi', + controller.stats['studyRecommendations'].toString(), + Colors.green, + Icons.school, + ), + ), + ], + ); + } + + Widget _buildStatCard( + String title, String value, Color color, IconData icon) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: color), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + value, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + } + + Widget _buildCharts() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Visualisasi Data', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Card( + elevation: 4, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Distribusi Rekomendasi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + SizedBox( + height: 200, + child: _buildPieChart(), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Card( + elevation: 4, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Tren Rekomendasi (Per Hari)', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + SizedBox( + height: 200, + child: _buildLineChart(), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildPieChart() { + final career = controller.stats['careerRecommendations']?.toDouble() ?? 0; + final study = controller.stats['studyRecommendations']?.toDouble() ?? 0; + + // If no data, show a placeholder + if (career == 0 && study == 0) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Tidak ada data untuk ditampilkan', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ); + } + + return PieChart( + PieChartData( + sections: [ + PieChartSectionData( + value: career, + title: 'Karir\n${career.toInt()}', + color: Colors.orange, + radius: 70, + titleStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + titlePositionPercentageOffset: 0.55, + ), + PieChartSectionData( + value: study, + title: 'Studi\n${study.toInt()}', + color: Colors.green, + radius: 70, + titleStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + titlePositionPercentageOffset: 0.55, + ), + ], + sectionsSpace: 2, + centerSpaceRadius: 40, + startDegreeOffset: 180, + ), + ); + } + + Widget _buildLineChart() { + // Group data by day for the chart + Map dailyData = {}; + + for (var doc in controller.filteredResults) { + final data = doc.data() as Map; + if (data['timestamp'] != null) { + final timestamp = data['timestamp'] as Timestamp; + final date = timestamp.toDate(); + final dateStr = DateFormat('yyyy-MM-dd').format(date); + + if (dailyData.containsKey(dateStr)) { + dailyData[dateStr] = dailyData[dateStr]! + 1; + } else { + dailyData[dateStr] = 1; + } + } + } + + // Sort by date + final sortedDates = dailyData.keys.toList()..sort(); + + // Create spots for the chart + final spots = []; + for (int i = 0; i < sortedDates.length; i++) { + spots.add(FlSpot(i.toDouble(), dailyData[sortedDates[i]]!.toDouble())); + } + + // If no data, show a placeholder + if (spots.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text( + 'Tidak ada data untuk ditampilkan', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ); + } + + return LineChart( + LineChartData( + gridData: const FlGridData(show: true), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 22, + interval: 1, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index >= 0 && index < sortedDates.length) { + final date = DateTime.parse(sortedDates[index]); + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + DateFormat('dd/MM').format(date), + style: const TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + interval: 1, + getTitlesWidget: (value, meta) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + value.toInt().toString(), + style: const TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ); + }, + ), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all(color: Colors.black12, width: 1), + ), + minX: 0, + maxX: (sortedDates.length - 1).toDouble(), + minY: 0, + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: true, + color: Colors.blue, + barWidth: 3, + isStrokeCapRound: true, + dotData: const FlDotData(show: true), + belowBarData: BarAreaData( + show: true, + color: Colors.blue.withOpacity(0.3), + ), + ), + ], + ), + ); + } + + Widget _buildFilters() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Filter', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Card( + elevation: 4, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: _buildFilterCategory(), + ), + const SizedBox(width: 12), + Expanded( + child: _buildFilterDate(), + ), + ], + ), + const SizedBox(height: 12), + _buildFilterClass(), + const SizedBox(height: 12), + _buildCustomDateRange(), + ], + ), + ), + ), + ], + ); + } + + Widget _buildFilterCategory() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Kategori', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Obx( + () => DropdownButtonFormField( + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + isDense: true, + ), + value: controller.selectedCategoryFilter.value, + onChanged: (value) { + if (value != null) { + controller.filterByCategory(value); + } + }, + items: const [ + DropdownMenuItem( + value: 'all', + child: Text('Semua Kategori'), + ), + DropdownMenuItem( + value: 'career', + child: Text('Rekomendasi Karir'), + ), + DropdownMenuItem( + value: 'study', + child: Text('Rekomendasi Studi'), + ), + ], + ), + ), + ], + ); + } + + Widget _buildFilterDate() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Periode', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Obx( + () => DropdownButtonFormField( + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + isDense: true, + ), + value: controller.selectedDateFilter.value, + onChanged: (value) { + if (value != null) { + controller.filterByDate(value); + } + }, + items: const [ + DropdownMenuItem( + value: 'all_time', + child: Text('Semua Waktu'), + ), + DropdownMenuItem( + value: 'today', + child: Text('Hari Ini'), + ), + DropdownMenuItem( + value: 'this_month', + child: Text('Bulan Ini'), + ), + ], + ), + ), + ], + ); + } + + Widget _buildFilterClass() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Kelas', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Obx( + () => DropdownButtonFormField( + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + value: controller.selectedClass.value, + onChanged: (value) { + if (value != null) { + controller.filterByClass(value); + } + }, + items: controller.availableClasses.map((className) { + return DropdownMenuItem( + value: className, + child: Text(className), + ); + }).toList(), + ), + ), + ], + ); + } + + Widget _buildCustomDateRange() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Rentang Tanggal Kustom', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'Tanggal Mulai', + prefixIcon: const Icon(Icons.calendar_today), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + isDense: true, + ), + readOnly: true, + onTap: () async { + final DateTime? picked = await showDatePicker( + context: Get.context!, + initialDate: DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime.now(), + ); + if (picked != null) { + // Handle start date selection + // Implement custom date range filter + } + }, + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'Tanggal Akhir', + prefixIcon: const Icon(Icons.calendar_today), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + isDense: true, + ), + readOnly: true, + onTap: () async { + final DateTime? picked = await showDatePicker( + context: Get.context!, + initialDate: DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime.now(), + ); + if (picked != null) { + // Handle end date selection + // Implement custom date range filter + } + }, + ), + ), + ], + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + // Apply custom date range filter + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text('Terapkan Filter'), + ), + ), + ], + ); + } + + Widget _buildStudentList() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Data Siswa (${controller.filteredResults.length})', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(Icons.download), + onPressed: () { + // Export data functionality + Get.snackbar( + 'Info', + 'Mengunduh data...', + snackPosition: SnackPosition.BOTTOM, + ); + }, + ), + ], + ), + const SizedBox(height: 8), + Card( + elevation: 4, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.filteredResults.length, + separatorBuilder: (context, index) => const Divider(height: 1), + itemBuilder: (context, index) { + final doc = controller.filteredResults[index]; + final data = doc.data() as Map; + + final isKerja = data['isKerja'] ?? false; + final username = data['username'] ?? 'Unknown'; + final timestamp = controller.formatTimestamp(data['timestamp']); + + return ListTile( + leading: CircleAvatar( + backgroundColor: isKerja ? Colors.orange : Colors.green, + child: Icon( + isKerja ? Icons.work : Icons.school, + color: Colors.white, + ), + ), + title: Text( + username, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Rekomendasi: ${isKerja ? 'Karir' : 'Studi'}'), + Text('Tanggal: $timestamp'), + ], + ), + trailing: IconButton( + icon: const Icon(Icons.visibility), + onPressed: () { + // View detailed information + Get.toNamed('/student-detail', arguments: doc.id); + }, + ), + onTap: () { + // Navigate to detail screen + Get.toNamed('/student-detail', arguments: doc.id); + }, + ); + }, + ), + ), + ], + ); + } +} + +// Extended TeacherDashboardController with additional functions for custom date range +class TeacherDashboardControllerExtended extends TeacherDashboardController { + final Rx startDate = Rx(null); + final Rx endDate = Rx(null); + + void setStartDate(DateTime date) { + startDate.value = date; + if (endDate.value != null) { + applyCustomDateRange(); + } + } + + void setEndDate(DateTime date) { + endDate.value = date; + if (startDate.value != null) { + applyCustomDateRange(); + } + } + + void applyCustomDateRange() { + if (startDate.value == null || endDate.value == null) return; + + final start = DateTime( + startDate.value!.year, + startDate.value!.month, + startDate.value!.day, + ); + + final end = DateTime( + endDate.value!.year, + endDate.value!.month, + endDate.value!.day, + ).add(const Duration(days: 1)); // Include the end date fully + + filteredResults.value = studentResults.where((doc) { + final data = doc.data() as Map; + if (data['timestamp'] == null) return false; + + final timestamp = data['timestamp'] as Timestamp; + final date = timestamp.toDate(); + return date.isAfter(start.subtract(const Duration(seconds: 1))) && + date.isBefore(end); + }).toList(); + } + + Future exportData() async { + // Implement data export functionality + // This would typically involve creating a CSV file + // and downloading it to the user's device + } +} diff --git a/lib/app/views/teacher_page.dart b/lib/app/views/teacher_page.dart new file mode 100644 index 0000000..e977484 --- /dev/null +++ b/lib/app/views/teacher_page.dart @@ -0,0 +1,561 @@ +import 'package:flutter/material.dart'; +import 'package:forward_chaining_man_app/app/views/dashboard_teacher.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_teacher_register.dart'; +import 'package:get/get.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +// Import your teacher dashboard or main page + +class TeacherLoginController extends GetxController { + final FirebaseAuth _auth = FirebaseAuth.instance; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + var isLoading = false.obs; + var errorMessage = ''.obs; + + // Text controllers for login form + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + + @override + void onInit() { + super.onInit(); + checkExistingSession(); + } + + @override + void onClose() { + emailController.dispose(); + passwordController.dispose(); + super.onClose(); + } + + // Check if user is already logged in + Future checkExistingSession() async { + try { + isLoading.value = true; + + // Check if we have a stored user_uid and role + final prefs = await SharedPreferences.getInstance(); + final storedUid = prefs.getString('user_uid'); + final storedRole = prefs.getString('user_role'); + + if (storedUid != null && storedRole == 'teacher') { + // Verify if user exists in Firebase Auth + User? currentUser = _auth.currentUser; + if (currentUser != null) { + // Check if user data exists in Firestore + final docSnapshot = await _firestore + .collection('teachers') + .doc(currentUser.uid) + .get(); + + if (docSnapshot.exists) { + // Navigate to teacher dashboard + Get.offAll(() => const TeacherDashboardPage()); + return; + } + } + } + } catch (e) { + print('Error checking session: $e'); + } finally { + isLoading.value = false; + } + } + + // Sign in with email and password + Future signInWithEmailPassword() async { + try { + isLoading.value = true; + errorMessage.value = ''; + + if (emailController.text.trim().isEmpty || + passwordController.text.isEmpty) { + errorMessage.value = 'Email dan password tidak boleh kosong'; + return; + } + + final userCredential = await _auth.signInWithEmailAndPassword( + email: emailController.text.trim(), + password: passwordController.text, + ); + + if (userCredential.user != null) { + // Check if this user is a teacher + final teacherDoc = await _firestore + .collection('teachers') + .doc(userCredential.user!.uid) + .get(); + + if (!teacherDoc.exists) { + // This user is not registered as a teacher + errorMessage.value = 'Akun ini tidak terdaftar sebagai guru'; + await _auth.signOut(); + return; + } + + // Save login session + await saveLoginSession(userCredential.user!.uid, teacherDoc); + + // Navigate to teacher dashboard + Get.offAll(() => const TeacherDashboardPage()); + } + } on FirebaseAuthException catch (e) { + switch (e.code) { + case 'user-not-found': + errorMessage.value = 'Email tidak terdaftar'; + break; + case 'wrong-password': + errorMessage.value = 'Password salah'; + break; + case 'invalid-email': + errorMessage.value = 'Format email tidak valid'; + break; + default: + errorMessage.value = 'Gagal masuk: ${e.message}'; + } + } catch (e) { + errorMessage.value = 'Terjadi kesalahan: ${e.toString()}'; + } finally { + isLoading.value = false; + } + } + +// Save login session to shared preferences + Future saveLoginSession(String uid, DocumentSnapshot teacherDoc) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('user_uid', uid); + await prefs.setBool('is_logged_in', true); + await prefs.setString('user_role', 'teacher'); + + // Extract schoolId from teacher document if it exists + final teacherData = teacherDoc.data() as Map?; + if (teacherData != null && teacherData.containsKey('schoolId')) { + final schoolId = teacherData['schoolId']; + if (schoolId != null && schoolId is String && schoolId.isNotEmpty) { + // Save the school ID to shared preferences + await prefs.setString('school_id', schoolId); + } + } + + // Update last login timestamp + await _firestore.collection('teachers').doc(uid).update({ + 'lastLogin': FieldValue.serverTimestamp(), + }); + } catch (e) { + print('Error saving session: $e'); + } + } +} + +class TeacherLoginPage extends StatelessWidget { + const TeacherLoginPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(TeacherLoginController()); + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade800, + Colors.indigo.shade900, + ], + ), + ), + child: SafeArea( + child: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Back button at the top + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only(top: 8, bottom: 20), + child: TextButton( + onPressed: () { + Get.back(); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + backgroundColor: Colors.white.withOpacity(0.2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.arrow_back, size: 18), + SizedBox(width: 8), + Text('Kembali ke Pemilihan Peran'), + ], + ), + ), + ), + ), + + // App Logo with Hero animation + Hero( + tag: 'app_logo', + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: Center( + child: Icon( + Icons.psychology, + size: 60, + color: Colors.blue + .shade700, // Mengubah warna ikon sesuai dengan skema warna biru + ), + ), + ), + ), + + const SizedBox(height: 20), + + // App Title + const Text( + 'EduGuide', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 1.2, + ), + ), + + const SizedBox(height: 8), + + // Teacher Login Subtitle + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + 'Portal Guru', + style: TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + + const SizedBox(height: 30), + + // Login Card + Obx(() => Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.person_pin, + color: Colors.blue.shade400, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Masuk sebagai Guru', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 4), + const Text( + 'Masukkan kredensial Anda', + style: TextStyle( + fontSize: 14, + color: Colors.black54, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 30), + + // Email field + CustomTextField( + controller: controller.emailController, + label: 'Email', + prefixIcon: Icons.email_outlined, + keyboardType: TextInputType.emailAddress, + ), + + const SizedBox(height: 20), + + // Password field + CustomTextField( + controller: controller.passwordController, + label: 'Password', + prefixIcon: Icons.lock_outline, + isPassword: true, + ), + + const SizedBox(height: 12), + + // // Forgot password + // Align( + // alignment: Alignment.centerRight, + // child: TextButton( + // onPressed: () { + // // Handle forgot password + // Get.snackbar( + // 'Lupa Password', + // 'Silakan hubungi administrator untuk reset password', + // backgroundColor: Colors.blue.shade50, + // colorText: Colors.blue.shade800, + // snackPosition: SnackPosition.BOTTOM, + // margin: const EdgeInsets.all(16), + // ); + // }, + // style: TextButton.styleFrom( + // padding: EdgeInsets.zero, + // minimumSize: const Size(50, 30), + // tapTargetSize: + // MaterialTapTargetSize.shrinkWrap, + // ), + // child: Text( + // 'Lupa Password?', + // style: TextStyle( + // color: Colors.blue.shade700, + // fontWeight: FontWeight.w500, + // fontSize: 14, + // ), + // ), + // ), + // ), + + const SizedBox(height: 24), + + // Error message + Obx(() => controller + .errorMessage.value.isNotEmpty + ? Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: + BorderRadius.circular(12), + border: Border.all( + color: Colors.red.shade200, + ), + ), + child: Row( + children: [ + Icon(Icons.error_outline, + color: Colors.red.shade800), + const SizedBox(width: 8), + Expanded( + child: Text( + controller.errorMessage.value, + style: TextStyle( + color: Colors.red.shade700, + fontSize: 14, + ), + ), + ), + ], + ), + ) + : const SizedBox.shrink()), + + const SizedBox(height: 24), + + // Login and Register buttons + Column( + children: [ + // Login button + _buildPrimaryButton( + label: 'Masuk', + icon: Icons.login, + isLoading: controller.isLoading.value, + onPressed: + controller.signInWithEmailPassword, + primaryColor: Colors.blue.shade700, + isOutlined: false, + ), + + const SizedBox(height: 16), + + // // Register button + // _buildPrimaryButton( + // label: 'Daftar Akun Baru', + // icon: Icons.person_add, + // isLoading: false, + // onPressed: () { + // Get.to( + // () => const TeacherRegisterPage()); + // }, + // primaryColor: Colors.blue.shade700, + // isOutlined: true, + // ), + ], + ), + ], + ), + ), + )), + + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ), + ), + ); + } + + // Helper method to build text fields + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required IconData prefixIcon, + bool isPassword = false, + TextInputType keyboardType = TextInputType.text, + }) { + return TextField( + controller: controller, + obscureText: isPassword, + keyboardType: keyboardType, + style: const TextStyle(fontSize: 16), + decoration: InputDecoration( + labelText: label, + labelStyle: TextStyle(color: Colors.grey.shade600), + prefixIcon: Icon(prefixIcon, color: Colors.blue.shade300), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide(color: Colors.blue.shade500, width: 1.5), + ), + filled: true, + fillColor: Colors.grey.shade50, + contentPadding: const EdgeInsets.symmetric(vertical: 16), + ), + ); + } + + // Enhanced helper method to build primary/secondary buttons + Widget _buildPrimaryButton({ + required String label, + required IconData icon, + required VoidCallback onPressed, + required bool isLoading, + required Color primaryColor, + required bool isOutlined, + }) { + return ElevatedButton( + onPressed: isLoading ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: isOutlined ? Colors.white : primaryColor, + foregroundColor: isOutlined ? primaryColor : Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + side: isOutlined + ? BorderSide(color: primaryColor, width: 1.5) + : BorderSide.none, + ), + elevation: isOutlined ? 0 : 2, + disabledBackgroundColor: + isOutlined ? Colors.grey.shade100 : primaryColor.withOpacity(0.6), + disabledForegroundColor: isOutlined ? Colors.grey : Colors.white70, + ), + child: isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 20), + const SizedBox(width: 10), + Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ); + } +} diff --git a/lib/dashboard_app.dart b/lib/dashboard_app.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..0a6b420 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,93 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyCLw5Y3FPYwfij3VfTDOciRFukzpki9gYk', + appId: '1:1072369607324:web:d17906f3452e68cb6f4359', + messagingSenderId: '1072369607324', + projectId: 'sirekomendasi-dc7de', + authDomain: 'sirekomendasi-dc7de.firebaseapp.com', + storageBucket: 'sirekomendasi-dc7de.firebasestorage.app', + measurementId: 'G-V7MND9NDT5', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyBIHdD_rH3YSfuZYcNKFd1YCMTXxYb6gdc', + appId: '1:1072369607324:android:f76a929d6f6258726f4359', + messagingSenderId: '1072369607324', + projectId: 'sirekomendasi-dc7de', + storageBucket: 'sirekomendasi-dc7de.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDI7KTuPhqGiIkX5pznUDYkToUmIhznqf8', + appId: '1:1072369607324:ios:1d8cb1dbadb289ff6f4359', + messagingSenderId: '1072369607324', + projectId: 'sirekomendasi-dc7de', + storageBucket: 'sirekomendasi-dc7de.firebasestorage.app', + androidClientId: '1072369607324-hp2vujt95ipkft4n348a96ndbiocqrdo.apps.googleusercontent.com', + iosClientId: '1072369607324-0e0k7pn3nqk6suq97jnegljp3k0161o5.apps.googleusercontent.com', + iosBundleId: 'com.example.forwardChainingManApp', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyDI7KTuPhqGiIkX5pznUDYkToUmIhznqf8', + appId: '1:1072369607324:ios:1d8cb1dbadb289ff6f4359', + messagingSenderId: '1072369607324', + projectId: 'sirekomendasi-dc7de', + storageBucket: 'sirekomendasi-dc7de.firebasestorage.app', + androidClientId: '1072369607324-hp2vujt95ipkft4n348a96ndbiocqrdo.apps.googleusercontent.com', + iosClientId: '1072369607324-0e0k7pn3nqk6suq97jnegljp3k0161o5.apps.googleusercontent.com', + iosBundleId: 'com.example.forwardChainingManApp', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyCLw5Y3FPYwfij3VfTDOciRFukzpki9gYk', + appId: '1:1072369607324:web:7c9de102a40074a66f4359', + messagingSenderId: '1072369607324', + projectId: 'sirekomendasi-dc7de', + authDomain: 'sirekomendasi-dc7de.firebaseapp.com', + storageBucket: 'sirekomendasi-dc7de.firebasestorage.app', + measurementId: 'G-4JLGBJVJBM', + ); + +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..38f565c --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as rootBundle; +import 'package:flutter/services.dart'; +import 'package:forward_chaining_man_app/app/views/about/page_about.dart'; +import 'package:forward_chaining_man_app/app/views/page_intro.dart'; +import 'package:forward_chaining_man_app/app/views/page_login.dart'; +import 'package:forward_chaining_man_app/app/views/page_profile.dart'; +import 'package:forward_chaining_man_app/app/views/splash_screen/page/page_splash_screen.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'package:firebase_core/firebase_core.dart'; +import 'package:intl/intl.dart' as intl; + +import 'package:url_launcher/url_launcher.dart'; + +// Global flag for developer mode +bool developerMode = false; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final studentLoginController = Get.put(StudentLoginController()); + return GetMaterialApp( + debugShowCheckedModeBanner: false, + title: 'Forward Chaining Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const SplashScreen(), + ); + } +} + +/// Root widget aplikasi + +// Helper function to replace the simple dialog with our new UI + diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..40f0536 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,145 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +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 "forward_chaining_man_app") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.forward_chaining_man_app") + +# 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) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# 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 dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# 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/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/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/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..f6f23bf --- /dev/null +++ b/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/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/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/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..f16b4c3 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +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/linux/main.cc b/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/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/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..6d9f0de --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,124 @@ +#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) + +// 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, "forward_chaining_man_app"); + 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, "forward_chaining_man_app"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + 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); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), 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() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#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/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..bc1dbc8 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import cloud_firestore +import firebase_auth +import firebase_core +import google_sign_in_ios +import path_provider_foundation +import share_plus +import shared_preferences_foundation +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..c795730 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8150ede --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,709 @@ +// !$*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 */; }; + E83DAF1C4F7E72A74A7B0907 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 26E55B693DCA8742C2CEC4F6 /* GoogleService-Info.plist */; }; +/* 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 */ + 26E55B693DCA8742C2CEC4F6 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + 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 /* forward_chaining_man_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = forward_chaining_man_app.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 */, + 26E55B693DCA8742C2CEC4F6 /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* forward_chaining_man_app.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 /* forward_chaining_man_app.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 */, + E83DAF1C4F7E72A74A7B0907 /* GoogleService-Info.plist 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.forwardChainingManApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/forward_chaining_man_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/forward_chaining_man_app"; + }; + 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.forwardChainingManApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/forward_chaining_man_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/forward_chaining_man_app"; + }; + 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.forwardChainingManApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/forward_chaining_man_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/forward_chaining_man_app"; + }; + 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.14; + 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.14; + 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.14; + 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/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..adc6779 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..8e02df2 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/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/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..dac0f33 --- /dev/null +++ b/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 = forward_chaining_man_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.forwardChainingManApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/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/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/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/macos/Runner/GoogleService-Info.plist b/macos/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..2c107a7 --- /dev/null +++ b/macos/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 1072369607324-0e0k7pn3nqk6suq97jnegljp3k0161o5.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.1072369607324-0e0k7pn3nqk6suq97jnegljp3k0161o5 + ANDROID_CLIENT_ID + 1072369607324-hp2vujt95ipkft4n348a96ndbiocqrdo.apps.googleusercontent.com + API_KEY + AIzaSyDI7KTuPhqGiIkX5pznUDYkToUmIhznqf8 + GCM_SENDER_ID + 1072369607324 + PLIST_VERSION + 1 + BUNDLE_ID + com.example.forwardChainingManApp + PROJECT_ID + sirekomendasi-dc7de + STORAGE_BUCKET + sirekomendasi-dc7de.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1072369607324:ios:1d8cb1dbadb289ff6f4359 + + \ No newline at end of file diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/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/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/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/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/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/ms.mmd b/ms.mmd new file mode 100644 index 0000000..8b1e3d1 --- /dev/null +++ b/ms.mmd @@ -0,0 +1,60 @@ +flowchart TD + A[Mulai Proses Forward Chaining] --> B1[Inisialisasi Working Memory] + subgraph "1. Persiapan Working Memory" + B1 --> B2["Konversi Jawaban Pengguna ke Format Q{id}=Yes/No"] + B2 --> B3["Contoh: Q1=No, Q2=Yes, Q3=Yes, dst."] + end + subgraph "2. Perhitungan Bobot" + C1["Inisialisasi Map untuk Menyimpan Bobot: minatBobotTotal, minatBobotBenar, minatContrib"] + C2["Hitung Total Bobot per Minat, Contoh: IPA (Sains Murni) Farmasi: 31 poin"] + end + subgraph "3. Pembentukan Aturan" + D1["Buat Aturan untuk Setiap Pertanyaan"] + D2["Contoh: Jika Q2=Yes maka +6 poin ke IPA (Sains Murni) Farmasi"] + end + subgraph "4. Proses Penalaran" + E1["Jalankan Mesin Penalaran (Forward Chaining)"] + E2["Periksa Setiap Aturan, Jika Aturan Belum Dijalankan dan Kondisi JIKA Terpenuhi"] + E3["Eksekusi Bagian MAKA, Tambahkan Bobot dan Catat Aturan yang Dijalankan"] + E4["Ulangi Sampai Tidak Ada Aturan Baru yang Terpenuhi"] + + E1 --> E2 + E2 --> E3 + E3 --> E4 + E4 -->|"Masih Ada Aturan"| E2 + end + subgraph "5. Perhitungan Skor" + F1["Hitung Persentase Skor untuk Setiap Minat"] + F2["Contoh Hasil: IPA Farmasi 71%, Teknik Biomedis 69%, Lingkungan 55%"] + end + subgraph "6. Pengurutan dan Penyusunan Hasil" + G1["Urutkan Minat Berdasarkan Persentase"] + G2["Jika Persentase Sama, Urutkan Berdasarkan Total Bobot Benar"] + G3["Ambil 3 Rekomendasi Teratas"] + + G1 --> G2 + G2 --> G3 + end + subgraph "7. Pembentukan Hasil Akhir" + H1["Ambil Data untuk Setiap Rekomendasi: Karir, Jurusan, Aturan, Kursus, Universitas"] + H2["Contoh Rekomendasi 1: IPA Farmasi, Skor 71%, Karir: Apoteker, Peneliti Obat, Jurusan: Farmasi, Kimia"] + end + %% Alur proses + B3 --> C1 + C1 --> C2 + C2 --> D1 + D1 --> D2 + D2 --> E1 + E4 -->|"Tidak Ada Aturan Baru"| F1 + F1 --> F2 + F2 --> G1 + G3 --> H1 + H1 --> H2 + H2 --> I[Selesai: Hasilkan RecommendationResult] + + %% Tambahan info atau log + J1["Hasil Penalaran: 22 Aturan Terpenuhi dari Total 48 Pertanyaan"] + J2["Hasil Skor: Farmasi 71%, Teknik Biomedis 69%, Lingkungan 55%"] + + E4 -.-> J1 + F2 -.-> J2 \ No newline at end of file diff --git a/ms.svg b/ms.svg new file mode 100644 index 0000000..e10f068 --- /dev/null +++ b/ms.svg @@ -0,0 +1 @@ +
7. Pembentukan Hasil Akhir
6. Pengurutan dan Penyusunan Hasil
5. Perhitungan Skor
4. Proses Penalaran
3. Pembentukan Aturan
2. Perhitungan Bobot
1. Persiapan Working Memory
Masih Ada Aturan
Tidak Ada Aturan Baru
Ambil Data untuk Setiap Rekomendasi: Karir, Jurusan, Aturan, Kursus, Universitas
Contoh Rekomendasi 1: IPA Farmasi, Skor 71%, Karir: Apoteker, Peneliti Obat, Jurusan: Farmasi, Kimia
Urutkan Minat Berdasarkan Persentase
Jika Persentase Sama, Urutkan Berdasarkan Total Bobot Benar
Ambil 3 Rekomendasi Teratas
Hitung Persentase Skor untuk Setiap Minat
Contoh Hasil: IPA Farmasi 71%, Teknik Biomedis 69%, Lingkungan 55%
Jalankan Mesin Penalaran (Forward Chaining)
Periksa Setiap Aturan, Jika Aturan Belum Dijalankan dan Kondisi JIKA Terpenuhi
Eksekusi Bagian MAKA, Tambahkan Bobot dan Catat Aturan yang Dijalankan
Ulangi Sampai Tidak Ada Aturan Baru yang Terpenuhi
Buat Aturan untuk Setiap Pertanyaan
Contoh: Jika Q2=Yes maka +6 poin ke IPA (Sains Murni) Farmasi
Inisialisasi Map untuk Menyimpan Bobot: minatBobotTotal, minatBobotBenar, minatContrib
Hitung Total Bobot per Minat, Contoh: IPA (Sains Murni) Farmasi: 31 poin
Konversi Jawaban Pengguna ke Format Q{id}=Yes/No
Inisialisasi Working Memory
Contoh: Q1=No, Q2=Yes, Q3=Yes, dst.
Mulai Proses Forward Chaining
Selesai: Hasilkan RecommendationResult
Hasil Penalaran: 22 Aturan Terpenuhi dari Total 48 Pertanyaan
Hasil Skor: Farmasi 71%, Teknik Biomedis 69%, Lingkungan 55%
\ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..550b294 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,810 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "401dd18096f5eaa140404ccbbbf346f83c850e6f27049698a7ee75a3488ddb32" + url: "https://pub.dev" + source: hosted + version: "1.3.52" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "8dfe49146560af035314477995bdfb4a7104f33cb7f881748572fbda38ecfa55" + url: "https://pub.dev" + source: hosted + version: "5.6.4" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: dc0bcc3239a4feaa4b7b184dc4b689bc97e6cea1a8320ee0d7ee64b3607858ea + url: "https://pub.dev" + source: hosted + version: "6.6.4" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "5091ed6771c886c9a93b1ec1ffa61770056210f1a3312095a73a5a4bab0c6a9f" + url: "https://pub.dev" + source: hosted + version: "4.4.4" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + data_table_2: + dependency: "direct main" + description: + name: data_table_2 + sha256: "3b4ffb66ab20f238bf5f89af37e3bedfc9bef456c1fe24a79487ec1864574c84" + url: "https://pub.dev" + source: hosted + version: "2.5.16" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: d0e2213daf3a740a379d4b788865dee6ada220496640abd875adb8f0fdbe5ba6 + url: "https://pub.dev" + source: hosted + version: "5.5.0" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "9c291bafc2a88599405fc810a4b4c249a40065b094379afb701efe443a3b0653" + url: "https://pub.dev" + source: hosted + version: "7.6.0" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: a971b2f3a9c16b45e12687bb2e3d0da0c6fc272c4fbf6fa7833d0587844cca4d + url: "https://pub.dev" + source: hosted + version: "5.14.0" + firebase_core: + dependency: transitive + description: + name: firebase_core + sha256: "6a4ea0f1d533443c8afc3d809cd36a4e2b8f2e2e711f697974f55bb31d71d1b8" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + url: "https://pub.dev" + source: hosted + version: "5.4.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: e47f5c2776de018fa19bc9f6f723df136bc75cdb164d64b65305babd715c8e41 + url: "https://pub.dev" + source: hosted + version: "2.21.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" + url: "https://pub.dev" + source: hosted + version: "0.69.2" + 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_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + url: "https://pub.dev" + source: hosted + version: "2.0.17" + 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: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a + url: "https://pub.dev" + source: hosted + version: "10.8.0" + get: + dependency: "direct main" + description: + name: get + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 + url: "https://pub.dev" + source: hosted + version: "4.7.2" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: fad6ddc80c427b0bba705f2116204ce1173e09cf299f85e053d57a55e5b2dd56 + url: "https://pub.dev" + source: hosted + version: "6.2.2" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "7af72e5502c313865c729223b60e8ae7bce0a1011b250c24edcf30d3d7032748" + url: "https://pub.dev" + source: hosted + version: "6.1.35" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: "8468465516a6fdc283ffbbb06ec03a860ee34e9ff84b0454074978705b42379b" + url: "https://pub.dev" + source: hosted + version: "5.8.0" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: ada595df6c30cead48e66b1f3a050edf0c5cf2ba60c185d69690e08adcc6281b + url: "https://pub.dev" + source: hosted + version: "0.12.4+3" + http: + dependency: transitive + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image_gallery_saver_plus: + dependency: "direct main" + description: + name: image_gallery_saver_plus + sha256: "199b9e24f8d85e98f11e3d35571ab68ae50626ad40e2bb85c84383f69a6950ad" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: fa39707f36786707b01eca7626d2c16c32aa603b3f3a146518518458847dc127 + url: "https://pub.dev" + source: hosted + version: "3.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + 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: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.dev" + source: hosted + version: "2.2.15" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + 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: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + 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" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + url: "https://pub.dev" + source: hosted + version: "10.1.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + url: "https://pub.dev" + source: hosted + version: "5.0.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + url: "https://pub.dev" + source: hosted + version: "2.4.6" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + 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: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + url: "https://pub.dev" + source: hosted + version: "6.3.14" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + 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: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + url: "https://pub.dev" + source: hosted + version: "1.1.18" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + url: "https://pub.dev" + source: hosted + version: "1.1.16" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + url: "https://pub.dev" + source: hosted + version: "14.2.4" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + win32: + dependency: transitive + description: + name: win32 + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + url: "https://pub.dev" + source: hosted + version: "5.10.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: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..6ccd35e --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,40 @@ +name: forward_chaining_man_app +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + cloud_firestore: ^5.6.4 + data_table_2: 2.5.16 + firebase_auth: ^5.5.0 + fl_chart: ^0.69.0 + flutter: + sdk: flutter + flutter_animate: ^4.5.2 + flutter_svg: ^2.0.17 + get: ^4.6.6 + google_sign_in: ^6.2.2 + intl: ^0.20.2 + lottie: ^3.2.0 + shared_preferences: ^2.5.2 + url_launcher: ^6.3.1 + path_provider: ^2.1.5 + share_plus: ^10.1.4 + image_gallery_saver_plus: ^4.0.1 + google_fonts: ^6.2.1 + font_awesome_flutter: ^10.8.0 +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + + + +flutter: + uses-material-design: true + + assets: + - assets/ \ No newline at end of file diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..0208451 --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + forward_chaining_man_app + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..5770594 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "forward_chaining_man_app", + "short_name": "forward_chaining_man_app", + "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/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/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/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..e2cbeaa --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(forward_chaining_man_app 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 "forward_chaining_man_app") + +# 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/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/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/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..9ab8234 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/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/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..f0ea876 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore + firebase_auth + firebase_core + share_plus + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +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/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/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/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..8826a9f --- /dev/null +++ b/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", "forward_chaining_man_app" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "forward_chaining_man_app" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "forward_chaining_man_app.exe" "\0" + VALUE "ProductName", "forward_chaining_man_app" "\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/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/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/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/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/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..f4d13e2 --- /dev/null +++ b/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"forward_chaining_man_app", 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/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/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/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/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/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/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/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/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/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/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_