From 74408e62a68e44e90eeaf5adc16289c84e57b7e5 Mon Sep 17 00:00:00 2001 From: Itzfebry Date: Thu, 17 Jul 2025 14:46:48 +0700 Subject: [PATCH] first commit android --- .gitignore | 75 ++ .metadata | 30 + IMPLEMENTATION_SUMMARY.md | 148 +++ QUIZ_AUTO_STOP_IMPLEMENTATION.md | 196 ++++ README.md | 16 + analysis_options.yaml | 28 + assets/data/mata_pelajaran.json | 53 + assets/icons/settings-sliders.png | Bin 0 -> 558 bytes assets/images/dashboardsiswa.png | Bin 0 -> 8824 bytes .../images/iPhone 14 & 15 Pro Max - 2 (3).png | Bin 0 -> 9185 bytes assets/images/matapelajaran.png | Bin 0 -> 10863 bytes assets/images/skoda.png | Bin 0 -> 61065 bytes assets/images/skolearn_icon.png | Bin 0 -> 171079 bytes assets/images/welcomescreen.png | Bin 0 -> 16731 bytes flutter_auth_project/README.md | 76 ++ flutter_auth_project/pubspec.lock | 354 ++++++ flutter_auth_project/pubspec.yaml | 20 + lib/constans/api_constans.dart | 59 + lib/constans/constansts_export.dart | 2 + lib/constans/theme_constant.dart | 95 ++ lib/debug_ssl_override.dart | 10 + lib/main.dart | 39 + lib/middlewares/auth_middleware.dart | 20 + .../detail_submit_tugas_siswa_mode.dart | 196 ++++ lib/models/kelas_model.dart | 54 + lib/models/mata_pelajaran_model.dart | 190 +++ lib/models/mata_pelajaran_simple_model.dart | 116 ++ lib/models/materi_buku_model.dart | 133 +++ lib/models/materi_video_model.dart | 133 +++ lib/models/quiz_answer_model.dart | 88 ++ lib/models/quiz_attempt_model.dart | 151 +++ lib/models/quiz_guru_model.dart | 78 ++ lib/models/quiz_mode.dart | 186 +++ lib/models/quiz_question_model.dart | 110 ++ lib/models/tahun_ajaran.dart | 60 + lib/models/tugas_model.dart | 272 +++++ lib/routes/app_pages.dart | 186 +++ lib/routes/app_routes.dart | 43 + lib/routes/export.dart | 22 + lib/views/auth/bindings/auth_binding.dart | 9 + .../auth/controllers/auth_controller.dart | 74 ++ .../controllers/check_token_controller.dart | 35 + lib/views/auth/login_page.dart | 253 ++++ lib/views/common/selection_page.dart | 20 + lib/views/common/splash_screen.dart | 92 ++ lib/views/common/welcome_page.dart | 64 + .../dashboard/bindings/dashboard_binding.dart | 9 + .../dashboard_guru_controller.dart | 51 + lib/views/guru/dashboard/index.dart | 411 +++++++ .../bindings/matpel_guru_binding.dart | 15 + .../controllers/kelas_controller.dart | 58 + .../mata_pelajaran_guru_controller.dart | 56 + .../controllers/tahun_ajaran_controller.dart | 67 ++ .../guru/mata_pelajaran/filter_matpel.dart | 165 +++ lib/views/guru/mata_pelajaran/index.dart | 264 +++++ lib/views/guru/profiles/index.dart | 198 ++++ .../bindings/matpel_quiz_guru_binding.dart | 15 + .../bindings/quiz_detail_guru_binding.dart | 9 + .../guru/quiz/bindings/quiz_guru_binding.dart | 9 + .../quiz_detail_guru_controller.dart | 60 + .../controllers/quiz_guru_controller.dart | 61 + lib/views/guru/quiz/index.dart | 284 +++++ lib/views/guru/quiz/quiz.dart | 171 +++ lib/views/guru/quiz/quiz_detail.dart | 154 +++ .../detail_submit_tugas_siswa_binding.dart | 10 + .../bindings/tugas_detail_guru_binding.dart | 13 + .../tugas/bindings/tugas_guru_binding.dart | 10 + .../detail_submit_tugas_siswa_controller.dart | 49 + .../review_submit_tugas_controller.dart | 69 ++ .../tugas_detail_guru_controller.dart | 64 + lib/views/guru/tugas/detail.dart | 182 +++ lib/views/guru/tugas/detail_submit_tugas.dart | 369 ++++++ lib/views/guru/tugas/filter_tugas.dart | 180 +++ lib/views/guru/tugas/index.dart | 193 +++ lib/views/guru/tugas/review_submit_tugas.dart | 353 ++++++ .../siswa/bindings/notifikasi_binding.dart | 9 + lib/views/siswa/bindings/siswa_binding.dart | 11 + .../siswa/bindings/ubah_password_binding.dart | 9 + .../controllers/notifikasi_controller.dart | 80 ++ .../notifikasi_count_controller.dart | 49 + .../siswa/controllers/siswa_controller.dart | 141 +++ .../controllers/ubah_password_controller.dart | 65 ++ .../bindings/mata_pelajaran_binding.dart | 9 + .../mata_pelajaran_controller.dart | 56 + .../mata_pelajaran_simple_controller.dart | 55 + .../siswa/matapelajaran/mata_pelajaran.dart | 321 +++++ .../siswa/materi/bindings/materi_binding.dart | 9 + .../materi/controllers/materi_controller.dart | 131 +++ lib/views/siswa/materi/index.dart | 587 ++++++++++ lib/views/siswa/notifikasi.dart | 352 ++++++ lib/views/siswa/profile.dart | 708 +++++++++++ .../quiz/bindings/matpel_quiz_binding.dart | 10 + .../siswa/quiz/bindings/quiz_binding.dart | 9 + .../quiz/bindings/quiz_finish_binding.dart | 9 + .../quiz/bindings/soal_quiz_binding.dart | 11 + .../controllers/quiz_attempt_controller.dart | 604 ++++++++++ .../quiz/controllers/quiz_controller.dart | 98 ++ .../controllers/quiz_finish_controller.dart | 117 ++ .../controllers/quiz_question_controller.dart | 364 ++++++ lib/views/siswa/quiz/matpel_quiz.dart | 416 +++++++ lib/views/siswa/quiz/matpel_quiz_detail.dart | 659 +++++++++++ lib/views/siswa/quiz/soal_quiz.dart | 707 +++++++++++ lib/views/siswa/quiz/soal_quiz_selesai.dart | 300 +++++ .../ranking/bindings/matpel_rank_binding.dart | 10 + .../ranking/bindings/quiz_rank_binding.dart | 9 + .../ranking/bindings/ranking_binding.dart | 9 + .../controllers/ranking_controller.dart | 94 ++ lib/views/siswa/ranking/index.dart | 480 ++++++++ lib/views/siswa/ranking/matpel_rank.dart | 481 ++++++++ .../siswa/ranking/matpel_rank_detail.dart | 396 +++++++ lib/views/siswa/ruang_diskusi.dart | 125 ++ lib/views/siswa/siswaDashboard.dart | 413 +++++++ .../tugas/bindings/detail_tugas_binding.dart | 9 + .../tugas/bindings/submit_tugas_binding.dart | 9 + .../siswa/tugas/bindings/tugas_binding.dart | 10 + .../controllers/submit_tugas_controller.dart | 110 ++ .../tugas/controllers/tugas_controller.dart | 148 +++ lib/views/siswa/tugas/tugas.dart | 478 ++++++++ lib/views/siswa/tugas/tugas_commit.dart | 789 +++++++++++++ lib/views/siswa/tugas/tugas_detail.dart | 1036 +++++++++++++++++ lib/views/siswa/ubah_password.dart | 456 ++++++++ lib/widgets/my_date_format.dart | 30 + lib/widgets/my_snackbar.dart | 42 + lib/widgets/my_text.dart | 45 + pubspec.lock | 754 ++++++++++++ pubspec.yaml | 44 + test/widget_test.dart | 31 + 127 files changed, 19239 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 IMPLEMENTATION_SUMMARY.md create mode 100644 QUIZ_AUTO_STOP_IMPLEMENTATION.md create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 assets/data/mata_pelajaran.json create mode 100644 assets/icons/settings-sliders.png create mode 100644 assets/images/dashboardsiswa.png create mode 100644 assets/images/iPhone 14 & 15 Pro Max - 2 (3).png create mode 100644 assets/images/matapelajaran.png create mode 100644 assets/images/skoda.png create mode 100644 assets/images/skolearn_icon.png create mode 100644 assets/images/welcomescreen.png create mode 100644 flutter_auth_project/README.md create mode 100644 flutter_auth_project/pubspec.lock create mode 100644 flutter_auth_project/pubspec.yaml create mode 100644 lib/constans/api_constans.dart create mode 100644 lib/constans/constansts_export.dart create mode 100644 lib/constans/theme_constant.dart create mode 100644 lib/debug_ssl_override.dart create mode 100644 lib/main.dart create mode 100644 lib/middlewares/auth_middleware.dart create mode 100644 lib/models/detail_submit_tugas_siswa_mode.dart create mode 100644 lib/models/kelas_model.dart create mode 100644 lib/models/mata_pelajaran_model.dart create mode 100644 lib/models/mata_pelajaran_simple_model.dart create mode 100644 lib/models/materi_buku_model.dart create mode 100644 lib/models/materi_video_model.dart create mode 100644 lib/models/quiz_answer_model.dart create mode 100644 lib/models/quiz_attempt_model.dart create mode 100644 lib/models/quiz_guru_model.dart create mode 100644 lib/models/quiz_mode.dart create mode 100644 lib/models/quiz_question_model.dart create mode 100644 lib/models/tahun_ajaran.dart create mode 100644 lib/models/tugas_model.dart create mode 100644 lib/routes/app_pages.dart create mode 100644 lib/routes/app_routes.dart create mode 100644 lib/routes/export.dart create mode 100644 lib/views/auth/bindings/auth_binding.dart create mode 100644 lib/views/auth/controllers/auth_controller.dart create mode 100644 lib/views/auth/controllers/check_token_controller.dart create mode 100644 lib/views/auth/login_page.dart create mode 100644 lib/views/common/selection_page.dart create mode 100644 lib/views/common/splash_screen.dart create mode 100644 lib/views/common/welcome_page.dart create mode 100644 lib/views/guru/dashboard/bindings/dashboard_binding.dart create mode 100644 lib/views/guru/dashboard/controllers/dashboard_guru_controller.dart create mode 100644 lib/views/guru/dashboard/index.dart create mode 100644 lib/views/guru/mata_pelajaran/bindings/matpel_guru_binding.dart create mode 100644 lib/views/guru/mata_pelajaran/controllers/kelas_controller.dart create mode 100644 lib/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart create mode 100644 lib/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart create mode 100644 lib/views/guru/mata_pelajaran/filter_matpel.dart create mode 100644 lib/views/guru/mata_pelajaran/index.dart create mode 100644 lib/views/guru/profiles/index.dart create mode 100644 lib/views/guru/quiz/bindings/matpel_quiz_guru_binding.dart create mode 100644 lib/views/guru/quiz/bindings/quiz_detail_guru_binding.dart create mode 100644 lib/views/guru/quiz/bindings/quiz_guru_binding.dart create mode 100644 lib/views/guru/quiz/controllers/quiz_detail_guru_controller.dart create mode 100644 lib/views/guru/quiz/controllers/quiz_guru_controller.dart create mode 100644 lib/views/guru/quiz/index.dart create mode 100644 lib/views/guru/quiz/quiz.dart create mode 100644 lib/views/guru/quiz/quiz_detail.dart create mode 100644 lib/views/guru/tugas/bindings/detail_submit_tugas_siswa_binding.dart create mode 100644 lib/views/guru/tugas/bindings/tugas_detail_guru_binding.dart create mode 100644 lib/views/guru/tugas/bindings/tugas_guru_binding.dart create mode 100644 lib/views/guru/tugas/controllers/detail_submit_tugas_siswa_controller.dart create mode 100644 lib/views/guru/tugas/controllers/review_submit_tugas_controller.dart create mode 100644 lib/views/guru/tugas/controllers/tugas_detail_guru_controller.dart create mode 100644 lib/views/guru/tugas/detail.dart create mode 100644 lib/views/guru/tugas/detail_submit_tugas.dart create mode 100644 lib/views/guru/tugas/filter_tugas.dart create mode 100644 lib/views/guru/tugas/index.dart create mode 100644 lib/views/guru/tugas/review_submit_tugas.dart create mode 100644 lib/views/siswa/bindings/notifikasi_binding.dart create mode 100644 lib/views/siswa/bindings/siswa_binding.dart create mode 100644 lib/views/siswa/bindings/ubah_password_binding.dart create mode 100644 lib/views/siswa/controllers/notifikasi_controller.dart create mode 100644 lib/views/siswa/controllers/notifikasi_count_controller.dart create mode 100644 lib/views/siswa/controllers/siswa_controller.dart create mode 100644 lib/views/siswa/controllers/ubah_password_controller.dart create mode 100644 lib/views/siswa/matapelajaran/bindings/mata_pelajaran_binding.dart create mode 100644 lib/views/siswa/matapelajaran/controllers/mata_pelajaran_controller.dart create mode 100644 lib/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart create mode 100644 lib/views/siswa/matapelajaran/mata_pelajaran.dart create mode 100644 lib/views/siswa/materi/bindings/materi_binding.dart create mode 100644 lib/views/siswa/materi/controllers/materi_controller.dart create mode 100644 lib/views/siswa/materi/index.dart create mode 100644 lib/views/siswa/notifikasi.dart create mode 100644 lib/views/siswa/profile.dart create mode 100644 lib/views/siswa/quiz/bindings/matpel_quiz_binding.dart create mode 100644 lib/views/siswa/quiz/bindings/quiz_binding.dart create mode 100644 lib/views/siswa/quiz/bindings/quiz_finish_binding.dart create mode 100644 lib/views/siswa/quiz/bindings/soal_quiz_binding.dart create mode 100644 lib/views/siswa/quiz/controllers/quiz_attempt_controller.dart create mode 100644 lib/views/siswa/quiz/controllers/quiz_controller.dart create mode 100644 lib/views/siswa/quiz/controllers/quiz_finish_controller.dart create mode 100644 lib/views/siswa/quiz/controllers/quiz_question_controller.dart create mode 100644 lib/views/siswa/quiz/matpel_quiz.dart create mode 100644 lib/views/siswa/quiz/matpel_quiz_detail.dart create mode 100644 lib/views/siswa/quiz/soal_quiz.dart create mode 100644 lib/views/siswa/quiz/soal_quiz_selesai.dart create mode 100644 lib/views/siswa/ranking/bindings/matpel_rank_binding.dart create mode 100644 lib/views/siswa/ranking/bindings/quiz_rank_binding.dart create mode 100644 lib/views/siswa/ranking/bindings/ranking_binding.dart create mode 100644 lib/views/siswa/ranking/controllers/ranking_controller.dart create mode 100644 lib/views/siswa/ranking/index.dart create mode 100644 lib/views/siswa/ranking/matpel_rank.dart create mode 100644 lib/views/siswa/ranking/matpel_rank_detail.dart create mode 100644 lib/views/siswa/ruang_diskusi.dart create mode 100644 lib/views/siswa/siswaDashboard.dart create mode 100644 lib/views/siswa/tugas/bindings/detail_tugas_binding.dart create mode 100644 lib/views/siswa/tugas/bindings/submit_tugas_binding.dart create mode 100644 lib/views/siswa/tugas/bindings/tugas_binding.dart create mode 100644 lib/views/siswa/tugas/controllers/submit_tugas_controller.dart create mode 100644 lib/views/siswa/tugas/controllers/tugas_controller.dart create mode 100644 lib/views/siswa/tugas/tugas.dart create mode 100644 lib/views/siswa/tugas/tugas_commit.dart create mode 100644 lib/views/siswa/tugas/tugas_detail.dart create mode 100644 lib/views/siswa/ubah_password.dart create mode 100644 lib/widgets/my_date_format.dart create mode 100644 lib/widgets/my_snackbar.dart create mode 100644 lib/widgets/my_text.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 test/widget_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66207b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +### Flutter/Dart ### +# Core Flutter/Dart files to ignore +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +*.dart.js +*.js.map +*.info.json +**/.last_build_id + +# Generated files +**/doc/api/ +*.symbols +*.map.json + +### Platform Specific ### +# Android +/android/ +/local.properties +**/gradle-wrapper.jar + +# iOS +/ios/ +**/Podfile.lock +**/Pods/ + +# Web +/web/ + +# Desktop +/windows/ +/linux/ +/macos/ + +### Build Directories ### +**/debug/ +**/profile/ +**/release/ +**/test/coverage/ + +### IDE ### +# IntelliJ/Android Studio +*.iml +*.ipr +*.iws +.idea/ + +# VS Code (uncomment if needed) +#.vscode/ + +### Environment ### +.env +.env* +.env.local +.env.*.local + +### System Files ### +.DS_Store +Thumbs.db +*.swp +*.log + +### Version Control ### +.svn/ +.history/ +migrate_working_dir/ + +### Miscellaneous ### +*.class +*.pyc +.atom/ +.buildlog/ \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..01965b7 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: android + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..26bd8dd --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,148 @@ +# Ringkasan Implementasi Quiz Auto-Stop + +## ✅ Implementasi Selesai + +Implementasi fitur quiz auto-stop ketika soal habis di level tertentu telah berhasil diselesaikan. Berikut adalah ringkasan lengkap: + +## 📁 File yang Dimodifikasi + +### 1. `lib/views/siswa/quiz/controllers/quiz_question_controller.dart` + +**Perubahan:** + +- ✅ Menambahkan penanganan khusus untuk response 404 dengan pesan soal habis +- ✅ Menambahkan penanganan untuk response 204 No Content +- ✅ Menambahkan pengecekan pesan dalam response 200 +- ✅ Menambahkan method `handleQuestionsExhausted()` +- ✅ Menambahkan method `autoFinishQuiz()` + +### 2. `lib/views/siswa/quiz/controllers/quiz_attempt_controller.dart` + +**Perubahan:** + +- ✅ Menambahkan method `stopQuizTimer()` untuk menghentikan timer quiz + +## 🔧 Fitur yang Diimplementasikan + +### 1. Deteksi Soal Habis + +Sistem dapat mendeteksi soal habis melalui: + +- **Status Code 404** dengan pesan "Tidak ada soal lagi di level ini" +- **Status Code 204** (No Content) +- **Status Code 200** dengan pesan soal habis dalam response body + +### 2. Penanganan Otomatis + +Ketika soal habis terdeteksi: + +1. **Timer Berhenti**: Timer quiz dihentikan secara otomatis +2. **Pesan User**: Dialog "Soal sudah habis di level ini, quiz selesai" ditampilkan +3. **Auto Finish**: Quiz diselesaikan otomatis dengan mengirim jawaban kosong untuk soal yang belum dijawab +4. **Redirect**: User diarahkan ke halaman hasil quiz + +### 3. Error Handling + +- **Network Error**: Menampilkan dialog error koneksi dengan opsi kembali ke dashboard +- **Auto-Finish Failure**: Menampilkan dialog error dengan opsi kembali ke dashboard +- **Fallback**: Langsung redirect ke halaman quiz selesai jika terjadi error + +## 🎯 Alur Kerja Lengkap + +``` +1. User mengerjakan quiz + ↓ +2. Sistem memanggil API next-question + ↓ +3. Backend mengembalikan 404/204/200 dengan pesan soal habis + ↓ +4. Frontend mendeteksi soal habis + ↓ +5. Timer quiz dihentikan + ↓ +6. Dialog pesan ditampilkan ke user + ↓ +7. Auto-finish quiz dipanggil + ↓ +8. Jawaban kosong dikirim untuk soal yang belum dijawab + ↓ +9. Endpoint auto-finish dipanggil + ↓ +10. User diarahkan ke halaman hasil quiz +``` + +## 📝 Pesan yang Didukung + +Sistem mendeteksi soal habis berdasarkan pesan berikut: + +- "Tidak ada soal lagi di level ini" +- "Soal habis" +- "Questions exhausted" +- "No more questions" + +## 🔍 Logging + +Sistem menambahkan logging detail untuk debugging: + +```dart +log("404 - Soal habis di level ini detected"); +log("Confirmed: Questions exhausted at this level"); +log("Quiz timer stopped"); +log("Auto finishing quiz for attempt ID: $attemptId"); +log("Auto finish success: $json"); +``` + +## ✅ Test Cases yang Harus Diuji + +1. **Response 404 dengan pesan soal habis** + + - Backend: 404 + "Tidak ada soal lagi di level ini" + - Expected: Timer berhenti, dialog muncul, auto-finish + +2. **Response 204 No Content** + + - Backend: 204 + - Expected: Timer berhenti, auto-finish + +3. **Response 200 dengan pesan soal habis** + + - Backend: 200 + message soal habis + - Expected: Timer berhenti, auto-finish + +4. **Response 404 tanpa pesan soal habis** + + - Backend: 404 tanpa pesan khusus + - Expected: Error dialog umum, tidak auto-finish + +5. **Network error saat auto-finish** + - Simulasi: Network error + - Expected: Error dialog koneksi, opsi kembali ke dashboard + +## 🚀 Cara Penggunaan + +Implementasi ini sudah terintegrasi otomatis dalam sistem quiz. Tidak ada konfigurasi tambahan yang diperlukan. Sistem akan: + +1. **Otomatis mendeteksi** ketika backend mengembalikan response yang mengindikasikan soal habis +2. **Otomatis menghentikan** timer quiz +3. **Otomatis menampilkan** pesan ke user +4. **Otomatis menyelesaikan** quiz dan mengarahkan ke halaman hasil + +## 📋 Checklist Implementasi + +- ✅ Penanganan response 404 dengan pesan soal habis +- ✅ Penanganan response 204 No Content +- ✅ Penanganan response 200 dengan pesan soal habis +- ✅ Method untuk menghentikan timer quiz +- ✅ Method untuk auto-finish quiz +- ✅ Dialog pesan untuk user +- ✅ Error handling untuk network error +- ✅ Error handling untuk auto-finish failure +- ✅ Fallback mechanism +- ✅ Logging untuk debugging +- ✅ Dokumentasi lengkap + +## 🎉 Hasil Akhir + +Dengan implementasi ini, quiz akan **otomatis berhenti** dan **tidak akan stuck** ketika soal habis di level manapun. User akan mendapat pengalaman yang smooth dengan notifikasi yang jelas dan sistem yang robust dalam menangani berbagai skenario error. + +**Implementasi selesai dan siap untuk digunakan!** 🚀 diff --git a/QUIZ_AUTO_STOP_IMPLEMENTATION.md b/QUIZ_AUTO_STOP_IMPLEMENTATION.md new file mode 100644 index 0000000..4193c60 --- /dev/null +++ b/QUIZ_AUTO_STOP_IMPLEMENTATION.md @@ -0,0 +1,196 @@ +# Implementasi Quiz Auto-Stop ketika Soal Habis di Level + +## Deskripsi + +Implementasi ini menambahkan fitur untuk menghentikan quiz otomatis ketika soal habis di level tertentu. Sistem akan mendeteksi response 404 atau pesan khusus dari backend dan secara otomatis menghentikan timer, menampilkan pesan ke user, dan menyelesaikan quiz. + +## File yang Dimodifikasi + +### 1. `lib/views/siswa/quiz/controllers/quiz_question_controller.dart` + +#### Perubahan Utama: + +- **Penanganan Response 404**: Menambahkan logika khusus untuk mendeteksi ketika soal habis di level tertentu +- **Penanganan Response 204**: Menambahkan penanganan untuk status 204 No Content +- **Pengecekan Pesan Response**: Memeriksa pesan dalam response body untuk mendeteksi soal habis +- **Method `handleQuestionsExhausted()`**: Method baru untuk menangani ketika soal habis +- **Method `autoFinishQuiz()`**: Method untuk menyelesaikan quiz otomatis + +#### Logika Deteksi Soal Habis: + +```dart +// Cek status code 404 +if (response.statusCode == 404) { + String responseBody = response.body.toLowerCase(); + if (responseBody.contains("tidak ada soal lagi di level ini") || + responseBody.contains("soal habis") || + responseBody.contains("questions exhausted")) { + await handleQuestionsExhausted(attemptId); + } +} + +// Cek status code 204 +if (response.statusCode == 204) { + await handleQuestionsExhausted(attemptId); +} + +// Cek pesan dalam response 200 +if (json.containsKey('message')) { + String message = json['message'].toString().toLowerCase(); + if (message.contains("tidak ada soal lagi di level ini") || + message.contains("soal habis") || + message.contains("questions exhausted") || + message.contains("no more questions")) { + await handleQuestionsExhausted(attemptId); + } +} +``` + +### 2. `lib/views/siswa/quiz/controllers/quiz_attempt_controller.dart` + +#### Perubahan Utama: + +- **Method `stopQuizTimer()`**: Method baru untuk menghentikan timer quiz yang dapat dipanggil dari controller lain + +## Alur Kerja + +### 1. Deteksi Soal Habis + +Sistem akan mendeteksi soal habis melalui: + +- **Status Code 404** dengan pesan "Tidak ada soal lagi di level ini" +- **Status Code 204** (No Content) +- **Status Code 200** dengan pesan soal habis dalam response body + +### 2. Penanganan Soal Habis + +Ketika soal habis terdeteksi: + +1. **Hentikan Timer**: Memanggil `stopQuizTimer()` untuk menghentikan countdown +2. **Tampilkan Pesan**: Menampilkan dialog "Soal sudah habis di level ini, quiz selesai" +3. **Auto Finish Quiz**: + - Kirim jawaban kosong untuk soal yang belum dijawab + - Panggil endpoint auto-finish quiz + - Redirect ke halaman hasil quiz + +### 3. Auto Finish Process + +```dart +Future autoFinishQuiz(String attemptId) async { + // 1. Kirim jawaban kosong untuk soal yang belum dijawab + for (String qid in quizAttemptC.allQuestionIds) { + if (!quizAttemptC.answeredQuestions.containsKey(qid)) { + await quizAttemptC.postQuizAttemptAnswer( + quizAttemptId: attemptId, + questionId: qid, + jawabanSiswa: "", + ); + } + } + + // 2. Panggil endpoint auto-finish + final response = await http.post( + Uri.parse("${ApiConstants.quizAutoFinishEnpoint}/$attemptId"), + headers: headers, + ); + + // 3. Redirect ke halaman hasil + if (response.statusCode == 200) { + Get.offAllNamed('/quiz-selesai', arguments: {'quiz_id': quizId}); + } +} +``` + +## Pesan yang Didukung + +Sistem akan mendeteksi soal habis berdasarkan pesan berikut dalam response: + +- "Tidak ada soal lagi di level ini" +- "Soal habis" +- "Questions exhausted" +- "No more questions" + +## Error Handling + +### 1. Network Error + +Jika terjadi error koneksi saat auto-finish: + +- Tampilkan dialog error koneksi +- Berikan opsi untuk kembali ke dashboard + +### 2. Auto-Finish Failure + +Jika auto-finish gagal: + +- Tampilkan dialog error +- Berikan opsi untuk kembali ke dashboard + +### 3. Fallback + +Jika terjadi error dalam penanganan soal habis: + +- Langsung redirect ke halaman quiz selesai + +## Testing + +### Test Cases yang Harus Diuji: + +1. **Response 404 dengan pesan soal habis** + + - Backend mengembalikan 404 + "Tidak ada soal lagi di level ini" + - Timer harus berhenti + - Dialog pesan harus muncul + - Quiz harus auto-finish + +2. **Response 204 No Content** + + - Backend mengembalikan 204 + - Timer harus berhenti + - Quiz harus auto-finish + +3. **Response 200 dengan pesan soal habis** + + - Backend mengembalikan 200 + message soal habis + - Timer harus berhenti + - Quiz harus auto-finish + +4. **Response 404 tanpa pesan soal habis** + + - Backend mengembalikan 404 tanpa pesan khusus + - Tampilkan error dialog umum + - Tidak auto-finish + +5. **Network error saat auto-finish** + - Simulasi network error + - Tampilkan error dialog + - Berikan opsi kembali ke dashboard + +## Logging + +Sistem menambahkan logging yang detail untuk debugging: + +```dart +log("404 - Soal habis di level ini detected"); +log("Confirmed: Questions exhausted at this level"); +log("Quiz timer stopped"); +log("Auto finishing quiz for attempt ID: $attemptId"); +log("Auto finish success: $json"); +``` + +## Dependencies + +Implementasi ini menggunakan: + +- `GetX` untuk state management dan navigation +- `http` package untuk API calls +- `shared_preferences` untuk menyimpan data lokal +- `dart:async` untuk timer management + +## Catatan Penting + +1. **Timer Management**: Timer quiz akan dihentikan secara otomatis ketika soal habis terdeteksi +2. **User Experience**: User akan mendapat notifikasi yang jelas bahwa quiz selesai karena soal habis +3. **Data Integrity**: Semua soal yang belum dijawab akan dikirim dengan jawaban kosong +4. **Error Recovery**: Sistem memiliki fallback untuk menangani berbagai jenis error +5. **Logging**: Semua proses dicatat dengan detail untuk memudahkan debugging diff --git a/README.md b/README.md new file mode 100644 index 0000000..a545398 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# ui + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/assets/data/mata_pelajaran.json b/assets/data/mata_pelajaran.json new file mode 100644 index 0000000..5696893 --- /dev/null +++ b/assets/data/mata_pelajaran.json @@ -0,0 +1,53 @@ +[ + { + "kelas": "VI", + "mapel": [ + { + "nama": "Matematika", + "materi": [ + {"id": 1, "judul": "Materi 1", "url": "url_materi_1"}, + {"id": 2, "judul": "Materi 2", "url": "url_materi_2"}, + {"id": 3, "judul": "Materi 3", "url": "url_materi_3"} + ], + "video": [ + {"id": 1, "judul": "Video 1", "url": "url_video_1"}, + {"id": 2, "judul": "Video 2", "url": "url_video_2"}, + {"id": 3, "judul": "Video 3", "url": "url_video_3"} + ], + "update": "8 Agustus", + "semester": "1 & 2", + "guru": "Bu Siti" + }, + { + "nama": "Bahasa Inggris", + "materi": [ + {"id": 1, "judul": "Materi 1", "url": "url_materi_1"}, + {"id": 2, "judul": "Materi 2", "url": "url_materi_2"}, + {"id": 3, "judul": "Materi 3", "url": "url_materi_3"} + ], + "video": [ + {"id": 1, "judul": "Video 1", "url": "url_video_1"}, + {"id": 2, "judul": "Video 2", "url": "url_video_2"} + ], + "update": "2 Agustus", + "semester": "1 & 2", + "guru": "Mrs. Ratna" + }, + { + "nama": "Bahasa Indonesia", + "materi": [ + {"id": 1, "judul": "Materi 1", "url": "url_materi_1"}, + {"id": 2, "judul": "Materi 2", "url": "url_materi_2"}, + {"id": 3, "judul": "Materi 3", "url": "url_materi_3"} + ], + "video": [ + {"id": 1, "judul": "Video 1", "url": "url_video_1"}, + {"id": 2, "judul": "Video 2", "url": "url_video_2"} + ], + "update": "2 Agustus", + "semester": "1 & 2", + "guru": "Mrs. Ratna" + } + ] + } +] diff --git a/assets/icons/settings-sliders.png b/assets/icons/settings-sliders.png new file mode 100644 index 0000000000000000000000000000000000000000..488409d2b493f25e41a1c4c5af84a2de1d2b0ba2 GIT binary patch literal 558 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 literal 0 HcmV?d00001 diff --git a/assets/images/iPhone 14 & 15 Pro Max - 2 (3).png b/assets/images/iPhone 14 & 15 Pro Max - 2 (3).png new file mode 100644 index 0000000000000000000000000000000000000000..85bc0d2c6813d926507a6dfc6f112671894deba5 GIT binary patch literal 9185 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 F{ts1%=EeX3 literal 0 HcmV?d00001 diff --git a/assets/images/skoda.png b/assets/images/skoda.png new file mode 100644 index 0000000000000000000000000000000000000000..f8aceb265b0e11be1274c8f301a4268799129b4c GIT binary patch literal 61065 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 literal 0 HcmV?d00001 diff --git a/assets/images/skolearn_icon.png b/assets/images/skolearn_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ba67140fbde219db2cc00dd40b43629d96a263 GIT binary patch literal 171079 zcmeFXRahp?vM%`H(zrD4?(XjHE{(gp%Lg>jxVt-zd*klz?oQ+G^r8Q?X6?Pt%*8x2 zx96gss>q6bD?>6WA~VAjs4@Tma`*RwhWu<9Tigo&yumm~YB>V{ z2>pLAkR%!e8~^~k-BMN4MN>|e+t}Wg-q6I}$dum0*5Q*I0N@qya4!KjEG~uJrV3T8%a}Y7Lu$1(2GF9=CS2gysHs&&+5a5U9_2B*lur+ltB=oSg zv2*73;G_5lm;3YiZ!-hMCzF$j8Mm^i_`g{`DLx7d7Z(R^1_pO`cY1dgdV42x1|}{p zE(S(s24-fuPYya~PdgVw4>~(%l1~I!!hbMCO`VOMEFD}d?d=HvVj3FRySngEP{0!Y z8(PT4#mUm>Kj?PO^oIYmxc==z|0y(sp@}mC6FuYKVG$DY{)?Ym$i(?C`LB7e= z0R5}W|AY}Y`b1%4W@Bb&<6>fE;^1QAWc*Kue_sAiepP!{V~f9{@H791?Z4Xo;pJuc zt6dw*e<|f3;@?{NPmh18>|c!k%`E@#O=V*IA2T_)I@$c=0w%@`rZ%RwrgkoWjl}eC zBbgX;yI8u|nEscEglt^?r-?p&kK4x3&YX|JgU;kro31u46#ObO3bJBC%JPJCgrBDP zze)6O5MGA=hX4OV9*6(t)qnl&pUL&t<33+}3d8bmVLmBtAy*d*dnbN5LJ3Pd!_S!` zZffM@X=vx{X=uUwm;L`@`1JaJhW-CZzn|0cKMMXIdSKl8{!Ab`+vBwc{|P9c#D7B4=VoVIBlE1ov)NWkiQ*-q@p0v7)A@8!79LKF36Z}6P@9$3>RU?p z(vM}UW82lF{}`IPfmr*F)KIiw7y8I`N5Q`(0fxXD^Hm#H8|}N9q6wNOcL^SgsydtPB7to3zp*JS%*48TwL6B#CYU5oyB~qQh|6B_Qrt}{~8>t z)xXG(k9k1HKkaPXp#SIbQ$PPl4;?dFmW}Jalt3sD4**bxh!Zh90Wb;XQ1|AJpL+!2 zgwic$PNxci6ik^5u*?@GLR#1RR#1~(Oio*CKiZL0R>0h1q&)}=;Kcn^M@6G z0FFUif|=+%kqOimtTv_v1GHO}|pwg;g^{ zOB3c+HHs**+o|RzWJ#ks&~ZIJ8vZGOJeAjp`6EOVkOSJPTdEy z#D2g{g|92;fl|G590NgT55|A${tTyn=cPQQAZmv1KJUKLApAw;z$0oOw98H@FSNe_ z?DVefxRqzMa)D+Ovq4|LZC zf+h86iokR5=3q^?3NMT|22=*LVeC@-T|c5#*HZSU3(OuK0o zfSZb8V996_R6st%+>yZ5EK$YGy4Zm4DoIZ?NmhsSOvV)+GO?zErpfNVeHG};85byd zaH-%XjVjHPO;ibdHzi#Kq&{@-Rjjz{SWsN!Niw!NFajT;9V$VW^51?fZ$1yNG~2k` z{RDa8k}f~ESWx7}@7@FOehs8axUSQZwaUQ2t$_F3txQG=)Y3 z9@ap7Uk=phOH1|c15i-YU&0DvI+zPiMP+3QiTDf-3t~Czc*_&opmIBf#;?dza#yj9 z;eJG^KycD{$-2OlDzw;WhZ$7m>v*#D15>iFVAk0;u2;Aw+$d@kBvBUm`-G-``-Fut z{;G6qM`(0V8MTrZ=DpsXcS!cimF9PI6ZEwdF2hSY`x>)egPbAq$Egd}Aqccu^x6K? zHa&gXo5vQ)V0X^n)xFy^1n(R{J$0*ZI0H5%T8&cGwsUULTxx{y_}IY8c_Z>y6u#BQ zKfBg2f(^1zzH;y2CRTKORV)tw&25Y2_4`|PW0ZL$f*mnaP)cvPGLBeohwztGm(7oB z)h4~N;sK|?xkS^_Ib3Xs_{3fUsxbs2`TN$h1vAYfP^j2=RvHDn?<3^(pm{G95g{i1 zlq3GqI^yrgFUq!GJPLG&dm=FB&H3^IZ;cm8zzqNBETp`}-Po6kgT6)u1-zJS&vmqA zR(zGr5^SAmib>MV_5*a&umL^><%-z2x#t1zKW42wUw8%a;kU0rdG*i{aSo-KNaTuv z$+<-UwT_|VL&-0qFm%_^6zqxSv#p*1+Q3%X7wIfBG~;3{abAV~C;Pe*Gt9u)k=TY$U+a+7$$gJdJ(IAQ3lvr-E5nWWyyh(ZCi$b6l zq*9}}P#NQo@Y_Jkko&_Y_*y8i8fs7aJ^lkee|l}fF~|fXrj9-LCf8kV1>2glq}xj~a(_ji zfc=Z@p4Qlek+Zk0&ep~OJdpofB50IlpPs=x9l?nk!Dv@2{+f1JP#sIDDX&0~oAWNQ z=!U4TL8=HAn#+PeXSOX!^-JbVZ$Sk*aqW27CtM7!87Kx2_VeVxxtfMs11P${fZ@*|uXfowcP`4lhH>oYF(<};` z%{w`U3OX>IEoYkcslUr~{fc1rrwTtZ31b|7y|sI(yu|1E;7W2GSeUrW9CM|_>qxbY z2L7-hIFWZ`#QjCn60Z`=sUllVZCD@4&hNfsrW)hme)`g=VRfy82V9`A`|->E8!vT6Zx#VK9v{i-oCu5YRULG17dP?w`| zHv#o6+|oMt3tI``&1wNhpijYWxEEDopK=j5XkYN-cFzHPaDa69?R$>8ZJRi7gJ?-< zktVaQc>tUrF)AVbhgu*6>L@)E}1};)Vy^A1RgU!DG)M2k*63R1ZSfP%r05 zJ!_P^Fs;SBOpNSF%6$|ja(LiTRn!~2gL>$0fxD}^@I>!_nOQ3%j6Mt1d^+O?Zb4qLoGWLvL9z+{!7W*Edi(%n7sc9#KHvf1hSCKw zl;?+0`g?|8m~3IYQ!;B3aw(6>CFT-&QKcA0Z5GcjIKw$(=>)Au4|^iDUjkjeQ+@Lw zX8*!$oUSiEtvi14q+kDspJml$)VkjykM{U3G9`bmwr#&VHmVx8QOO4IsKnYS?ZUa% ztDmV)o$_LdB!LTsyA!fxZCi}tTF|RLwnf${6 zq-@2xr$IzCwUKh_nFe_Qz`OH9ZHXlDz)kR#n1_E(Z%I|m^CeCwmj9IyC#WMvZPE8d z&*={I?JLlnp~-!-O^G&&|Mc^R;GC#F#-agpB^&I=L3rz-qNDuK0(K@2Rm$%95J)6x z3+f^9>TK%9gq)uQAK(i$I5=!ZX~qS!RUEm2wwm#r2o9;#U2JUBu8PYTJ-__Mi_Ly^ z4TdD*1({GM@(tmZg?^+z^rbF^`-XqRP2zm4zNwvpPh2a02|AO#u!t9j4vb-BX}owG zTOTBsrG$EH@oI3Ib|`R}j9nX$?7Dg<%`D@K-YzM@c*+^EZy+zOgFj2366y(iuMQc7 zy5xBj{sND%YtFN-8MAk$snII4H%>}59f$YhDa5i8x&#*B02sI>d>J2jn1l~yUOTQA zhMyJ~vS?i20J$mve9!^U-yYemkvwf#teXbRPsMChrGneXcCfF?#jRxVnZ zZ!3IIMTEMp=hiQ1a4#HJ=m?4TgV`3Kx+3@tn`1m`S_M;-e_b{I(^X*re%}Zm?~F~_ zLt$C;?&lXR5)3I!(!oQ@!2g6sYn+IB?Z7=41fr7_j0vjwM~#-iccPnfE?K8HiYKw7 zhCK&^?zA`af}u>B&4+QcNOsi9U^OHx`u_WH=G6<==6Kiy z5$%eG3<~kWSJC(P?(*aq1q;UuR0-s94%lbh*~I9shQ0#t($AEzPNN&x{29GzipJPY zbNRHGWVAoN&1A-njJfFD6ci`M%*+eogrkXrDA~X9v}H(@VXigmSqf_EpoqA7{eVg6 zAQ(T^jkIjGjeP^nlTw*omY={WeN@~&sX?bjj)l$mBfuPBA19ebBa$@)u82BVAzt-! zOciNEV7PfWZ`g7UnYNsCTb!uoRD1`B4q~bdNKQ_R@EQDd#Koc}BE0pz+iNz>p{Dx& zJ9Z@Cs9jhjI#}^iqxGUSFmDRnEjaMR4`NW8hjzzAEaSP}lPopX-vh!+lgpf}s=$q~ zqJ8x_+(i$4NvhmyK+KCPSK*t@W&35c)50-}$fpNKe5%3@%mWfg0K!nM0j7RD_tY)# zX{^DooDJZmG07BIC}CBXNOs}Oz+bkMWK`NpR9{d}b5=)?<-1i|+miPSgVe;-w`v;k zUk%z`&bD3Er!c^#t_0RIch1X$f!*uB69*lGXrMcIJLC|>aG@BtrTZjP65?C!PwA;G zu=o02yrvRnfSR~bM0r`6g2w-yLpg2mUKbqp;YP= z4B;{LqW-aL~nl5gI#4ig8n&0ld%A7Roob#$Y^4N@Y|MlwJ<8u$`Y^NYC~ zd|2L(5Aw(Ob)XT<%n`t2(#ZSBV0~Ep57KAYsu%sNKR!zjjKJ3cOr=OOtujw7-TT`GppDqdJ&-Mde^j=x<0Sd6b6hF8FH%WmR;oyU_j0>)@JTX*&Y)H}( z>fVa>;x4+MAeXAgAg3}Y36*-l z-il(lR_e24X1a5P8X6jG^s0SO&GjTOVp6?I69r~-gQk{;`IQ@PBR!&H6J7j0iYIUg z#pJCZimATHpC)PYoV+3$;*^%!X;@Kt=_9)Ouo4q}J_TKhB?5`;4bDD>$ml8O(>!@k z9|N^nOF2cw;yEC{C$kG@N)pt^HjK+VHuRycO1c4ueYw*j2i>CQRIlsv7xrL@cRtK5>6Ft)2cQOvB8L{b_KWf~|e*6;77ZVJ9NpNE%_e zhO=X6aTT613d!0p7wR5=zRSL~z%pnH!Zt78RHRr|V8pug#T*A6KZ0z8B>jLb^hnl! zR$AYAp3Rhw?0HjEKp|D;|Dq`3xEYVhIvE|2-t(A^bz;@9tfzA+>BRWX7gS!9HBIC0 zWkDDw$?Q_~XT4uJS!^t(DWTg}^ks8^N3V)wXwZ||2jL)QNLZB_Vt>B17l=_1H#LeM zh2hLN1$xNO{@aC^Kjc}>7rUU{hZ4B-nc;Uet@8MuU18(+ww~C0Xcs7uR3@9_BOKd< zdO!nxE`t?d5^Ejy(PJ!rqZi~}_ASn3!`!4{gMD!3FaT2dl0DezS>B!jz@D`ulB=J1 zM)m1CdBrlu7H+YD5Y~{?-yhsmx1&4SkPWPWYhpQ_*i5ETY9c@yS3lDR2 zCUC0FC|KwRpIw2P#aPEjI}S?pMH9$DIq2KqQ0Mo+wS1$mhG!xQ3O1ZZS&oxsP~ z*t-&@)Rja?W;BsETkJ`U(`@mAW+I~D~;YrKr7158z0r&RvmC^#G4N1v2S9Kt9S zW-#1Pl!G$)h)7U9eb-k*8{io2^r<^z+zMOO^r8_I^k}2ExF?s7=ROm4fuM%2)pt-e zPb}5TZ#?aeTd5OR>kMw;LIaCKdda4PWl(+lkBW#}mEx(IN~l4qW2LkqI%V8=svY=p zJ}cG5QIRBh?Ue8POUIzU`%{6V@(%?^Ppi)y;4M4n8W1!GIn6dNp-tYZ@0Di-$_@1_20H6(F5U$ zfBD6f`$L?m&W+9D?n|u zHD;|t{czX#Di6jJgb5N=y`!vU0vE(FA*hTC=YEEK?x+tU$ng6Bj-?}8TVfCj$W6(t zZAL)yJ0FXYg7BIOK({kldo~5=eYPc+o)nIYfFiXV?%@A}yO06rIfHXS>fsG;I<=X7 zo$aW4_zZz13qj=LMBYnqHm45yRQ(9ij=!5}$4PdM#=eII9&e|#mBCyC#zRY70^x4u zDkb#JP}5<5-5x|s++gRijo!Voxaos@17(ycnkp7jzAuXwnsN}YeIckvv^@;g&|11< zbYAQ@l*PM439qROVUlby-rOY=^h-H!fr&F`rlckMEce!8=`KYbq50$xfbb~J3*C(q zVQX`Tl`HbzFP?+_8EzFI_K$Who`_T<$BLs%arb5(30Zg#*7_@Y66E&5L2;jtaOY%G zJrB9r8;CsAUW8fD@QR#5aBVuGIipnxRIoW5v=8rb`nIo?f2xO-m7U2*52mCz;0)s@ z0B@?q7r2ZrZ-Z%lYG|D-49V10ZuqT!4iFE)P`q`J30-CUCVH?!ZjMXQec=3HW|%`< z@$?m?=|~r>nwg80NsN!F9Pm;;!gG>vd8|-{ZC|7#&oeZj66NyqqD#iJ}VL4*g87yP5;)#dC;1lWW#ls}>r7AAYwANFfLg1oqy;6r9UK81H;dpJLc!;5P$b8M*-kS$?t@#Dt*wnHN)K#r9~SX`ub8}Vg1L$SeoPyAo3H+0Yn}T{w&p|+ z7Jp%s?q+Zx=q(+Hzzq)yjfU93rMqI;HC+6t%r8*H%5C2T?>rkvWFG^xRavBNJZ`U8 zkK2&Od-4^f4=acR{kh7Mzw}vRy0BaAc~x{|Z;5(IE_FxdayQJpmI}VTx(o>ax#8iD zh^66p^U6L3SWp~r&FNiQ?CFhE!i#s<#e<|_%B4t3&F zEhT10O+y#51hRf11xzv_$&&2nA|s;@w7ljcC*5Me(*ImW8tWeJ|BhWYKx7mXbrUO* z=ut#qT5yz1QQf+(#QgQ8^DYZ7uJm1GTK|<2br&y5FN)w#-F2$=eB3gX=O~lVMAPX2 zptJmac&sE8JJ%okvx4NyE9;^lj)8C(!t(>D==~XFRtD-ev5%0F@>plraWXE+KkVPU zq5TPcCJLC#W@j)BQ5$2FsIho!$)XUPwIJZZ4zk7D%A2sosr?2PH75Ngp+)hfk0^{J5M7V0}z7`|2w2Dngw1Ku;D`4mloj3R#^5^eQf^+ z_!O@nxZov?<6>aqsR9%DSk~rrsknQrw&MEM-^}z&o<~toi}&ST?Ww*vBDtOdD0NKn zDC?s5Dx4JzKCI%nCHL;aR@86&sqmAilms}-I&c(m;d!Wf0;z)0b7oECCoxK&XNgwd z3KJ7X^FA6ifhRa=$q*CQV1Nm;lg^I#$fP!pqNPjB5BQ4hxJRAAUAn{c$+)%W(`~W9 zKP)BF?+exWThEw`P$n`&ZT6y%$@<+j26Dob2Ncg9YhO4%8{L zFN$mymwj5t+6f@dUPIfNyfnt>x1i0nSg&dBs?(@zk_XA7q7Jh%b2oB&VGcbCC>y&;D`)1{= z3CfEfE1>nc+*zSBcRZ;CcGm@UetE73xy^>;gA6a1o(=b>IdGZkVwi@qtya%MaNKoCOD0m9AyGzyMK%n|0R3KNwh60E-6{?ZrFt`zgPhX)P79cbH~SYslU z3l(%4_t7`>d>>MqZ4ptTHUdbuYN*1XNwit2);&S@HJ+Kur5QUYar(f-5#rPnfn4z#>)_FWyiwYOnHZ?sy?0@HL4%v^+UvSm_H*nnq}iJZw3fGsDVB}cHDO; zo@3h_NY!*#()ARhp@`c0lkADIxtMH8r$l)Bd`2g1R{SU+Lax?Nx-eI&s6<#$jk4Fw z^N{KWsnYRHMF}CA9M7*z7R<-mkxJt(YRg0=863+=;)#9+O^{?n2v)e(S-{%XWIB30r8q!l)pOM ze#oLs@-5hc79VB%JP}4af#+C3>yT0}GN7kM36dT9Qnt6<3gu!$6P&^6P#iE5E`5O@ zfWZZ8ry}(kyf`q2!`)Y)wvPFxPIat`f}{~Hj;`iJFTv=EBUfICHD#$g?dkr}vX+Ml z>K7R}UrMxt8yi@X02gj(T2)Xny}PagI4!jtHsP-q6d;IQs!DFt$eSq<{mc|0{w~Ca zZ2O0Af8auspY-($)^?c?-q>n?(}b1iGisho%N>tlAdAKnf_tM@7{!H?)ql0}_BLYy zN9YuZMG}+Nh^roF1{;Ca+)%9Jg;ivffeTbupkJRe-KMS&=l%)TeFC{^Us~@IvY-;T zfFjc}Db5u(J>k$tS3RizXJcy5m*5ZGS+#wh>D_q6gjuk(5)&buP_Rnl1A7tVJ$MtW zz5T3Mz!K|MTpG(nf{&y@D``i?-iHjNaDGMj&fy)c6{?Cm-}yLRzu`FM0kchLbw0wG z`b2FR0&plTVUWc5x2D?O-q%f3IYPBfNF*zHvPbZ2 z*%sSM+9i{X%JwNX&&)iyEDuH+AFw_t*jce-^*M6sNmnaihxX-N_xvFh>EQ%2DwJXR zSwM6n)lK~Tz4lm&ln|p_C}1c}>vEUsK6EvSlO4*T_h_u?!eLyw=J6QC(6dMIqH8ln zA95PS;b-BlduO;iQGzYY|5?Fn^@&Voy;ouCQpwD=Tmlw93(4!Xo!Wax!~tAx9XOef zQ*>2Cz_gISYEDY)yPkK^M`5lAq|f1; z>Z%=hgMLzZt=G8QaoZ^vlWq{^qg?cQ;`St7fT6Aw8K(@3nH{Ntd%n@zAzgw?N;?lR zYqD1FLvydUzm|HDEJEj*{;1rfY$?a_#1Yk1=->67&vgCQ0vqe9tZj%Ng@wQUBPMVw zbd)5NqW1ey;j_R(58?GGHUlVnlI~5Jtcn}k6*S;#t*y+ue7#hwzsz%K!0t6)6ST{z zYPksjJ^$NGUA_q&_V&ZJMWG$29JE~L;zes4=-oO|J&fXco z>^ucT(S12^j37h}x)S(ZOpoi+MPoMK2IcucSn2*XqmI?grVy@qa z(*^rBtYhwdR5W&g0m`Av7D}f^t#DGTK@@*jy6x5pDiGy6Zln$ND}xZde0X76m7v** zF#?(B;}nDvx2=%(ptXQ=gmC;Z*OqlNw59f| zFS)8&p=GUbz~(+F>nR7(lg(EHp_7Wfvq28ykETDM>sPP_?=U|o%{U3XPEritYy6|{ z)$}1;bgm{z-hZjU7C1@NtC@qJM2)d>f0u|;JrHLXFwuC4oRqUpMAG&@V3G*(o(-YST^ZOGUXn z8W2G{CS-^Ab|KmG5BrK<2eB_W>fj>SPnS8kUdVs3NmVrJ(D7}sprja zw$IOp8ytJ{w*FyuopQdtU*l;iunccY1*@m|b4u{TPgM?yN@JpD)IgCEIJz`~-2yNA z?7J7ox}coqA_ya5M53$nhP=0*CXVf1No+a}p&0##zT{>Jw>4IeI@AyVHQ8cI>$yWC zkLSj}(Y`ueG31xwUR7vGLrkx-8r|k~R_HJU7h8YPRe=s*1zPmS;{7 z^)xdf)TrB><1m69DstT^Tr0e;-M7tvS?Nqz-|TXk6?}$Cgs5+v8a=f^Hxr)$A|?{c ziuLcFB!KSGEeCvopgQNvI}QoF9p-oRW=tNIZT4w#`NoplLM&0qog{IM%f8^M73OK2 zs442{aSta(aZEzk9}zVq*BzcC#%|!-(tXQ9$@^$B#ah@P+vJnbyJ^5ehty^%mo=?c zsfbxP_J!VNOib86Qk!ZjWW6i0m}O9${*DCer}_51PGl~E1jns94(D=z&?5#wuY*|8 zQ%fKfjZcf9fBhDt=L2Fv5ToPW`95bd3Dg1v>~#HsD{KDf@7IsQO?%L3Y!Yp3HO@4L zmrrElb}ux!mp{n{H2@YxUuW#rRt;l|eC-fK;bbQfvQLr}PBl4Q}I{B}Xr z4Iz07oYZlmv;f>I-X)TgVxT{Fwhk+tGq`toj;j`=W9Q}sfBxGTkbAy_*vE?pB1+z7 zBTqq*B=_hMGR+rqCmTb+43U>ZDN3~$E1~;@N4+%6pu4`3JTTb+omf3FCJX__%Lj|< zI`)PQ@nWCRKnIKiVdtwBFmDlJRynwP@`MFL7gcN1F z)@)leZiOLP2+0veScOkaY{$6SbXHp)57_aC15e=S_#PZMU=eEi{xtxx3x9TE_T0S| zLa_5k$kV`Vw>*Jlb}DnXj<;{-%x+_Bz7>`+ez{N`?19XL!cmK@tUMW;E6Sd~bhg-P z@B2Wx1QXIqTzG2(Cue*hxiyywIT?nMNk4+hrcvD#sytL<&@brc>*IpU%UqktElqZ^>l)>YKv~?#xTN{l9&#QP#&Dhy$>53I3j@G6ncSbvxp#RqUGxyn92j z7)h0nP^wKezU&6c5C53aPh*^6s@A}!Mzu<#)Ja&orzZu46Q>RwWY+`f0PE_5n>&rr%&}kwN`{jrKXWmU_Y;(V&{R;T>UilCLgyymb<&|;;aX2zUGb^-Iy4@biyG&VzAiSwbRo1QA^Ektc zTM5sD4h#m#AYLZh`X5mRDZ}O{S%|%=6@&O46cF*wJ4CJ`q;M97lLfYPn#H^EiUxtz zH}W*aZxYhF2$NX?NXu|w(@$J!r5Imx&4HvRS(P-1ApC(rU<1Akz9@FB_lQJKPm0CV z(YCHn{kGLZ68z8H>+ib~B8`vVP}qB_;Q2f7KCEI~f^-FBEk;j*gU1-X2oq_aqiCC6 zK)3;Xce_)4J*YW|6)D{|IGk=&hYTO}LJcecYpncpGYml!NDD-Pi=>zDnp939gQF55 zM!*8ZzG9XBb{;t8`=Snh(YQ)we2KwB(RmWsr6Bi8t`J{tRw6VL8PAfJflpo};is&TQp0s$C$_rbk+!3>2igD$+9@kd( zkF^oTJbc(ur4!Q}v4AP1Z5mW0fZ>do^L7fN z`StKln{)>lpyfmjL z{JYp>6c_dglQi042u8w0AL@%7KRCOrNsvrkkrP8i+pA`1^jQ9AV zsJk{I*WR1{uD~C`24t3I8syyz%Io?)q7^|H=f~T@QMfv)=7M2ktPHBj4GQ*^C@<3il zKi6Nk^1A2tU?#xFc{I2QtOmQ@C>DS+9BH7EEarrAu*+%Wq;P$b{_pM`rvcg*5a{I* zNEBH+$zjeTmXWo!B{8F~JG=m{US4L{;b~Z-%as&vNSeezkdanvv;(#ko@fq(i=oX<#ik55KdreENw^KJ`q_Db1O@z-JX-a9Dj1KmNSxrM*+Ju`QWcWcSOlm8=?0OMD zqe}ZWSY1`|D1xaBSLnE1&bRpE+LjrGFoh2#zETC#{@xof{dfOJpXdHi_w$?qI$cVe z2G~>5TZ3k4VaW=&reb=xEkkc2N}_5LWoLI37Gqp7W)Z8+xP`iLSdbSjHK!-~W+FEu z;)>qvcsC=N54@`%17xm zCW7q#oAx9eoVfhY=1m<6k~MNAM6J!&&2wv}tycTz>vST?GlLD|4gDCztsB@Oy}n!0`haaGV-Pi#}9}P~W$DfB_SfDX+UiXs9cgT_Cq&pMhc9z#Cg+Ch!yF;Fod5DjT}O@?#r+_1 znFW%4jO1FrtevF0?B&OVI4{)`E_wLru+NjVI(S!Gfc~kcxTYbccq>A51wJFU zhC1v4(bA3PGVn4c5=MX&M$)`L>UOAmPR+n*ZRBJD7Q*)(Ne!Lf!$?mDw8NAqKyeFT&s3ISXv-f2~i)q9WB{^k1_%B^X4D6jD!()|K&~B$HqjE@vN$)-12~ zwCm(vvYG8z@rvp%nyo{U?YGz6j0#?D4SoLfNUy>Ac`ZLgKTZt&iq+tSc3v%%(pq^>iac036r4tR=blD$S!HS&{oANp%`q4$pT)zzj}e$x%BuIm>DOB0(t3x4~MxfSF=)$F^fp} zlbaJ$y-OU6jkFX1Rpzlbv>Ej0D)~=zir%C6dey6Dy%^c86+-kc)wv8J2l&9Cdk}n| z19!t(Up9mWKqY8>RiCa|A7UBOtgFv?Wv!*8QmYPwH|Ao@+fuB~CBip7P%dY#MZ^!k zU8W}7s&^N2uzEZS|CFLJAf55|5wi-o+FCK8cFi)|@o(T&re+ zF)QyJGJ@y&33b6|NNIxLc930R>>E_$Sc=PF+7mJI^9p=pWb9hfxKXh&ZgISE=V9gnI9Q!a|9bmyTK1zWN)q1QSu0Pa_L1)5c@y87-HF%^1e7 zU&{^*leNrk@m`xM(d-9kRY&%O)-on&xQl&rw|UYr_gk0$JN4T<&j+-ojp1hXb?Vqf zggO6BhC>W0hFIQD0VkIDo_nb9l|}ZCmlyp3ovJ@dXEx^``O8~f0K~T66j!k{oeV^> zXEPcI60D}nn@9nfUKlmrBsISLFERv>J-oM`KYa3yj{_KVTvC1a8;|v#J7Ozw0C&@D zg!KrTbh#S5Ft7SAsMC8Ig<6`?#;M0L$s8dI;rQVQ-`G$Q7L6jpJ@HV!>sZv8?MHBj z7-Ar!D)!eZzw#p2u%nn#(bTHcR@`Gg-l-+}nF{1HUxVAI_?+e9-d4y-DaW>+Dciku zzo%}s<}EZEWB**T$Wgo*eqq{&X%=y8EVEKKZM8|w$<1oI&cuo!sa#wRNUNF&wC zf#8F%wYLw-2Ua#bx0hlLTS|sIACWLE-*iHJ(;D4}i?rqGGwjfy$ehf;=Nb>$bU1YL znJUkYKs*L9YYhP`>oZ($KSMkn481w8zxEGw+6eqm;gwD=OC?>vKlQ`#f6hDFekbtd zZ)&Am|8wIX{O3G-E2eS<;rbXg1CFh`#Ud?@$G=)+g!Fx=d9{w$L$H0hke87%Tqd(bRg@i zglW4M2fY#89ckV=(1In{GkJ@7OE-p1t#P>l8I`hq@L{;LX7mEw-3((~zED}hEeQM% zIY7UCc~i^Y;59Q3@)+=$m6w1%K>!j))m4}&_P4o_a5Pm7xQOY-rpc=FJ`&AonBmyf zon9@$DkF39MKm6EIS?HRXD|oz)2W)FsvSx7v>=Uv2>1hUaAgdWKK9|MY&Ej7@)VVl z!#$;Nfon=pIQ`N*ULYTbhZ$EJ)n4=uFrj{O7xE%S=r@BPT!p z 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 literal 0 HcmV?d00001 diff --git a/flutter_auth_project/README.md b/flutter_auth_project/README.md new file mode 100644 index 0000000..40b4a7c --- /dev/null +++ b/flutter_auth_project/README.md @@ -0,0 +1,76 @@ +# Flutter Authentication Project + +This project is a Flutter application that implements an authentication system using GetX. It features role-based login and routing for different user roles, including students, teachers, and admins. + +## Project Structure + +``` +flutter_auth_project +├── lib +│ ├── controllers +│ │ └── auth_controller.dart +│ ├── middlewares +│ │ └── auth_middleware.dart +│ ├── models +│ │ └── users.dart +│ ├── routes +│ │ ├── app_pages.dart +│ │ └── app_routes.dart +│ ├── services +│ │ └── auth_services.dart +│ ├── views +│ │ ├── admin +│ │ │ └── admin_dashboard.dart +│ │ ├── auth +│ │ │ └── login_page.dart +│ │ ├── common +│ │ │ ├── selection_page.dart +│ │ │ └── welcome_page.dart +│ │ ├── guru +│ │ │ └── guru_dashboard.dart +│ │ └── siswa +│ │ └── siswa_dashboard.dart +│ └── main.dart +├── pubspec.yaml +└── README.md +``` + +## Features + +- **Role-Based Authentication**: Users can log in as students, teachers, or admins, and are redirected to their respective dashboards. +- **GetX State Management**: Utilizes GetX for state management and dependency injection. +- **Middleware for Authentication**: Checks if the user is authenticated and redirects accordingly. + +## Setup Instructions + +1. **Clone the Repository**: + ``` + git clone + cd flutter_auth_project + ``` + +2. **Install Dependencies**: + Run the following command to install the required packages: + ``` + flutter pub get + ``` + +3. **Run the Application**: + Use the following command to run the application: + ``` + flutter run + ``` + +## Usage + +- Navigate to the login page to authenticate. +- After successful login, users will be redirected to their respective dashboards based on their roles. +- Admins, teachers, and students will have different functionalities available on their dashboards. + +## Contributing + +Contributions are welcome! Please feel free to submit a pull request or open an issue for any suggestions or improvements. + +## License + +This project is licensed under the MIT License. See the LICENSE file for more details. \ No newline at end of file diff --git a/flutter_auth_project/pubspec.lock b/flutter_auth_project/pubspec.lock new file mode 100644 index 0000000..421f26f --- /dev/null +++ b/flutter_auth_project/pubspec.lock @@ -0,0 +1,354 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + 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" + get: + dependency: "direct main" + description: + name: get + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 + url: "https://pub.dev" + source: hosted + version: "4.7.2" + http: + dependency: "direct main" + description: + name: http + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" + source: hosted + version: "0.13.6" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.dev" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + 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: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.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" + 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" + 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: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + url: "https://pub.dev" + source: hosted + version: "2.4.8" + 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.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + 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: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/flutter_auth_project/pubspec.yaml b/flutter_auth_project/pubspec.yaml new file mode 100644 index 0000000..73ad056 --- /dev/null +++ b/flutter_auth_project/pubspec.yaml @@ -0,0 +1,20 @@ +name: flutter_auth_project +description: A Flutter project with an authentication system using GetX. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + get: ^4.3.8 + http: ^0.13.3 + shared_preferences: ^2.0.6 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/lib/constans/api_constans.dart b/lib/constans/api_constans.dart new file mode 100644 index 0000000..79ee317 --- /dev/null +++ b/lib/constans/api_constans.dart @@ -0,0 +1,59 @@ +import 'constansts_export.dart'; + +class ApiConstants { + var idAttempt; + + static String? baseUrl = dotenv.env['URL']; + static String? baseUrlApi = "${dotenv.env['HOST']}"; + // Tambahkan log untuk debugging konfigurasi URL + static void debugApiUrls() { + // ignore: avoid_print + print("ApiConstants.baseUrl: $baseUrl"); + // ignore: avoid_print + print("ApiConstants.baseUrlApi: $baseUrlApi"); + // ignore: avoid_print + print("ApiConstants.loginEnpoint: $loginEnpoint"); + } + + static String loginEnpoint = "$baseUrlApi/login"; + static String logoutEnpoint = "$baseUrlApi/logout"; + + static String klsMatpelEnpoint = "$baseUrlApi/kelasmatapelajarans"; + static String getMeEnpoint = "$baseUrlApi/get-me"; + static String getMateriEnpoint = "$baseUrlApi/get-materi"; + static String mataPelajaranEnpoint = "$baseUrlApi/get-mata-pelajaran"; + static String mataPelajaranSimpleEnpoint = + "$baseUrlApi/get-mata-pelajaran-simple"; + static String tugasEnpoint = "$baseUrlApi/get-tugas"; + static String submitTugasEnpoint = "$baseUrlApi/submit-tugas"; + static String updateTugasEnpoint = "$baseUrlApi/update-tugas"; + + static String kelasEnpoint = "$baseUrlApi/kelas"; + static String tahunAjaranEnpoint = "$baseUrlApi/tahun-ajaran"; + + static String getDetailSubmitTugasSiswaEnpoint = + "$baseUrlApi/get-submit-tugas-siswa"; + + static String quizEnpoint = "$baseUrlApi/quiz"; + static String quizAttemptStartEnpoint = "$baseUrlApi/quiz-attempts/start"; + static String quizAttemptFinishEnpoint = "$baseUrlApi/quiz-attempts/finish"; + static String quizAutoFinishEnpoint = "$baseUrlApi/quiz-attempts/auto-finish"; + + static String quizTopFiveEnpoint = "$baseUrlApi/quiz-top-five"; + static String quizGuruEnpoint = "$baseUrlApi/quiz-guru"; + static String quizDetailGuruEnpoint = "$baseUrlApi/get-quiz-attempt-guru"; + + // Notifikasi + static String notifikasiCountEnpoit = "$baseUrlApi/siswa/notifikasi/count"; + static String notifikasiEnpoit = "$baseUrlApi/siswa/notifikasi"; + + // Ubah Password + static String ubahPasswordEnpoint = "$baseUrlApi/change-password"; + + // Ubah Password + static String analysisSiswaEnpoint = "$baseUrlApi/analysis-siswa"; + + static String checkToken = "$baseUrlApi/check-token"; + + static String dashboardGuruEndpoint = "$baseUrl/dashboard/guru"; +} diff --git a/lib/constans/constansts_export.dart b/lib/constans/constansts_export.dart new file mode 100644 index 0000000..c83ffd3 --- /dev/null +++ b/lib/constans/constansts_export.dart @@ -0,0 +1,2 @@ +export 'package:flutter_dotenv/flutter_dotenv.dart'; +export 'theme_constant.dart'; \ No newline at end of file diff --git a/lib/constans/theme_constant.dart b/lib/constans/theme_constant.dart new file mode 100644 index 0000000..a595751 --- /dev/null +++ b/lib/constans/theme_constant.dart @@ -0,0 +1,95 @@ + import 'package:flutter/material.dart'; + + class AppTheme { + // Primary Green Theme Colors + static const Color primaryGreen = Color(0xFF3B8C6E); + static const Color primaryGreenLight = Color(0xFF5FAF91); + static const Color primaryGreenDark = Color(0xFF266B50); + + // Accent Colors + static const Color accentOrange = Color(0xFFF5A623); + static const Color accentPurple = Color(0xFF9B51E0); + static const Color accentBlue = Color(0xFF4A90E2); + + // Neutral Colors + static const Color neutralWhite = Color(0xFFFFFFFF); + static const Color neutralGrey = Color(0xFFF5F5F5); + static const Color neutralDarkGrey = Color(0xFF9E9E9E); + static const Color neutralBlack = Color(0xFF333333); + + // Gradient for cards and backgrounds + static const Gradient greenGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [primaryGreenLight, primaryGreen], + ); + + static const Gradient accentGradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [accentOrange, Color(0xFFFA8E22)], + ); + + // Text Styles + static const TextStyle headingStyle = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: neutralBlack, + ); + + static const TextStyle subheadingStyle = TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: neutralBlack, + ); + + static const TextStyle bodyStyle = TextStyle( + fontSize: 14, + color: neutralBlack, + ); + + static const TextStyle smallStyle = TextStyle( + fontSize: 12, + color: neutralDarkGrey, + ); + + // Button Styles + static final ButtonStyle primaryButtonStyle = ElevatedButton.styleFrom( + backgroundColor: primaryGreen, + foregroundColor: neutralWhite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ); + + static final ButtonStyle secondaryButtonStyle = OutlinedButton.styleFrom( + foregroundColor: primaryGreen, + side: const BorderSide(color: primaryGreen), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ); + + // Card Decoration + static final BoxDecoration cardDecoration = BoxDecoration( + color: neutralWhite, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ); + + static final BoxDecoration gradientCardDecoration = BoxDecoration( + gradient: greenGradient, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ); + } \ No newline at end of file diff --git a/lib/debug_ssl_override.dart b/lib/debug_ssl_override.dart new file mode 100644 index 0000000..cf8fb52 --- /dev/null +++ b/lib/debug_ssl_override.dart @@ -0,0 +1,10 @@ +import 'dart:io'; + +class DebugHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? context) { + return super.createHttpClient(context) + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..b872fca --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get/get.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:ui/routes/app_pages.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/auth/bindings/auth_binding.dart'; +import 'debug_ssl_override.dart'; +import 'dart:io'; + +Future main() async { + HttpOverrides.global = DebugHttpOverrides(); + await initializeDateFormatting('id_ID', null).then((_) async { + WidgetsFlutterBinding.ensureInitialized(); + await dotenv.load(fileName: ".env"); + final prefs = await SharedPreferences.getInstance(); + Get.put(prefs, permanent: true); + runApp(const MyApp()); + }); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return GetMaterialApp( + debugShowCheckedModeBanner: false, + initialRoute: AppRoutes.splash, + getPages: AppPages.pages, + initialBinding: AuthBinding(), + theme: ThemeData( + textTheme: GoogleFonts.poppinsTextTheme(Theme.of(context).textTheme), + ), + ); + } +} diff --git a/lib/middlewares/auth_middleware.dart b/lib/middlewares/auth_middleware.dart new file mode 100644 index 0000000..59611b7 --- /dev/null +++ b/lib/middlewares/auth_middleware.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/routes/app_routes.dart'; + +class AuthMiddleware extends GetMiddleware { + @override + int? get priority => 1; + + @override + RouteSettings? redirect(String? route) { + final prefs = Get.find(); + final token = prefs.getString('token'); + + if (token == null || token.isEmpty) { + return const RouteSettings(name: AppRoutes.login); + } + return null; // Lanjutkan ke halaman yang diminta + } +} diff --git a/lib/models/detail_submit_tugas_siswa_mode.dart b/lib/models/detail_submit_tugas_siswa_mode.dart new file mode 100644 index 0000000..62158a9 --- /dev/null +++ b/lib/models/detail_submit_tugas_siswa_mode.dart @@ -0,0 +1,196 @@ +import 'dart:convert'; + +DetailSubmitTugasSiswaModel detailSubmitTugasSiswaModelFromJson(String str) => + DetailSubmitTugasSiswaModel.fromJson(json.decode(str)); + +String detailSubmitTugasSiswaModelToJson(DetailSubmitTugasSiswaModel data) => + json.encode(data.toJson()); + +class DetailSubmitTugasSiswaModel { + bool status; + String message; + List data; + + DetailSubmitTugasSiswaModel({ + required this.status, + required this.message, + required this.data, + }); + + factory DetailSubmitTugasSiswaModel.fromJson(Map json) => + DetailSubmitTugasSiswaModel( + status: json["status"], + message: json["message"], + data: List.from(json["data"].map((x) => Datum.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + int id; + int userId; + String nisn; + String nama; + String jk; + String kelas; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + SubmitTugas? submitTugas; + + Datum({ + required this.id, + required this.userId, + required this.nisn, + required this.nama, + required this.jk, + required this.kelas, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + required this.submitTugas, + }); + + factory Datum.fromJson(Map json) => Datum( + id: json["id"], + userId: json["user_id"], + nisn: json["nisn"], + nama: json["nama"], + jk: json["jk"], + kelas: json["kelas"], + tahunAjaran: json["tahun_ajaran"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + submitTugas: json["submit_tugas"] == null + ? null + : SubmitTugas.fromJson(json["submit_tugas"]), + ); + + Map toJson() => { + "id": id, + "user_id": userId, + "nisn": nisn, + "nama": nama, + "jk": jk, + "kelas": kelas, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "submit_tugas": submitTugas?.toJson(), + }; +} + +class SubmitTugas { + int id; + DateTime tanggal; + String nisn; + int tugasId; + String? text; + String? file; + int? nilai; + DateTime createdAt; + DateTime updatedAt; + Tugas tugas; + + SubmitTugas({ + required this.id, + required this.tanggal, + required this.nisn, + required this.tugasId, + required this.text, + required this.file, + this.nilai, + required this.createdAt, + required this.updatedAt, + required this.tugas, + }); + + factory SubmitTugas.fromJson(Map json) => SubmitTugas( + id: json["id"], + tanggal: DateTime.parse(json["tanggal"]), + nisn: json["nisn"], + tugasId: json["tugas_id"], + text: json["text"], + file: json["file"], + nilai: json["nilai"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + tugas: Tugas.fromJson(json["tugas"]), + ); + + Map toJson() => { + "id": id, + "tanggal": tanggal.toIso8601String(), + "nisn": nisn, + "tugas_id": tugasId, + "text": text, + "file": file, + "nilai": nilai, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "tugas": tugas.toJson(), + }; +} + +class Tugas { + int id; + DateTime tanggal; + DateTime tenggat; + String guruNip; + String nama; + int matapelajaranId; + String kelas; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + String? deskripsi; + + Tugas({ + required this.id, + required this.tanggal, + required this.tenggat, + required this.guruNip, + required this.nama, + required this.matapelajaranId, + required this.kelas, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + this.deskripsi, + }); + + factory Tugas.fromJson(Map json) => Tugas( + id: json["id"], + tanggal: DateTime.parse(json["tanggal"]), + tenggat: DateTime.parse(json["tenggat"]), + guruNip: json["guru_nip"], + nama: json["nama"], + matapelajaranId: json["matapelajaran_id"], + kelas: json["kelas"], + tahunAjaran: json["tahun_ajaran"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + deskripsi: json["deskripsi"], + ); + + Map toJson() => { + "id": id, + "tanggal": + "${tanggal.year.toString().padLeft(4, '0')}-${tanggal.month.toString().padLeft(2, '0')}-${tanggal.day.toString().padLeft(2, '0')}", + "tenggat": + "${tenggat.year.toString().padLeft(4, '0')}-${tenggat.month.toString().padLeft(2, '0')}-${tenggat.day.toString().padLeft(2, '0')}", + "guru_nip": guruNip, + "nama": nama, + "matapelajaran_id": matapelajaranId, + "kelas": kelas, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "deskripsi": deskripsi, + }; +} diff --git a/lib/models/kelas_model.dart b/lib/models/kelas_model.dart new file mode 100644 index 0000000..1abae3a --- /dev/null +++ b/lib/models/kelas_model.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; + +KelasModel kelasModelFromJson(String str) => + KelasModel.fromJson(json.decode(str)); + +String kelasModelToJson(KelasModel data) => json.encode(data.toJson()); + +class KelasModel { + bool status; + String message; + List data; + + KelasModel({ + required this.status, + required this.message, + required this.data, + }); + + factory KelasModel.fromJson(Map json) => KelasModel( + status: json["status"], + message: json["message"], + data: List.from(json["data"].map((x) => Datum.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + String nama; + DateTime createdAt; + DateTime updatedAt; + + Datum({ + required this.nama, + required this.createdAt, + required this.updatedAt, + }); + + factory Datum.fromJson(Map json) => Datum( + nama: json["nama"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + ); + + Map toJson() => { + "nama": nama, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/models/mata_pelajaran_model.dart b/lib/models/mata_pelajaran_model.dart new file mode 100644 index 0000000..8226f19 --- /dev/null +++ b/lib/models/mata_pelajaran_model.dart @@ -0,0 +1,190 @@ +// To parse this JSON data, do +// +// final mataPelajaranModel = mataPelajaranModelFromJson(jsonString); + +import 'dart:convert'; + +MataPelajaranModel mataPelajaranModelFromJson(String str) => + MataPelajaranModel.fromJson(json.decode(str)); + +String mataPelajaranModelToJson(MataPelajaranModel data) => + json.encode(data.toJson()); + +class MataPelajaranModel { + bool status; + String message; + List data; + + MataPelajaranModel({ + required this.status, + required this.message, + required this.data, + }); + + factory MataPelajaranModel.fromJson(Map json) => + MataPelajaranModel( + status: json["status"], + message: json["message"], + data: List.from(json["data"].map((x) => Datum.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + int id; + String nama; + String guruNip; + String kelas; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + Guru guru; + List materi; + int jumlahBuku; + int jumlahVideo; + + Datum({ + required this.id, + required this.nama, + required this.guruNip, + required this.kelas, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + required this.guru, + required this.materi, + required this.jumlahBuku, + required this.jumlahVideo, + }); + + factory Datum.fromJson(Map json) => Datum( + id: json["id"], + nama: json["nama"], + guruNip: json["guru_nip"], + kelas: json["kelas"], + tahunAjaran: json["tahun_ajaran"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + guru: Guru.fromJson(json["guru"]), + materi: + List.from(json["materi"].map((x) => Materi.fromJson(x))), + jumlahBuku: json["jumlah_buku"], + jumlahVideo: json["jumlah_video"], + ); + + Map toJson() => { + "id": id, + "nama": nama, + "guru_nip": guruNip, + "kelas": kelas, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "guru": guru.toJson(), + "materi": List.from(materi.map((x) => x.toJson())), + "jumlah_buku": jumlahBuku, + "jumlah_video": jumlahVideo, + }; +} + +class Guru { + int id; + int userId; + String nip; + String nama; + String jk; + DateTime createdAt; + DateTime updatedAt; + + Guru({ + required this.id, + required this.userId, + required this.nip, + required this.nama, + required this.jk, + required this.createdAt, + required this.updatedAt, + }); + + factory Guru.fromJson(Map json) => Guru( + id: json["id"], + userId: json["user_id"], + nip: json["nip"], + nama: json["nama"], + jk: json["jk"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + ); + + Map toJson() => { + "id": id, + "user_id": userId, + "nip": nip, + "nama": nama, + "jk": jk, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} + +class Materi { + int id; + DateTime tanggal; + int matapelajaranId; + String semester; + String type; + String judulMateri; + String deskripsi; + String path; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + + Materi({ + required this.id, + required this.tanggal, + required this.matapelajaranId, + required this.semester, + required this.type, + required this.judulMateri, + required this.deskripsi, + required this.path, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + }); + + factory Materi.fromJson(Map json) => Materi( + id: json["id"], + tanggal: DateTime.parse(json["tanggal"]), + matapelajaranId: json["matapelajaran_id"], + semester: json["semester"], + type: json["type"], + judulMateri: json["judul_materi"], + deskripsi: json["deskripsi"], + path: json["path"], + tahunAjaran: json["tahun_ajaran"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + ); + + Map toJson() => { + "id": id, + "tanggal": + "${tanggal.year.toString().padLeft(4, '0')}-${tanggal.month.toString().padLeft(2, '0')}-${tanggal.day.toString().padLeft(2, '0')}", + "matapelajaran_id": matapelajaranId, + "semester": semester, + "type": type, + "judul_materi": judulMateri, + "deskripsi": deskripsi, + "path": path, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/models/mata_pelajaran_simple_model.dart b/lib/models/mata_pelajaran_simple_model.dart new file mode 100644 index 0000000..8a4b067 --- /dev/null +++ b/lib/models/mata_pelajaran_simple_model.dart @@ -0,0 +1,116 @@ +import 'dart:convert'; + +MataPelajaranSimpleModel mataPelajaranSimpleModelFromJson(String str) => + MataPelajaranSimpleModel.fromJson(json.decode(str)); + +String mataPelajaranSimpleModelToJson(MataPelajaranSimpleModel data) => + json.encode(data.toJson()); + +class MataPelajaranSimpleModel { + bool status; + String message; + List data; + + MataPelajaranSimpleModel({ + required this.status, + required this.message, + required this.data, + }); + + factory MataPelajaranSimpleModel.fromJson(Map json) => + MataPelajaranSimpleModel( + status: json["status"], + message: json["message"], + data: List.from(json["data"].map((x) => Datum.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + int id; + String nama; + String guruNip; + String kelas; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + Guru guru; + + Datum({ + required this.id, + required this.nama, + required this.guruNip, + required this.kelas, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + required this.guru, + }); + + factory Datum.fromJson(Map json) => Datum( + id: json["id"], + nama: json["nama"], + guruNip: json["guru_nip"], + kelas: json["kelas"], + tahunAjaran: json["tahun_ajaran"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + guru: Guru.fromJson(json["guru"]), + ); + + Map toJson() => { + "id": id, + "nama": nama, + "guru_nip": guruNip, + "kelas": kelas, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "guru": guru.toJson(), + }; +} + +class Guru { + int id; + int userId; + String nip; + String nama; + String jk; + DateTime createdAt; + DateTime updatedAt; + + Guru({ + required this.id, + required this.userId, + required this.nip, + required this.nama, + required this.jk, + required this.createdAt, + required this.updatedAt, + }); + + factory Guru.fromJson(Map json) => Guru( + id: json["id"], + userId: json["user_id"], + nip: json["nip"], + nama: json["nama"], + jk: json["jk"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + ); + + Map toJson() => { + "id": id, + "user_id": userId, + "nip": nip, + "nama": nama, + "jk": jk, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/models/materi_buku_model.dart b/lib/models/materi_buku_model.dart new file mode 100644 index 0000000..a6804ce --- /dev/null +++ b/lib/models/materi_buku_model.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; + +MateriBukuModel materiBukuModelFromJson(String str) => + MateriBukuModel.fromJson(json.decode(str)); + +String materiBukuModelToJson(MateriBukuModel data) => + json.encode(data.toJson()); + +class MateriBukuModel { + bool status; + String message; + List data; + + MateriBukuModel({ + required this.status, + required this.message, + required this.data, + }); + + factory MateriBukuModel.fromJson(Map json) => + MateriBukuModel( + status: json["status"], + message: json["message"], + data: List.from(json["data"].map((x) => Datum.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + int id; + DateTime tanggal; + int matapelajaranId; + String semester; + String type; + String judulMateri; + String deskripsi; + String path; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + MataPelajaran mataPelajaran; + + Datum({ + required this.id, + required this.tanggal, + required this.matapelajaranId, + required this.semester, + required this.type, + required this.judulMateri, + required this.deskripsi, + required this.path, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + required this.mataPelajaran, + }); + + factory Datum.fromJson(Map json) => Datum( + id: json["id"], + tanggal: DateTime.parse(json["tanggal"]), + matapelajaranId: json["matapelajaran_id"], + semester: json["semester"], + type: json["type"], + judulMateri: json["judul_materi"], + deskripsi: json["deskripsi"], + path: json["path"], + tahunAjaran: json["tahun_ajaran"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + mataPelajaran: MataPelajaran.fromJson(json["mata_pelajaran"]), + ); + + Map toJson() => { + "id": id, + "tanggal": + "${tanggal.year.toString().padLeft(4, '0')}-${tanggal.month.toString().padLeft(2, '0')}-${tanggal.day.toString().padLeft(2, '0')}", + "matapelajaran_id": matapelajaranId, + "semester": semester, + "type": type, + "judul_materi": judulMateri, + "deskripsi": deskripsi, + "path": path, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "mata_pelajaran": mataPelajaran.toJson(), + }; +} + +class MataPelajaran { + int id; + String nama; + String guruNip; + String kelas; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + + MataPelajaran({ + required this.id, + required this.nama, + required this.guruNip, + required this.kelas, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + }); + + factory MataPelajaran.fromJson(Map json) => MataPelajaran( + id: json["id"], + nama: json["nama"], + guruNip: json["guru_nip"], + kelas: json["kelas"], + tahunAjaran: json["tahun_ajaran"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + ); + + Map toJson() => { + "id": id, + "nama": nama, + "guru_nip": guruNip, + "kelas": kelas, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/models/materi_video_model.dart b/lib/models/materi_video_model.dart new file mode 100644 index 0000000..8989636 --- /dev/null +++ b/lib/models/materi_video_model.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; + +MateriVideoModel materiVideoModelFromJson(String str) => + MateriVideoModel.fromJson(json.decode(str)); + +String materiVideoModelToJson(MateriVideoModel data) => + json.encode(data.toJson()); + +class MateriVideoModel { + bool status; + String message; + List data; + + MateriVideoModel({ + required this.status, + required this.message, + required this.data, + }); + + factory MateriVideoModel.fromJson(Map json) => + MateriVideoModel( + status: json["status"], + message: json["message"], + data: List.from(json["data"].map((x) => Datum.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + int id; + DateTime tanggal; + int matapelajaranId; + String semester; + String type; + String judulMateri; + String deskripsi; + String path; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + MataPelajaran mataPelajaran; + + Datum({ + required this.id, + required this.tanggal, + required this.matapelajaranId, + required this.semester, + required this.type, + required this.judulMateri, + required this.deskripsi, + required this.path, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + required this.mataPelajaran, + }); + + factory Datum.fromJson(Map json) => Datum( + id: json["id"], + tanggal: DateTime.parse(json["tanggal"]), + matapelajaranId: json["matapelajaran_id"], + semester: json["semester"], + type: json["type"], + judulMateri: json["judul_materi"], + deskripsi: json["deskripsi"], + path: json["path"], + tahunAjaran: json["tahun_ajaran"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + mataPelajaran: MataPelajaran.fromJson(json["mata_pelajaran"]), + ); + + Map toJson() => { + "id": id, + "tanggal": + "${tanggal.year.toString().padLeft(4, '0')}-${tanggal.month.toString().padLeft(2, '0')}-${tanggal.day.toString().padLeft(2, '0')}", + "matapelajaran_id": matapelajaranId, + "semester": semester, + "type": type, + "judul_materi": judulMateri, + "deskripsi": deskripsi, + "path": path, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "mata_pelajaran": mataPelajaran.toJson(), + }; +} + +class MataPelajaran { + int id; + String nama; + String guruNip; + String kelas; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + + MataPelajaran({ + required this.id, + required this.nama, + required this.guruNip, + required this.kelas, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + }); + + factory MataPelajaran.fromJson(Map json) => MataPelajaran( + id: json["id"], + nama: json["nama"], + guruNip: json["guru_nip"], + kelas: json["kelas"], + tahunAjaran: json["tahun_ajaran"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + ); + + Map toJson() => { + "id": id, + "nama": nama, + "guru_nip": guruNip, + "kelas": kelas, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/models/quiz_answer_model.dart b/lib/models/quiz_answer_model.dart new file mode 100644 index 0000000..22484f2 --- /dev/null +++ b/lib/models/quiz_answer_model.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +QuizAnswerModel quizAnswerModelFromJson(String str) => + QuizAnswerModel.fromJson(json.decode(str)); + +String quizAnswerModelToJson(QuizAnswerModel data) => + json.encode(data.toJson()); + +class QuizAnswerModel { + bool status; + String message; + Data data; + + QuizAnswerModel({ + required this.status, + required this.message, + required this.data, + }); + + factory QuizAnswerModel.fromJson(Map json) => + QuizAnswerModel( + status: json["status"], + message: json["message"], + data: Data.fromJson(json["data"]), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": data.toJson(), + }; +} + +class Data { + int quizId; + int correct; + int fase; + int newLevel; + int skorSementara; + bool selesai; + int? waktuTersisa; + + Data({ + required this.quizId, + required this.correct, + required this.fase, + required this.newLevel, + required this.skorSementara, + required this.selesai, + this.waktuTersisa, + }); + + factory Data.fromJson(Map json) => Data( + quizId: json["quiz_id"] is int + ? json["quiz_id"] + : int.tryParse(json["quiz_id"].toString()) ?? 0, + correct: json["correct"] is int + ? json["correct"] + : int.tryParse(json["correct"].toString()) ?? 0, + fase: json["fase"] is int + ? json["fase"] + : int.tryParse(json["fase"].toString()) ?? 1, + newLevel: json["new_level"] is int + ? json["new_level"] + : int.tryParse(json["new_level"].toString()) ?? 1, + skorSementara: json["skor_sementara"] is int + ? json["skor_sementara"] + : int.tryParse(json["skor_sementara"].toString()) ?? 0, + selesai: json["selesai"] is bool + ? json["selesai"] + : json["selesai"] == 1 || + json["selesai"] == "1" || + json["selesai"] == true, + waktuTersisa: json["waktu_tersisa"] is int + ? json["waktu_tersisa"] + : int.tryParse(json["waktu_tersisa"].toString()), + ); + + Map toJson() => { + "quiz_id": quizId, + "correct": correct, + "fase": fase, + "new_level": newLevel, + "skor_sementara": skorSementara, + "selesai": selesai, + "waktu_tersisa": waktuTersisa, + }; +} diff --git a/lib/models/quiz_attempt_model.dart b/lib/models/quiz_attempt_model.dart new file mode 100644 index 0000000..eece389 --- /dev/null +++ b/lib/models/quiz_attempt_model.dart @@ -0,0 +1,151 @@ +import 'dart:convert'; + +QuizAttemptModel quizAttemptModelFromJson(String str) => + QuizAttemptModel.fromJson(json.decode(str)); + +String quizAttemptModelToJson(QuizAttemptModel data) => + json.encode(data.toJson()); + +class QuizAttemptModel { + bool status; + String message; + Data data; + + QuizAttemptModel({ + required this.status, + required this.message, + required this.data, + }); + + factory QuizAttemptModel.fromJson(Map json) => + QuizAttemptModel( + status: json["status"], + message: json["message"], + data: Data.fromJson(json["data"]), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": data.toJson(), + }; +} + +class Data { + int id; + int quizId; + String nisn; + String skor; + int levelAkhir; + int jumlahSoalDijawab; + int fase; + String benar; + DateTime? waktuMulai; + DateTime? waktuSelesai; + DateTime createdAt; + DateTime updatedAt; + String jumlahSoal; + String jawabanBenar; + String jawabanSalah; + + Data({ + required this.id, + required this.quizId, + required this.nisn, + required this.skor, + required this.levelAkhir, + required this.jumlahSoalDijawab, + required this.fase, + required this.benar, + this.waktuMulai, + this.waktuSelesai, + required this.createdAt, + required this.updatedAt, + required this.jumlahSoal, + required this.jawabanBenar, + required this.jawabanSalah, + }); + + factory Data.fromJson(Map json) => Data( + id: json["id"] is int + ? json["id"] + : int.tryParse(json["id"].toString()) ?? 0, + quizId: json["quiz_id"] is int + ? json["quiz_id"] + : int.tryParse(json["quiz_id"].toString()) ?? 0, + nisn: json["nisn"]?.toString() ?? "", + skor: json["skor"]?.toString() ?? "0", + levelAkhir: json["level_akhir"] is int + ? json["level_akhir"] + : int.tryParse(json["level_akhir"].toString()) ?? 1, + jumlahSoalDijawab: json["jumlah_soal_dijawab"] is int + ? json["jumlah_soal_dijawab"] + : int.tryParse(json["jumlah_soal_dijawab"].toString()) ?? 0, + fase: json["fase"] is int + ? json["fase"] + : int.tryParse(json["fase"].toString()) ?? 1, + benar: json["benar"]?.toString() ?? "{}", + waktuMulai: json["waktu_mulai"] != null + ? DateTime.tryParse(json["waktu_mulai"].toString()) + : null, + waktuSelesai: json["waktu_selesai"] != null + ? DateTime.tryParse(json["waktu_selesai"].toString()) + : null, + createdAt: json["created_at"] != null + ? DateTime.tryParse(json["created_at"].toString()) ?? DateTime.now() + : DateTime.now(), + updatedAt: json["updated_at"] != null + ? DateTime.tryParse(json["updated_at"].toString()) ?? DateTime.now() + : DateTime.now(), + jumlahSoal: json["jumlah_soal"]?.toString() ?? "0", + jawabanBenar: json["jawaban_benar"]?.toString() ?? "0", + jawabanSalah: json["jawaban_salah"]?.toString() ?? "0", + ); + + Map toJson() => { + "id": id, + "quiz_id": quizId, + "nisn": nisn, + "skor": skor, + "level_akhir": levelAkhir, + "jumlah_soal_dijawab": jumlahSoalDijawab, + "fase": fase, + "benar": benar, + "waktu_mulai": waktuMulai?.toIso8601String(), + "waktu_selesai": waktuSelesai?.toIso8601String(), + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "jumlah_soal": jumlahSoal, + "jawaban_benar": jawabanBenar, + "jawaban_salah": jawabanSalah, + }; + + // Method untuk menghitung skor berdasarkan jawaban benar dan jumlah soal + String get calculatedSkor { + try { + int benar = int.tryParse(jawabanBenar) ?? 0; + int total = int.tryParse(jumlahSoal) ?? 0; + if (total > 0) { + return "$benar/$total"; + } + return "0/0"; + } catch (e) { + return "0/0"; + } + } + + // Method untuk mendapatkan jawaban benar sebagai integer + int get jawabanBenarInt { + return int.tryParse(jawabanBenar) ?? 0; + } + + // Method untuk mendapatkan jawaban salah sebagai integer + int get jawabanSalahInt { + return int.tryParse(jawabanSalah) ?? 0; + } + + // Method untuk mendapatkan jumlah soal sebagai integer + int get jumlahSoalInt { + return int.tryParse(jumlahSoal) ?? 0; + } +} diff --git a/lib/models/quiz_guru_model.dart b/lib/models/quiz_guru_model.dart new file mode 100644 index 0000000..af78fe8 --- /dev/null +++ b/lib/models/quiz_guru_model.dart @@ -0,0 +1,78 @@ +// To parse this JSON data, do +// +// final quizGuruModel = quizGuruModelFromJson(jsonString); + +import 'dart:convert'; + +QuizGuruModel quizGuruModelFromJson(String str) => + QuizGuruModel.fromJson(json.decode(str)); + +String quizGuruModelToJson(QuizGuruModel data) => json.encode(data.toJson()); + +class QuizGuruModel { + bool status; + String message; + List data; + + QuizGuruModel({ + required this.status, + required this.message, + required this.data, + }); + + factory QuizGuruModel.fromJson(Map json) => QuizGuruModel( + status: json["status"], + message: json["message"], + data: List.from(json["data"].map((x) => Datum.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + int id; + String judul; + String deskripsi; + int totalSoal; + String totalSoalTampil; + int matapelajaranId; + DateTime createdAt; + DateTime updatedAt; + + Datum({ + required this.id, + required this.judul, + required this.deskripsi, + required this.totalSoal, + required this.totalSoalTampil, + required this.matapelajaranId, + required this.createdAt, + required this.updatedAt, + }); + + factory Datum.fromJson(Map json) => Datum( + id: json["id"], + judul: json["judul"], + deskripsi: json["deskripsi"], + totalSoal: json["total_soal"], + totalSoalTampil: json["total_soal_tampil"], + matapelajaranId: json["matapelajaran_id"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + ); + + Map toJson() => { + "id": id, + "judul": judul, + "deskripsi": deskripsi, + "total_soal": totalSoal, + "total_soal_tampil": totalSoalTampil, + "matapelajaran_id": matapelajaranId, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/models/quiz_mode.dart b/lib/models/quiz_mode.dart new file mode 100644 index 0000000..862cfa2 --- /dev/null +++ b/lib/models/quiz_mode.dart @@ -0,0 +1,186 @@ +// To parse this JSON data, do +// +// final quizModel = quizModelFromJson(jsonString); + +import 'dart:convert'; + +QuizModel quizModelFromJson(String str) => QuizModel.fromJson(json.decode(str)); + +String quizModelToJson(QuizModel data) => json.encode(data.toJson()); + +class QuizModel { + bool status; + String message; + List data; + + QuizModel({ + required this.status, + required this.message, + required this.data, + }); + + factory QuizModel.fromJson(Map json) => QuizModel( + status: json["status"], + message: json["message"], + data: List.from(json["data"].map((x) => Datum.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + int id; + String judul; + String deskripsi; + int totalSoal; + String totalSoalTampil; + int? waktu; + int matapelajaranId; + DateTime createdAt; + DateTime updatedAt; + QuizAttempt? quizAttempt; + + Datum({ + required this.id, + required this.judul, + required this.deskripsi, + required this.totalSoal, + required this.totalSoalTampil, + this.waktu, + required this.matapelajaranId, + required this.createdAt, + required this.updatedAt, + required this.quizAttempt, + }); + + factory Datum.fromJson(Map json) => Datum( + id: json["id"] is int + ? json["id"] + : int.tryParse(json["id"].toString()) ?? 0, + judul: json["judul"]?.toString() ?? "", + deskripsi: json["deskripsi"]?.toString() ?? "", + totalSoal: json["total_soal"] is int + ? json["total_soal"] + : int.tryParse(json["total_soal"].toString()) ?? 0, + totalSoalTampil: json["total_soal_tampil"]?.toString() ?? "0", + waktu: json["waktu"] is int + ? (json["waktu"] > 0 && json["waktu"] <= 1440 + ? json["waktu"] + : null) + : (json["waktu"] == null + ? null + : (() { + int? parsed = int.tryParse(json["waktu"].toString()); + return (parsed != null && parsed > 0 && parsed <= 1440) + ? parsed + : null; + })()), + matapelajaranId: json["matapelajaran_id"] is int + ? json["matapelajaran_id"] + : int.tryParse(json["matapelajaran_id"].toString()) ?? 0, + createdAt: json["created_at"] != null + ? DateTime.tryParse(json["created_at"].toString()) ?? DateTime.now() + : DateTime.now(), + updatedAt: json["updated_at"] != null + ? DateTime.tryParse(json["updated_at"].toString()) ?? DateTime.now() + : DateTime.now(), + quizAttempt: json["quiz_attempt"] == null + ? null + : QuizAttempt.fromJson(json["quiz_attempt"]), + ); + + Map toJson() => { + "id": id, + "judul": judul, + "deskripsi": deskripsi, + "total_soal": totalSoal, + "total_soal_tampil": totalSoalTampil, + "waktu": waktu, + "matapelajaran_id": matapelajaranId, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "quiz_attempt": quizAttempt?.toJson(), + }; +} + +class QuizAttempt { + int id; + int quizId; + String nisn; + String skor; + int levelAkhir; + int jumlahSoalDijawab; + int fase; + String benar; + DateTime? waktuMulai; + DateTime? waktuSelesai; + DateTime createdAt; + DateTime updatedAt; + + QuizAttempt({ + required this.id, + required this.quizId, + required this.nisn, + required this.skor, + required this.levelAkhir, + required this.jumlahSoalDijawab, + required this.fase, + required this.benar, + this.waktuMulai, + this.waktuSelesai, + required this.createdAt, + required this.updatedAt, + }); + + factory QuizAttempt.fromJson(Map json) => QuizAttempt( + id: json["id"] is int + ? json["id"] + : int.tryParse(json["id"].toString()) ?? 0, + quizId: json["quiz_id"] is int + ? json["quiz_id"] + : int.tryParse(json["quiz_id"].toString()) ?? 0, + nisn: json["nisn"]?.toString() ?? "", + skor: json["skor"]?.toString() ?? "0", + levelAkhir: json["level_akhir"] is int + ? json["level_akhir"] + : int.tryParse(json["level_akhir"].toString()) ?? 1, + jumlahSoalDijawab: json["jumlah_soal_dijawab"] is int + ? json["jumlah_soal_dijawab"] + : int.tryParse(json["jumlah_soal_dijawab"].toString()) ?? 0, + fase: json["fase"] is int + ? json["fase"] + : int.tryParse(json["fase"].toString()) ?? 1, + benar: json["benar"]?.toString() ?? "{}", + waktuMulai: json["waktu_mulai"] != null + ? DateTime.tryParse(json["waktu_mulai"].toString()) + : null, + waktuSelesai: json["waktu_selesai"] != null + ? DateTime.tryParse(json["waktu_selesai"].toString()) + : null, + createdAt: json["created_at"] != null + ? DateTime.tryParse(json["created_at"].toString()) ?? DateTime.now() + : DateTime.now(), + updatedAt: json["updated_at"] != null + ? DateTime.tryParse(json["updated_at"].toString()) ?? DateTime.now() + : DateTime.now(), + ); + + Map toJson() => { + "id": id, + "quiz_id": quizId, + "nisn": nisn, + "skor": skor, + "level_akhir": levelAkhir, + "jumlah_soal_dijawab": jumlahSoalDijawab, + "fase": fase, + "benar": benar, + "waktu_mulai": waktuMulai?.toIso8601String(), + "waktu_selesai": waktuSelesai?.toIso8601String(), + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/models/quiz_question_model.dart b/lib/models/quiz_question_model.dart new file mode 100644 index 0000000..ab06221 --- /dev/null +++ b/lib/models/quiz_question_model.dart @@ -0,0 +1,110 @@ +import 'dart:convert'; + +QuizQuestionModel quizQuestionModelFromJson(String str) => + QuizQuestionModel.fromJson(json.decode(str)); + +String quizQuestionModelToJson(QuizQuestionModel data) => + json.encode(data.toJson()); + +class QuizQuestionModel { + bool status; + String message; + Data? data; + + QuizQuestionModel({ + required this.status, + required this.message, + this.data, + }); + + factory QuizQuestionModel.fromJson(Map json) => + QuizQuestionModel( + status: json["status"] ?? false, + message: json["message"] ?? "", + data: json["data"] == null ? null : Data.fromJson(json["data"]), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": data?.toJson(), + }; +} + +class Data { + int id; + int quizId; + String pertanyaan; + String opsiA; + String opsiB; + String opsiC; + String opsiD; + String jawabanBenar; + int level; + int? waktuTersisa; + DateTime? waktuMulai; + DateTime createdAt; + DateTime updatedAt; + + Data({ + required this.id, + required this.quizId, + required this.pertanyaan, + required this.opsiA, + required this.opsiB, + required this.opsiC, + required this.opsiD, + required this.jawabanBenar, + required this.level, + this.waktuTersisa, + this.waktuMulai, + required this.createdAt, + required this.updatedAt, + }); + + factory Data.fromJson(Map json) => Data( + id: json["id"] is int + ? json["id"] + : int.tryParse(json["id"].toString()) ?? 0, + quizId: json["quiz_id"] is int + ? json["quiz_id"] + : int.tryParse(json["quiz_id"].toString()) ?? 0, + pertanyaan: json["pertanyaan"]?.toString() ?? "", + opsiA: json["opsi_a"]?.toString() ?? "", + opsiB: json["opsi_b"]?.toString() ?? "", + opsiC: json["opsi_c"]?.toString() ?? "", + opsiD: json["opsi_d"]?.toString() ?? "", + jawabanBenar: json["jawaban_benar"]?.toString() ?? "", + level: json["level"] is int + ? json["level"] + : int.tryParse(json["level"].toString()) ?? 0, + waktuTersisa: json["waktu_tersisa"] == null + ? null + : int.tryParse(json["waktu_tersisa"].toString()), + waktuMulai: json["waktu_mulai"] != null + ? DateTime.tryParse(json["waktu_mulai"].toString()) + : null, + createdAt: json["created_at"] != null + ? DateTime.tryParse(json["created_at"].toString()) ?? DateTime.now() + : DateTime.now(), + updatedAt: json["updated_at"] != null + ? DateTime.tryParse(json["updated_at"].toString()) ?? DateTime.now() + : DateTime.now(), + ); + + Map toJson() => { + "id": id, + "quiz_id": quizId, + "pertanyaan": pertanyaan, + "opsi_a": opsiA, + "opsi_b": opsiB, + "opsi_c": opsiC, + "opsi_d": opsiD, + "jawaban_benar": jawabanBenar, + "level": level, + "waktu_tersisa": waktuTersisa, + "waktu_mulai": waktuMulai?.toIso8601String(), + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/models/tahun_ajaran.dart b/lib/models/tahun_ajaran.dart new file mode 100644 index 0000000..77c9dd4 --- /dev/null +++ b/lib/models/tahun_ajaran.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +TahunAjaranModel tahunAjaranModelFromJson(String str) => + TahunAjaranModel.fromJson(json.decode(str)); + +String tahunAjaranModelToJson(TahunAjaranModel data) => + json.encode(data.toJson()); + +class TahunAjaranModel { + bool status; + String message; + List data; + + TahunAjaranModel({ + required this.status, + required this.message, + required this.data, + }); + + factory TahunAjaranModel.fromJson(Map json) => + TahunAjaranModel( + status: json["status"], + message: json["message"], + data: List.from(json["data"].map((x) => Datum.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + String tahun; + String status; + DateTime createdAt; + DateTime updatedAt; + + Datum({ + required this.tahun, + required this.status, + required this.createdAt, + required this.updatedAt, + }); + + factory Datum.fromJson(Map json) => Datum( + tahun: json["tahun"], + status: json["status"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + ); + + Map toJson() => { + "tahun": tahun, + "status": status, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/models/tugas_model.dart b/lib/models/tugas_model.dart new file mode 100644 index 0000000..1c094df --- /dev/null +++ b/lib/models/tugas_model.dart @@ -0,0 +1,272 @@ +// To parse this JSON data, do +// +// final tugasModel = tugasModelFromJson(jsonString); + +import 'dart:convert'; +import 'dart:developer'; + +TugasModel tugasModelFromJson(String str) => + TugasModel.fromJson(json.decode(str)); + +String tugasModelToJson(TugasModel data) => json.encode(data.toJson()); + +class TugasModel { + bool status; + String message; + List data; + + TugasModel({ + required this.status, + required this.message, + required this.data, + }); + + factory TugasModel.fromJson(Map json) { + try { + return TugasModel( + status: json["status"] ?? false, + message: json["message"] ?? "No message", + data: json["data"] != null + ? List.from(json["data"].map((x) => Datum.fromJson(x))) + : [], + ); + } catch (e) { + throw Exception("Error parsing TugasModel: $e"); + } + } + + Map toJson() => { + "status": status, + "message": message, + "data": List.from(data.map((x) => x.toJson())), + }; +} + +class Datum { + int id; + DateTime tanggal; + DateTime tenggat; + String guruNip; + String nama; + int matapelajaranId; + String kelas; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + MataPelajaran mataPelajaran; + SubmitTugas? submitTugas; + String? deskripsi; + + Datum({ + required this.id, + required this.tanggal, + required this.tenggat, + required this.guruNip, + required this.nama, + required this.matapelajaranId, + required this.kelas, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + required this.mataPelajaran, + this.submitTugas, + this.deskripsi, + }); + + factory Datum.fromJson(Map json) { + try { + log("Parsing Datum with id: " + json['id'].toString()); + log("submit_tugas type: " + + (json['submit_tugas']?.runtimeType.toString() ?? 'null')); + log("submit_tugas value: " + json['submit_tugas'].toString()); + + SubmitTugas? submitTugas; + if (json["submit_tugas"] == null) { + submitTugas = null; + log("submit_tugas is null"); + } else if (json["submit_tugas"] is List) { + var submitList = json["submit_tugas"] as List; + log("submit_tugas is List with length: " + + submitList.length.toString()); + if (submitList.isNotEmpty) { + submitTugas = SubmitTugas.fromJson(submitList[0]); + log("Created SubmitTugas from first item in list"); + } else { + submitTugas = null; + log("submit_tugas list is empty"); + } + } else if (json["submit_tugas"] is Map) { + submitTugas = SubmitTugas.fromJson(json["submit_tugas"]); + log("Created SubmitTugas from Map"); + } else { + submitTugas = null; + log("submit_tugas is neither List nor Map, type: " + + json['submit_tugas'].runtimeType.toString()); + } + + return Datum( + id: json["id"] ?? 0, + tanggal: json["tanggal"] != null + ? DateTime.parse(json["tanggal"].toString()) + : DateTime.now(), + tenggat: json["tenggat"] != null + ? DateTime.parse(json["tenggat"].toString()) + : DateTime.now(), + guruNip: json["guru_nip"]?.toString() ?? "", + nama: json["nama"]?.toString() ?? "", + matapelajaranId: json["matapelajaran_id"] ?? 0, + kelas: json["kelas"]?.toString() ?? "", + tahunAjaran: json["tahun_ajaran"]?.toString() ?? "", + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"].toString()) + : DateTime.now(), + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"].toString()) + : DateTime.now(), + mataPelajaran: json["mata_pelajaran"] != null + ? MataPelajaran.fromJson(json["mata_pelajaran"]) + : MataPelajaran( + id: 0, + nama: "", + guruNip: "", + kelas: "", + tahunAjaran: "", + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + submitTugas: submitTugas, + deskripsi: + json["deskripsi"] != null ? json["deskripsi"].toString() : null, + ); + + // Log hasil parsing deskripsi + log("Final deskripsi value: ${json["deskripsi"] != null ? json["deskripsi"].toString() : null}"); + } catch (e) { + log("Error parsing Datum: $e"); + throw Exception("Error parsing Datum: $e"); + } + } + + Map toJson() => { + "id": id, + "tanggal": tanggal.toIso8601String(), + "tenggat": tenggat.toIso8601String(), + "guru_nip": guruNip, + "nama": nama, + "matapelajaran_id": matapelajaranId, + "kelas": kelas, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "mata_pelajaran": mataPelajaran.toJson(), + "submit_tugas": submitTugas?.toJson(), + "deskripsi": deskripsi, + }; +} + +class MataPelajaran { + int id; + String nama; + String guruNip; + String kelas; + String tahunAjaran; + DateTime createdAt; + DateTime updatedAt; + + MataPelajaran({ + required this.id, + required this.nama, + required this.guruNip, + required this.kelas, + required this.tahunAjaran, + required this.createdAt, + required this.updatedAt, + }); + + factory MataPelajaran.fromJson(Map json) { + try { + return MataPelajaran( + id: json["id"] ?? 0, + nama: json["nama"]?.toString() ?? "", + guruNip: json["guru_nip"]?.toString() ?? "", + kelas: json["kelas"]?.toString() ?? "", + tahunAjaran: json["tahun_ajaran"]?.toString() ?? "", + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"].toString()) + : DateTime.now(), + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"].toString()) + : DateTime.now(), + ); + } catch (e) { + throw Exception("Error parsing MataPelajaran: $e"); + } + } + + Map toJson() => { + "id": id, + "nama": nama, + "guru_nip": guruNip, + "kelas": kelas, + "tahun_ajaran": tahunAjaran, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} + +class SubmitTugas { + int id; + DateTime tanggal; + String nisn; + int tugasId; + String? text; + String? file; + DateTime createdAt; + DateTime updatedAt; + + SubmitTugas({ + required this.id, + required this.tanggal, + required this.nisn, + required this.tugasId, + this.text, + this.file, + required this.createdAt, + required this.updatedAt, + }); + + factory SubmitTugas.fromJson(Map json) { + try { + return SubmitTugas( + id: json["id"] ?? 0, + tanggal: json["tanggal"] != null + ? DateTime.parse(json["tanggal"].toString()) + : DateTime.now(), + nisn: json["nisn"] ?? "", + tugasId: json["tugas_id"] ?? 0, + text: json["text"], + file: json["file"], + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"].toString()) + : DateTime.now(), + updatedAt: json["updated_at"] != null + ? DateTime.parse(json["updated_at"].toString()) + : DateTime.now(), + ); + } catch (e) { + throw Exception("Error parsing SubmitTugas: $e"); + } + } + + Map toJson() => { + "id": id, + "tanggal": + "${tanggal.year.toString().padLeft(4, '0')}-${tanggal.month.toString().padLeft(2, '0')}-${tanggal.day.toString().padLeft(2, '0')}", + "nisn": nisn, + "tugas_id": tugasId, + "text": text, + "file": file, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + }; +} diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart new file mode 100644 index 0000000..2514e7f --- /dev/null +++ b/lib/routes/app_pages.dart @@ -0,0 +1,186 @@ +import 'package:ui/views/guru/dashboard/bindings/dashboard_binding.dart'; +import 'package:ui/views/guru/mata_pelajaran/bindings/matpel_guru_binding.dart'; +import 'package:ui/views/guru/mata_pelajaran/index.dart'; +import 'package:ui/views/guru/profiles/index.dart'; +import 'package:ui/views/guru/quiz/bindings/matpel_quiz_guru_binding.dart'; +import 'package:ui/views/guru/quiz/bindings/quiz_detail_guru_binding.dart'; +import 'package:ui/views/guru/quiz/bindings/quiz_guru_binding.dart'; +import 'package:ui/views/guru/quiz/index.dart'; +import 'package:ui/views/guru/quiz/quiz.dart'; +import 'package:ui/views/guru/quiz/quiz_detail.dart'; +import 'package:ui/views/guru/tugas/bindings/detail_submit_tugas_siswa_binding.dart'; +import 'package:ui/views/guru/tugas/bindings/tugas_detail_guru_binding.dart'; +import 'package:ui/views/guru/tugas/bindings/tugas_guru_binding.dart'; +import 'package:ui/views/guru/tugas/detail.dart'; +import 'package:ui/views/guru/tugas/detail_submit_tugas.dart'; +import 'package:ui/views/guru/tugas/index.dart'; +import 'package:ui/views/guru/tugas/review_submit_tugas.dart'; +import 'package:ui/views/siswa/bindings/notifikasi_binding.dart'; +import 'package:ui/views/siswa/bindings/ubah_password_binding.dart'; +import 'package:ui/views/siswa/profile.dart'; +import 'package:ui/views/siswa/quiz/bindings/matpel_quiz_binding.dart'; +import 'package:ui/views/siswa/quiz/bindings/quiz_binding.dart'; +import 'package:ui/views/siswa/quiz/bindings/quiz_finish_binding.dart'; +import 'package:ui/views/siswa/quiz/bindings/soal_quiz_binding.dart'; +import 'package:ui/views/siswa/quiz/matpel_quiz.dart'; +import 'package:ui/views/siswa/quiz/matpel_quiz_detail.dart'; +import 'package:ui/views/siswa/quiz/soal_quiz.dart'; +import 'package:ui/views/siswa/quiz/soal_quiz_selesai.dart'; +import 'package:ui/views/siswa/ranking/bindings/matpel_rank_binding.dart'; +import 'package:ui/views/siswa/ranking/bindings/quiz_rank_binding.dart'; +import 'package:ui/views/siswa/ranking/bindings/ranking_binding.dart'; +import 'package:ui/views/siswa/ranking/index.dart'; +import 'package:ui/views/siswa/ranking/matpel_rank.dart'; +import 'package:ui/views/siswa/ranking/matpel_rank_detail.dart'; +import 'package:ui/views/siswa/ubah_password.dart'; + +import 'app_routes.dart'; +import 'export.dart'; + +class AppPages { + static final pages = [ + GetPage(name: AppRoutes.splash, page: () => const SplashScreen()), + GetPage(name: AppRoutes.welcome, page: () => const WelcomePage()), + GetPage(name: AppRoutes.selection, page: () => const SelectionPage()), + GetPage( + name: AppRoutes.login, + page: () => const LoginPage(), + binding: AuthBinding()), + GetPage( + name: AppRoutes.siswaDashboard, + page: () => const SiswaDashboardPage(), + middlewares: [AuthMiddleware()], + binding: SiswaBinding(), + ), + GetPage( + name: AppRoutes.notifikasiSiswa, + page: () => NotifSiswa(), + middlewares: [AuthMiddleware()], + binding: NotifikasiBinding(), + ), + GetPage( + name: AppRoutes.guruDashboard, + page: () => GuruDashboardPage(), + middlewares: [AuthMiddleware()], + binding: DashboardBinding(), + ), + GetPage( + name: AppRoutes.kelasmatapelajarans, + page: () => KelasMataPelajaranPage(), + binding: MataPelajaranBinding(), + ), + GetPage( + name: AppRoutes.materiSiswa, + page: () => const MateriView(), + binding: MateriBinding(), + ), + GetPage( + name: AppRoutes.tugasSiswa, + page: () => Tugas(), + binding: TugasBinding(), + ), + GetPage( + name: AppRoutes.tugasDetailSiswa, + page: () => const TugasDetail(), + binding: DetailTugasBinding(), + ), + GetPage( + name: AppRoutes.tugasCommitSiswa, + page: () => const TugasCommit(), + binding: SubmitTugasBinding(), + ), + GetPage( + name: AppRoutes.matpelQuiz, + page: () => MatpelQuiz(), + binding: MatpelQuizBinding(), + ), + GetPage( + name: AppRoutes.matpelQuizDetail, + page: () => MatpelQuizDetail(), + binding: QuizBinding(), + ), + GetPage( + name: AppRoutes.soalQuiz, + page: () => const SoalQuiz(), + binding: SoalQuizBinding(), + ), + GetPage( + name: AppRoutes.quizSelesai, + page: () => SoalQuizSelesai(), + binding: QuizFinishBinding(), + ), + + GetPage( + name: AppRoutes.matpelRankQuiz, + page: () => MatpelRank(), + binding: MatpelRankBinding(), + ), + GetPage( + name: AppRoutes.matpelQuizRankDetail, + page: () => MatpelRankDetail(), + binding: QuizRankBinding(), + ), + GetPage( + name: AppRoutes.rankSiswa, + page: () => RankSiswa(), + binding: RankingBinding(), + ), + + GetPage( + name: AppRoutes.profileSiswa, + page: () => ProfileSiswa(), + ), + GetPage( + name: AppRoutes.ubahPassord, + page: () => const UbahPasswordPage(), + binding: UbahPasswordBinding(), + ), + + // GURU + GetPage( + name: AppRoutes.guruMatpel, + page: () => const MataPelajaranGuru(), + binding: MatpelGuruBinding(), + ), + GetPage( + name: AppRoutes.profileguru, + page: () => const ProfileGuruPage(), + binding: SiswaBinding(), + ), + GetPage( + name: AppRoutes.tugasGuru, + page: () => TugasGuruPage(), + binding: TugasGuruBinding(), + ), + GetPage( + name: AppRoutes.tugasDetailGuru, + page: () => DetailTugasGuru(), + binding: TugasDetailGuruBinding(), + ), + GetPage( + name: AppRoutes.detailSubmitTugasDetailGuru, + page: () => const DetailSubmitTugas(), + binding: DetailSubmitTugasSiswaBinding(), + ), + GetPage( + name: AppRoutes.reviewSubmitTugasSiswaOnGuru, + page: () => const ReviewSubmitTugas(), + // binding: DetailSubmitTugasSiswaBinding(), + ), + GetPage( + name: AppRoutes.mataPelajaranQuizGuru, + page: () => const MataPelajaranQuizGuru(), + binding: MatpelQuizGuruBinding(), + ), + GetPage( + name: AppRoutes.quizGuru, + page: () => QuizGuru(), + binding: QuizGuruBinding(), + ), + GetPage( + name: AppRoutes.quizDetailGuru, + page: () => QuizDetailGuru(), + binding: QuizDetailGuruBinding(), + ), + ]; +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart new file mode 100644 index 0000000..f7ddf83 --- /dev/null +++ b/lib/routes/app_routes.dart @@ -0,0 +1,43 @@ +class AppRoutes { + // Siswa + static const String siswaDashboard = "/siswa-dashboard"; + static const String kelasmatapelajarans = '/kelas-matapelajaran'; + static const String mataPelajaran = "/mata-pelajaran"; + static const String notifikasiSiswa = '/notifikasi-siswa'; + static const String materiSiswa = '/materi-siswa'; + static const String tugasSiswa = '/tugas-siswa'; + static const String tugasDetailSiswa = '/tugas-detail-siswa'; + static const String tugasCommitSiswa = '/tugas-commit-siswa'; + static const String matpelQuiz = '/matpel-quiz'; + static const String matpelQuizDetail = '/matpel-quiz-detail'; + static const String soalQuiz = '/soal-quiz'; + static const String quizSelesai = '/quiz-selesai'; + + static const String matpelRankQuiz = '/matpel-quiz-rank'; + static const String matpelQuizRankDetail = '/matpel-quiz-rank-detail'; + static const String rankSiswa = '/rank-siswa'; + static const String profileSiswa = '/profile-siswa'; + + // Guru + static const String guruDashboard = "/guru-dashboard"; + static const String guruMatpel = "/guru-Matpel"; + static const String profileguru = "/profile-guru"; + static const String tugasGuru = "/tugas-guru"; + static const String tugasDetailGuru = "/tugas-detail-guru"; + static const String detailSubmitTugasDetailGuru = + "/detail-submit-tugas-siswa-guru"; + static const String reviewSubmitTugasSiswaOnGuru = + "/review-submit-tugas-siswa-on-guru"; + + static const String mataPelajaranQuizGuru = "/matapelajaran-quiz-guru"; + static const String quizGuru = "/quiz-guru"; + static const String quizDetailGuru = "/quiz-detail-guru"; + + // + static const String welcome = "/welcome"; + static const String selection = "/selection"; + static const String login = "/login"; + static const String splash = "/splash"; + + static const String ubahPassord = "/ubah-password"; +} diff --git a/lib/routes/export.dart b/lib/routes/export.dart new file mode 100644 index 0000000..27bd1ec --- /dev/null +++ b/lib/routes/export.dart @@ -0,0 +1,22 @@ +export 'package:get/get.dart'; +export 'package:ui/middlewares/auth_middleware.dart'; +export 'package:ui/views/auth/bindings/auth_binding.dart'; +export 'package:ui/views/auth/login_page.dart'; +export 'package:ui/views/common/splash_screen.dart'; +export 'package:ui/views/common/welcome_page.dart'; +export 'package:ui/views/common/selection_page.dart'; +export 'package:ui/views/guru/dashboard/index.dart'; +export 'package:ui/views/siswa/bindings/siswa_binding.dart'; +export 'package:ui/views/siswa/matapelajaran/bindings/mata_pelajaran_binding.dart'; +export 'package:ui/views/siswa/matapelajaran/mata_pelajaran.dart'; +export 'package:ui/views/siswa/materi/bindings/materi_binding.dart'; +export 'package:ui/views/siswa/materi/index.dart'; +export 'package:ui/views/siswa/notifikasi.dart'; +export 'package:ui/views/siswa/siswaDashboard.dart'; +export 'package:ui/views/siswa/tugas/bindings/detail_tugas_binding.dart'; +export 'package:ui/views/siswa/tugas/bindings/submit_tugas_binding.dart'; +export 'package:ui/views/siswa/tugas/bindings/tugas_binding.dart'; +export 'package:ui/views/siswa/tugas/tugas.dart'; +export 'package:ui/views/siswa/tugas/tugas_commit.dart'; +export 'package:ui/views/siswa/tugas/tugas_detail.dart'; + diff --git a/lib/views/auth/bindings/auth_binding.dart b/lib/views/auth/bindings/auth_binding.dart new file mode 100644 index 0000000..85f5e22 --- /dev/null +++ b/lib/views/auth/bindings/auth_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/auth/controllers/auth_controller.dart'; + +class AuthBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AuthController()); + } +} diff --git a/lib/views/auth/controllers/auth_controller.dart b/lib/views/auth/controllers/auth_controller.dart new file mode 100644 index 0000000..63d3df9 --- /dev/null +++ b/lib/views/auth/controllers/auth_controller.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/widgets/my_snackbar.dart'; + +class AuthController extends GetxController { + var isLoading = false.obs; + SharedPreferences? prefs; + TextEditingController loginC = TextEditingController(); + TextEditingController passwordC = TextEditingController(); + + Future login() async { + prefs = await SharedPreferences.getInstance(); + var headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + try { + isLoading(true); + Map body = { + 'login': loginC.text, + 'password': passwordC.text, + }; + if (loginC.text == "" || passwordC.text == "") { + snackbarfailed("Inputan login tidak boleh kosong!"); + } else { + // Tambahkan log URL endpoint untuk debugging + ApiConstants.debugApiUrls(); + debugPrint("Login endpoint: ${ApiConstants.loginEnpoint}"); + final response = await http.post( + Uri.parse(ApiConstants.loginEnpoint), + body: jsonEncode(body), + headers: headers, + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + // Cek struktur JSON sebelum akses + if (json['data'] == null || json['data']['user'] == null) { + debugPrint("Struktur JSON tidak sesuai: ${response.body}"); + snackbarfailed("Login gagal, data user tidak ditemukan!"); + return; + } + final user = json['data']['user']; + await prefs?.setString('token', json['data']['token']); + await prefs?.setString('nama', user['nama']); + await prefs?.setString('role', user['user']['role']); + + if (user['user']['role'] == "siswa") { + await prefs?.setString('nisn', user['nisn']); + Get.offAllNamed(AppRoutes.siswaDashboard); + } else { + await prefs?.setString('nip', user['nip']); + Get.offAllNamed(AppRoutes.guruDashboard); + } + snackbarSuccess("Login Berhasil"); + } else { + debugPrint("Login gagal: ${response.body}"); + snackbarfailed("Login Gagal, inputan atau sandi salah!"); + } + } + } catch (e) { + debugPrint("Login Exception: $e"); + snackbarfailed("Terjadi error: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/auth/controllers/check_token_controller.dart b/lib/views/auth/controllers/check_token_controller.dart new file mode 100644 index 0000000..81d78ba --- /dev/null +++ b/lib/views/auth/controllers/check_token_controller.dart @@ -0,0 +1,35 @@ +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; +import 'package:ui/constans/api_constans.dart'; + +class CheckTokenController extends GetxController { + Future checkToken() async { + log("controller check token running..."); + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + return 401; // langsung return + } + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + try { + final response = await http.get( + Uri.parse(ApiConstants.checkToken), + headers: headers, + ); + log("Status Code dari API: ${response.statusCode}"); + return response.statusCode; + } catch (e) { + log("Error getMe: $e"); + return 500; + } + } +} diff --git a/lib/views/auth/login_page.dart b/lib/views/auth/login_page.dart new file mode 100644 index 0000000..dc78040 --- /dev/null +++ b/lib/views/auth/login_page.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/views/auth/controllers/auth_controller.dart'; +import 'dart:math' as math; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State with TickerProviderStateMixin { + late String role; + late String idFieldLabel; + final AuthController authController = Get.find(); + late AnimationController _controller; + late Animation _animation; + + final Map roleImages = { + "siswa": "https://cdn-icons-png.flaticon.com/512/201/201818.png", + "guru": "https://cdn-icons-png.flaticon.com/512/1995/1995574.png", + "admin": "https://cdn-icons-png.flaticon.com/512/2206/2206368.png", + }; + + @override + void initState() { + super.initState(); + role = (Get.arguments is String) ? Get.arguments as String : "siswa"; + idFieldLabel = getIdFieldLabel(role); + + _initializeAnimation(); + } + + void _initializeAnimation() { + if (!mounted) return; + + _controller = AnimationController( + duration: const Duration(seconds: 10), + vsync: this, + ); + + _animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.linear, + ), + ); + + if (mounted) { + _controller.repeat(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + String getIdFieldLabel(String role) { + switch (role.toLowerCase()) { + case "siswa": + return "NIS atau Email Siswa"; + case "guru": + return "Email atau Nip Guru"; + default: + return "ID"; + } + } + + Widget _buildAnimatedShapes() { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + if (!mounted) return const SizedBox.shrink(); + + return Stack( + children: [ + // Shape 1 + Positioned( + left: math.sin(_animation.value * 2 * math.pi) * 50 + + Get.width * 0.1, + top: math.cos(_animation.value * 2 * math.pi) * 50 + + Get.height * 0.1, + child: Transform.rotate( + angle: _animation.value * 2 * math.pi, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color.fromRGBO(157, 157, 136, 0.1), + Color.fromRGBO(33, 198, 41, 0.1), + ], + ), + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ), + // Shape 2 + Positioned( + right: math.cos(_animation.value * 2 * math.pi) * 50 + + Get.width * 0.1, + top: math.sin(_animation.value * 2 * math.pi) * 50 + + Get.height * 0.2, + child: Transform.rotate( + angle: -_animation.value * 2 * math.pi, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color.fromRGBO(157, 157, 136, 0.1), + Color.fromRGBO(33, 198, 41, 0.1), + ], + ), + borderRadius: BorderRadius.circular(25), + ), + ), + ), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFE6F7F4), + body: Stack( + children: [ + // Animated Background Shapes + _buildAnimatedShapes(), + + // Main Content + Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: Column( + children: [ + Image.network( + roleImages[role.toLowerCase()]!, + width: 100, + height: 100, + ), + const SizedBox(height: 16.0), + Text( + role.capitalizeFirst ?? role, + style: const TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: Color(0xFF2E7D32), + ), + ), + const SizedBox(height: 32.0), + TextField( + controller: authController.loginC, + decoration: _inputDecoration(idFieldLabel), + ), + const SizedBox(height: 24.0), + TextField( + controller: authController.passwordC, + obscureText: true, + decoration: _inputDecoration("Kata Sandi"), + ), + const SizedBox(height: 32.0), + Obx(() => authController.isLoading.value + ? const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF2E7D32)), + ) + : ElevatedButton( + onPressed: () { + authController.login(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2E7D32), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 50, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: const Text( + "Masuk", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + )), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + InputDecoration _inputDecoration(String label) { + return InputDecoration( + labelText: label, + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30.0), + borderSide: const BorderSide(color: Color(0xFF2E7D32)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(30.0), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(30.0), + borderSide: const BorderSide(color: Color(0xFF2E7D32), width: 2), + ), + labelStyle: TextStyle(color: Colors.grey.shade600), + ); + } +} diff --git a/lib/views/common/selection_page.dart b/lib/views/common/selection_page.dart new file mode 100644 index 0000000..93f4b53 --- /dev/null +++ b/lib/views/common/selection_page.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; + +class SelectionPage extends StatelessWidget { + const SelectionPage({super.key}); + + @override + Widget build(BuildContext context) { + // Langsung redirect ke login siswa saat halaman ini dibuka + Future.microtask(() { + Get.offAllNamed(AppRoutes.login, arguments: 'siswa'); + }); + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } +} diff --git a/lib/views/common/splash_screen.dart b/lib/views/common/splash_screen.dart new file mode 100644 index 0000000..a1aebd8 --- /dev/null +++ b/lib/views/common/splash_screen.dart @@ -0,0 +1,92 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/auth/controllers/check_token_controller.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + _SplashScreenState createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + final checkTokenC = Get.put(CheckTokenController()); + @override + void initState() { + super.initState(); + _checkLoginStatus(); + } + + Future _checkLoginStatus() async { + await Future.delayed(const Duration(seconds: 2)); + + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + final role = prefs.getString('role'); + + int statusToken = await checkTokenC.checkToken(); + + if (statusToken == 401) { + log("Menjalankan Logout"); + await prefs.remove('nama'); + await prefs.remove('token'); + await prefs.remove('role'); + await prefs.remove('nisn'); + await prefs.remove('nip'); + await prefs.clear(); + + if (role == "siswa") { + Get.offAllNamed(AppRoutes.login, arguments: "siswa"); + } else if (role == "guru") { + Get.offAllNamed(AppRoutes.login, arguments: "guru"); + } + } + + if (token != null && token.isNotEmpty) { + // Jika token ada, arahkan ke dashboard sesuai role + switch (role) { + case 'siswa': + Get.offAllNamed(AppRoutes.siswaDashboard); + break; + case 'guru': + Get.offAllNamed(AppRoutes.guruDashboard); + break; + default: + Get.offAllNamed(AppRoutes.login); // Jika role tidak dikenali + break; + } + } else { + if (!mounted) return; + Get.offAllNamed(AppRoutes.welcome); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.teal[100], // Bisa diganti dengan warna tema + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + "assets/images/skoda.png", + width: 100, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.error_outline, + size: 100, color: Colors.red); + }, + ), + const SizedBox(height: 20), + const CircularProgressIndicator( + color: Colors.white), // Animasi loading + ], + ), + ), + ); + } +} diff --git a/lib/views/common/welcome_page.dart b/lib/views/common/welcome_page.dart new file mode 100644 index 0000000..5d12b53 --- /dev/null +++ b/lib/views/common/welcome_page.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; + +class WelcomePage extends StatelessWidget { + const WelcomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + color: Colors.white, + image: const DecorationImage( + image: AssetImage("assets/images/welcomescreen.png"), + fit: BoxFit.cover, + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "E-Learning", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + const Text( + "SD NEGERI", + style: TextStyle( + fontSize: 16, + color: Colors.black, + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + Get.offAllNamed(AppRoutes + .selection); // Tidak bisa kembali ke Welcome setelah pindah + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.teal, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + padding: + const EdgeInsets.symmetric(horizontal: 40, vertical: 12), + ), + child: const Text( + "Masuk", + style: TextStyle(fontSize: 18, color: Colors.white), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/guru/dashboard/bindings/dashboard_binding.dart b/lib/views/guru/dashboard/bindings/dashboard_binding.dart new file mode 100644 index 0000000..4c9cbe8 --- /dev/null +++ b/lib/views/guru/dashboard/bindings/dashboard_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/controllers/siswa_controller.dart'; + +class DashboardBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SiswaController()); + } +} diff --git a/lib/views/guru/dashboard/controllers/dashboard_guru_controller.dart b/lib/views/guru/dashboard/controllers/dashboard_guru_controller.dart new file mode 100644 index 0000000..9c79500 --- /dev/null +++ b/lib/views/guru/dashboard/controllers/dashboard_guru_controller.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:http/http.dart' as http; + +class DashboardGuruController extends GetxController { + var isLoading = false.obs; + var dataUser = Rxn>(); + + @override + void onInit() { + super.onInit(); + getMe(); + } + + Future getMe() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + try { + final response = await http.get( + Uri.parse(ApiConstants.getMeEnpoint), + headers: headers, + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + log("Get Me Response: $json"); + dataUser.value = json; + } else { + log("Terjadi kesalahan get data user: ${response.statusCode}"); + throw Exception("Failed to get user data"); + } + } catch (e) { + log("Error get user data: $e"); + rethrow; + } + } +} diff --git a/lib/views/guru/dashboard/index.dart b/lib/views/guru/dashboard/index.dart new file mode 100644 index 0000000..770ec11 --- /dev/null +++ b/lib/views/guru/dashboard/index.dart @@ -0,0 +1,411 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/controllers/siswa_controller.dart'; +import 'package:ui/widgets/my_text.dart'; +import 'dart:math' as math; +import 'dart:ui'; +import 'package:ui/views/guru/dashboard/controllers/dashboard_guru_controller.dart'; +import 'package:flutter/rendering.dart'; + +class GuruDashboardPage extends StatefulWidget { + const GuruDashboardPage({super.key}); + + @override + State createState() => _GuruDashboardPageState(); +} + +class _GuruDashboardPageState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + final DashboardGuruController dashboardC = Get.put(DashboardGuruController()); + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 10), + vsync: this, + )..repeat(); + + _animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.linear, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Color _getRainbowColor(double value) { + final hue = (value * 360) % 360; + return HSLColor.fromAHSL(1.0, hue, 0.7, 0.5).toColor(); + } + + Widget _buildMenuTitle() { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + final rainbowColor = _getRainbowColor(_animation.value); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: rainbowColor.withOpacity(0.3), + blurRadius: 15, + spreadRadius: 2, + ), + BoxShadow( + color: rainbowColor.withOpacity(0.2), + blurRadius: 8, + spreadRadius: -2, + ), + ], + ), + child: MyText( + text: "Menu", + fontSize: 18, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ); + }, + ); + } + + Widget _buildMenuCard({ + required IconData icon, + required String title, + required Color color, + required VoidCallback onTap, + }) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + final glowColor = + HSLColor.fromColor(color).withLightness(0.7).toColor(); + final innerGlowColor = + HSLColor.fromColor(color).withLightness(0.8).toColor(); + + return Container( + height: 110, + margin: const EdgeInsets.only(bottom: 5), + child: InkWell( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.white.withOpacity(0.2), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: + glowColor.withOpacity(0.3 + (_animation.value * 0.2)), + blurRadius: 15, + spreadRadius: 2, + ), + BoxShadow( + color: innerGlowColor + .withOpacity(0.2 + (_animation.value * 0.1)), + blurRadius: 8, + spreadRadius: -2, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(15), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + color.withOpacity(0.2), + color.withOpacity(0.05), + ], + stops: const [0.0, 1.0], + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.2), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: Icon( + icon, + color: color, + size: 28, + ), + ), + const SizedBox(height: 8), + MyText( + text: title, + fontSize: 15, + color: Colors.black87, + fontWeight: FontWeight.w600, + ), + ], + ), + ), + ), + ), + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final SiswaController siswaC = Get.find(); + + return Scaffold( + body: Stack( + children: [ + // Background + Container( + color: const Color.fromARGB(255, 255, 255, 255), + ), + + // Animated Shapes + AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Stack( + children: [ + // Shape 1 + Positioned( + left: math.sin(_animation.value * 2 * math.pi) * 100 + + Get.width * 0.2, + top: math.cos(_animation.value * 2 * math.pi) * 100 + + Get.height * 0.2, + child: Transform.rotate( + angle: _animation.value * 2 * math.pi, + child: Container( + width: 150, + height: 150, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF81C784).withOpacity(0.3), + const Color(0xFF66BB6A).withOpacity(0.2), + ], + ), + borderRadius: BorderRadius.circular(30), + ), + ), + ), + ), + // Shape 2 + Positioned( + right: math.cos(_animation.value * 2 * math.pi) * 100 + + Get.width * 0.2, + top: math.sin(_animation.value * 2 * math.pi) * 100 + + Get.height * 0.3, + child: Transform.rotate( + angle: -_animation.value * 2 * math.pi, + child: Container( + width: 200, + height: 200, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF66BB6A).withOpacity(0.3), + const Color(0xFF4CAF50).withOpacity(0.2), + ], + ), + borderRadius: BorderRadius.circular(40), + ), + ), + ), + ), + // Shape 3 + Positioned( + left: math.cos(_animation.value * 2 * math.pi) * 150 + + Get.width * 0.3, + bottom: math.sin(_animation.value * 2 * math.pi) * 150 + + Get.height * 0.2, + child: Transform.rotate( + angle: _animation.value * 4 * math.pi, + child: Container( + width: 180, + height: 180, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF4CAF50).withOpacity(0.3), + const Color(0xFF43A047).withOpacity(0.2), + ], + ), + borderRadius: BorderRadius.circular(35), + ), + ), + ), + ), + ], + ); + }, + ), + + // Main Content + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Section + Container( + width: Get.width, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: const [ + Color.fromRGBO(157, 157, 136, 0), + Color.fromRGBO(33, 198, 41, 1), + ], + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 40), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MyText( + text: "Selamat Datang,", + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 5), + Obx(() { + if (dashboardC.isLoading.value) { + return const MyText( + text: "Loading...", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ); + } + return MyText( + text: dashboardC.dataUser.value?['data'] + ?['user']?['nama'] ?? + "Guru", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ); + }), + ], + ), + ], + ), + ], + ), + ), + + // Menu Section + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMenuTitle(), + const SizedBox(height: 10), + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 1.1, + children: [ + _buildMenuCard( + icon: Icons.assignment, + title: "Tugas", + color: Colors.blue, + onTap: () => Get.toNamed(AppRoutes.tugasGuru), + ), + _buildMenuCard( + icon: Icons.quiz, + title: "Quiz", + color: Colors.orange, + onTap: () => + Get.toNamed(AppRoutes.mataPelajaranQuizGuru), + ), + _buildMenuCard( + icon: Icons.class_, + title: "Kelas", + color: Colors.purple, + onTap: () => Get.toNamed(AppRoutes.guruMatpel), + ), + _buildMenuCard( + icon: Icons.person, + title: "Profil", + color: Colors.teal, + onTap: () => Get.toNamed(AppRoutes.profileguru), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/guru/mata_pelajaran/bindings/matpel_guru_binding.dart b/lib/views/guru/mata_pelajaran/bindings/matpel_guru_binding.dart new file mode 100644 index 0000000..5caa33a --- /dev/null +++ b/lib/views/guru/mata_pelajaran/bindings/matpel_guru_binding.dart @@ -0,0 +1,15 @@ +import 'package:get/get.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/kelas_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart'; + +class MatpelGuruBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => KelasController()); + Get.lazyPut(() => TahunAjaranController()); + Get.lazyPut( + () => MataPelajaranGuruController(), + ); + } +} diff --git a/lib/views/guru/mata_pelajaran/controllers/kelas_controller.dart b/lib/views/guru/mata_pelajaran/controllers/kelas_controller.dart new file mode 100644 index 0000000..afeb80b --- /dev/null +++ b/lib/views/guru/mata_pelajaran/controllers/kelas_controller.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/kelas_model.dart'; + +class KelasController extends GetxController { + var isLoading = false.obs; + var selectedKelas = Rxn(); + KelasModel? kelasM; + + @override + void onInit() { + super.onInit(); + getKelas(); + } + + Future getKelas() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + try { + isLoading(true); + final response = await http.get( + Uri.parse(ApiConstants.kelasEnpoint), + headers: headers, + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + kelasM = KelasModel.fromJson(json); + + // Set nilai pertama ke selectedKelas jika belum ada + if (kelasM != null && kelasM!.data.isNotEmpty) { + selectedKelas.value = kelasM!.data[0].nama; + } + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + } + } catch (e) { + log("Error getMe: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart b/lib/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart new file mode 100644 index 0000000..b974920 --- /dev/null +++ b/lib/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/mata_pelajaran_model.dart'; +import 'package:http/http.dart' as http; + +class MataPelajaranGuruController extends GetxController { + var isLoading = false.obs; + MataPelajaranModel? mataPelajaranM; + var isEmptyData = true.obs; + var isFetchData = false.obs; + + Future getMatPel({ + required String kelas, + required String tahunAjaran, + }) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isFetchData(true); + isLoading(true); + final response = await http.get( + Uri.parse( + "${ApiConstants.mataPelajaranEnpoint}?kelas=$kelas&tahun_ajaran=$tahunAjaran"), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + mataPelajaranM = MataPelajaranModel.fromJson(data); + if (mataPelajaranM?.data.isEmpty ?? true) { + isEmptyData(true); + } else { + isEmptyData(false); + } + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + } + } catch (e) { + log("Error getMe: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart b/lib/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart new file mode 100644 index 0000000..1624ba1 --- /dev/null +++ b/lib/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/tahun_ajaran.dart'; +// import 'package:ui/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart'; + +class TahunAjaranController extends GetxController { + var isLoading = false.obs; + var selectedTahun = Rxn(); + TahunAjaranModel? tahunAjaranM; + + @override + void onInit() { + super.onInit(); + getTahunAjaran(); + + // // fetch matpel tiap kali kelas berubah + // ever(selectedTahun, (kelas) { + // if (kelas != null) { + // final matpelGuruC = Get.find(); + // matpelGuruC.getMatPel(kelas: kelas); + // } + // }); + } + + Future getTahunAjaran() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + try { + isLoading(true); + final response = await http.get( + Uri.parse(ApiConstants.tahunAjaranEnpoint), + headers: headers, + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + tahunAjaranM = TahunAjaranModel.fromJson(json); + + // Set nilai pertama ke selectedTahun jika belum ada + if (tahunAjaranM != null && tahunAjaranM!.data.isNotEmpty) { + selectedTahun.value = tahunAjaranM!.data[0].tahun; + } + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + } + } catch (e) { + log("Error getMe: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/guru/mata_pelajaran/filter_matpel.dart b/lib/views/guru/mata_pelajaran/filter_matpel.dart new file mode 100644 index 0000000..605d8ff --- /dev/null +++ b/lib/views/guru/mata_pelajaran/filter_matpel.dart @@ -0,0 +1,165 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/kelas_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class FilterMatpel extends StatelessWidget { + FilterMatpel({ + super.key, + required this.kelasC, + required this.tahunAjaranC, + required this.matpelGuruC, + }); + + final KelasController kelasC; + final TahunAjaranController tahunAjaranC; + final MataPelajaranGuruController matpelGuruC; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MyText( + text: "Filter Mata Pelajaran", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Obx(() { + if (kelasC.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (kelasC.kelasM?.data == null || + kelasC.kelasM!.data.isEmpty) { + return const Text('Tidak ada data kelas'); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: DropdownButtonFormField( + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 8), + isDense: true, + ), + icon: const Icon(Icons.arrow_drop_down, + color: Colors.grey, size: 20), + dropdownColor: Colors.white, + isExpanded: true, + value: kelasC.selectedKelas.value, + items: kelasC.kelasM!.data.map((kelas) { + return DropdownMenuItem( + value: kelas.nama, + child: Text( + kelas.nama, + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + kelasC.selectedKelas.value = value; + matpelGuruC.getMatPel( + kelas: value, + tahunAjaran: tahunAjaranC.selectedTahun.value ?? '', + ); + } + }, + ), + ); + }), + ), + const SizedBox(width: 12), + Expanded( + child: Obx(() { + if (tahunAjaranC.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (tahunAjaranC.tahunAjaranM?.data == null || + tahunAjaranC.tahunAjaranM!.data.isEmpty) { + return const Text('Tidak ada data Tahun'); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: DropdownButtonFormField( + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 8), + isDense: true, + ), + icon: const Icon(Icons.arrow_drop_down, + color: Colors.grey, size: 20), + dropdownColor: Colors.white, + isExpanded: true, + value: tahunAjaranC.selectedTahun.value, + items: tahunAjaranC.tahunAjaranM!.data.map((tahun) { + return DropdownMenuItem( + value: tahun.tahun, + child: Text( + tahun.tahun, + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + tahunAjaranC.selectedTahun.value = value; + matpelGuruC.getMatPel( + kelas: kelasC.selectedKelas.value ?? '', + tahunAjaran: value, + ); + } + }, + ), + ); + }), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/views/guru/mata_pelajaran/index.dart b/lib/views/guru/mata_pelajaran/index.dart new file mode 100644 index 0000000..12de096 --- /dev/null +++ b/lib/views/guru/mata_pelajaran/index.dart @@ -0,0 +1,264 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/kelas_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/filter_matpel.dart'; +import 'package:ui/widgets/my_date_format.dart'; +import 'package:ui/widgets/my_text.dart'; + +class MataPelajaranGuru extends StatefulWidget { + const MataPelajaranGuru({super.key}); + + @override + State createState() => _MataPelajaranGuruState(); +} + +class _MataPelajaranGuruState extends State { + MataPelajaranGuruController matpelGuruC = + Get.find(); + + KelasController kelasC = Get.find(); + TahunAjaranController tahunAjaranC = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const MyText( + text: "Mata Pelajaran", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + backgroundColor: const Color(0xFF57E389), + elevation: 0, + ), + body: Container( + decoration: const BoxDecoration( + color: Color(0xFF57E389), + ), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + child: Column( + children: [ + // Header with Filter + Container( + width: Get.width, + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + color: Color(0xFF57E389), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + ), + child: FilterMatpel( + kelasC: kelasC, + tahunAjaranC: tahunAjaranC, + matpelGuruC: matpelGuruC, + ), + ), + + // Data List + Expanded( + child: Obx(() { + if (matpelGuruC.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } else if (!matpelGuruC.isFetchData.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.filter_list, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + const MyText( + text: "Silahkan Filter untuk\nMelihat Data", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + textAlign: TextAlign.center, + ), + ], + ), + ); + } else if (matpelGuruC.isEmptyData.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.school, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + MyText( + text: + "Data Mata Pelajaran kelas ${kelasC.selectedKelas.value}\nPada tahun ajaran ${tahunAjaranC.selectedTahun.value}\nMasih Kosong", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + textAlign: TextAlign.center, + ), + ], + ), + ); + } + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: matpelGuruC.mataPelajaranM?.data.length ?? 0, + itemBuilder: (context, index) { + final data = matpelGuruC.mataPelajaranM!.data[index]; + return GestureDetector( + onTap: () => Get.toNamed(AppRoutes.materiSiswa, + arguments: data.id), + child: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF57E389) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.book, + color: Color(0xFF57E389), + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + MyText( + text: data.nama, + fontSize: 18, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + const SizedBox(height: 4), + MyText( + text: "Guru: ${data.guru.nama}", + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + _buildStatItem( + icon: Icons.menu_book, + value: "${data.jumlahBuku}", + label: "Materi", + color: Colors.red, + ), + _buildStatItem( + icon: Icons.video_library, + value: "${data.jumlahVideo}", + label: "Video", + color: Colors.blue, + ), + _buildStatItem( + icon: Icons.update, + value: (data.materi.isNotEmpty + ? data.materi.last.tanggal + : data.createdAt) + .getSimpleDayAndDate(), + label: "Diperbarui", + color: Colors.green, + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + }), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatItem({ + required IconData icon, + required String value, + required String label, + required Color color, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(height: 4), + MyText( + text: value, + fontSize: 12, + color: color, + fontWeight: FontWeight.w600, + ), + MyText( + text: label, + fontSize: 12, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ); + } +} diff --git a/lib/views/guru/profiles/index.dart b/lib/views/guru/profiles/index.dart new file mode 100644 index 0000000..115e239 --- /dev/null +++ b/lib/views/guru/profiles/index.dart @@ -0,0 +1,198 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/views/siswa/controllers/siswa_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class ProfileGuruPage extends StatefulWidget { + const ProfileGuruPage({super.key}); + + @override + State createState() => _ProfileGuruPageState(); +} + +class _ProfileGuruPageState extends State { + SiswaController siswaC = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const MyText( + text: "Profil Guru", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + backgroundColor: const Color(0xFF57E389), + elevation: 0, + ), + body: Container( + decoration: const BoxDecoration( + color: Color(0xFF57E389), + ), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + child: Obx( + () { + if (siswaC.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + var data = siswaC.dataUser; + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFF57E389), + width: 2, + ), + ), + child: const CircleAvatar( + radius: 50, + backgroundImage: NetworkImage( + 'https://cdn.builder.io/api/v1/image/assets/7269843b34254a84ac205c1bfd7d31c3/85a37fd6b502cfa6f311cf6cb4af2f561dfa7c5fcbfb1afdd620a50cbfe97ea1?apiKey=7269843b34254a84ac205c1bfd7d31c3&', + ), + ), + ), + const SizedBox(height: 20), + MyText( + text: data['user']['nama'], + fontSize: 24, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + const SizedBox(height: 8), + MyText( + text: "Guru", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 30), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + _buildInfoTile( + icon: Icons.badge, + title: "NIP", + value: data['user']['nip'], + ), + const Divider(height: 1), + _buildInfoTile( + icon: Icons.email, + title: "Email", + value: data['user']['user']['email'], + ), + ], + ), + ), + const SizedBox(height: 30), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () async { + await siswaC.logout(role: "guru"); + }, + icon: const Icon( + Icons.logout, + color: Colors.white, + ), + label: const MyText( + text: 'Logout', + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ), + ), + ); + } + + Widget _buildInfoTile({ + required IconData icon, + required String title, + required String value, + }) { + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF57E389).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: const Color(0xFF57E389), + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText( + text: title, + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 4), + MyText( + text: value, + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/guru/quiz/bindings/matpel_quiz_guru_binding.dart b/lib/views/guru/quiz/bindings/matpel_quiz_guru_binding.dart new file mode 100644 index 0000000..7fe552b --- /dev/null +++ b/lib/views/guru/quiz/bindings/matpel_quiz_guru_binding.dart @@ -0,0 +1,15 @@ +import 'package:get/get.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/kelas_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart'; + +class MatpelQuizGuruBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => KelasController()); + Get.lazyPut(() => TahunAjaranController()); + Get.lazyPut( + () => MataPelajaranGuruController(), + ); + } +} diff --git a/lib/views/guru/quiz/bindings/quiz_detail_guru_binding.dart b/lib/views/guru/quiz/bindings/quiz_detail_guru_binding.dart new file mode 100644 index 0000000..de68c74 --- /dev/null +++ b/lib/views/guru/quiz/bindings/quiz_detail_guru_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/guru/quiz/controllers/quiz_detail_guru_controller.dart'; + +class QuizDetailGuruBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => QuizDetailGuruController()); + } +} diff --git a/lib/views/guru/quiz/bindings/quiz_guru_binding.dart b/lib/views/guru/quiz/bindings/quiz_guru_binding.dart new file mode 100644 index 0000000..a7c0399 --- /dev/null +++ b/lib/views/guru/quiz/bindings/quiz_guru_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/guru/quiz/controllers/quiz_guru_controller.dart'; + +class QuizGuruBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => QuizGuruController()); + } +} diff --git a/lib/views/guru/quiz/controllers/quiz_detail_guru_controller.dart b/lib/views/guru/quiz/controllers/quiz_detail_guru_controller.dart new file mode 100644 index 0000000..5bd00fd --- /dev/null +++ b/lib/views/guru/quiz/controllers/quiz_detail_guru_controller.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; + +class QuizDetailGuruController extends GetxController { + var isLoading = false.obs; + var isEmptyData = true.obs; + var data = [].obs; + + @override + void onInit() { + super.onInit(); + var id = Get.arguments['quiz_id']; + var kelas = Get.arguments['kelas']; + var tahunAjaran = Get.arguments['tahun_ajaran']; + + getQuiz(id, kelas, tahunAjaran); + } + + Future getQuiz(id, kelas, tahunAjaran) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse( + "${ApiConstants.quizDetailGuruEnpoint}?quiz_id=$id&kelas=$kelas&tahun_ajaran=$tahunAjaran"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = await jsonDecode(response.body); + data.value = json['data']; + log(json.toString()); + if (json['data'].length == 0) { + isEmptyData(true); + } else { + isEmptyData(false); + } + } else { + log("Terjadi kesalahan get data quiz detail guru: ${response.statusCode}"); + } + } catch (e) { + log("Error get quiz detail guru: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/guru/quiz/controllers/quiz_guru_controller.dart b/lib/views/guru/quiz/controllers/quiz_guru_controller.dart new file mode 100644 index 0000000..a4bc7e3 --- /dev/null +++ b/lib/views/guru/quiz/controllers/quiz_guru_controller.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/quiz_guru_model.dart'; +import 'package:http/http.dart' as http; + +class QuizGuruController extends GetxController { + var isLoading = false.obs; + QuizGuruModel? quizGuruM; + var isEmptyData = true.obs; + + @override + void onInit() { + super.onInit(); + var id = Get.arguments['id']; + var kelas = Get.arguments['kelas']; + var tahunAjaran = Get.arguments['tahun_ajaran']; + + getQuiz(id, kelas, tahunAjaran); + } + + Future getQuiz(id, kelas, tahunAjaran) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse( + "${ApiConstants.quizGuruEnpoint}?matapelajaran_id=$id&kelas=$kelas&tahun_ajaran=$tahunAjaran"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = await jsonDecode(response.body); + log(json.toString()); + quizGuruM = QuizGuruModel.fromJson(json); + if (quizGuruM!.data.isEmpty) { + isEmptyData(true); + } else { + isEmptyData(false); + } + } else { + log("Terjadi kesalahan get data quiz guru: ${response.statusCode}"); + } + } catch (e) { + log("Error get quiz guru: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/guru/quiz/index.dart b/lib/views/guru/quiz/index.dart new file mode 100644 index 0000000..1d2e0ca --- /dev/null +++ b/lib/views/guru/quiz/index.dart @@ -0,0 +1,284 @@ +// ignore_for_file: must_be_immutable + +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/kelas_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/mata_pelajaran_guru_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/filter_matpel.dart'; +import 'package:ui/widgets/my_date_format.dart'; +import 'package:ui/widgets/my_text.dart'; + +class MataPelajaranQuizGuru extends StatefulWidget { + const MataPelajaranQuizGuru({super.key}); + + @override + State createState() => _MataPelajaranQuizGuruState(); +} + +class _MataPelajaranQuizGuruState extends State { + MataPelajaranGuruController matpelGuruC = + Get.find(); + KelasController kelasC = Get.find(); + TahunAjaranController tahunAjaranC = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const MyText( + text: "Mata Pelajaran Quiz", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + backgroundColor: const Color(0xFF57E389), + elevation: 0, + ), + body: Container( + decoration: const BoxDecoration( + color: Color(0xFF57E389), + ), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + child: Column( + children: [ + // Header with Filter + Container( + width: Get.width, + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + color: Color(0xFF57E389), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + ), + child: FilterMatpel( + kelasC: kelasC, + tahunAjaranC: tahunAjaranC, + matpelGuruC: matpelGuruC, + ), + ), + + // Data List + Expanded( + child: Obx(() { + if (matpelGuruC.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } else if (!matpelGuruC.isFetchData.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.filter_list, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + const MyText( + text: "Silahkan Filter untuk\nMelihat Data", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + textAlign: TextAlign.center, + ), + ], + ), + ); + } else if (matpelGuruC.isEmptyData.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.school, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + MyText( + text: + "Data Mata Pelajaran kelas ${kelasC.selectedKelas.value}\nPada tahun ajaran ${tahunAjaranC.selectedTahun.value}\nMasih Kosong", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + textAlign: TextAlign.center, + ), + ], + ), + ); + } + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: matpelGuruC.mataPelajaranM?.data.length ?? 0, + itemBuilder: (context, index) { + final data = matpelGuruC.mataPelajaranM!.data[index]; + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(15), + onTap: () { + Get.toNamed( + AppRoutes.quizGuru, + arguments: { + 'id': data.id, + 'matpel': data.nama, + 'kelas': kelasC.selectedKelas.value, + 'tahun_ajaran': + tahunAjaranC.selectedTahun.value + }, + ); + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.1), + borderRadius: + BorderRadius.circular(12), + ), + child: const Icon( + Icons.quiz, + color: Colors.orange, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + MyText( + text: data.nama, + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + const SizedBox(height: 4), + MyText( + text: "Guru: ${data.guru.nama}", + fontSize: 12, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ), + const Icon( + Icons.arrow_forward_ios, + color: Colors.grey, + size: 16, + ), + ], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + _buildStatItem( + icon: Icons.menu_book, + value: "${data.jumlahBuku}", + label: "Materi", + color: Colors.red, + ), + _buildStatItem( + icon: Icons.video_library, + value: "${data.jumlahVideo}", + label: "Video", + color: Colors.blue, + ), + _buildStatItem( + icon: Icons.update, + value: (data.materi.isNotEmpty + ? data.materi.last.tanggal + : data.createdAt) + .getSimpleDayAndDate(), + label: "Diperbarui", + color: Colors.green, + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ); + }), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatItem({ + required IconData icon, + required String value, + required String label, + required Color color, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(height: 4), + MyText( + text: value, + fontSize: 12, + color: color, + fontWeight: FontWeight.w600, + ), + MyText( + text: label, + fontSize: 12, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ); + } +} diff --git a/lib/views/guru/quiz/quiz.dart b/lib/views/guru/quiz/quiz.dart new file mode 100644 index 0000000..302774f --- /dev/null +++ b/lib/views/guru/quiz/quiz.dart @@ -0,0 +1,171 @@ +// ignore_for_file: must_be_immutable +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/guru/quiz/controllers/quiz_guru_controller.dart'; +import 'package:ui/widgets/my_text.dart'; +import 'package:ui/widgets/my_date_format.dart'; + +class QuizGuru extends StatelessWidget { + QuizGuru({super.key}); + QuizGuruController quizC = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: MyText( + text: "Quiz ${Get.arguments['matpel']}", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + backgroundColor: const Color(0xFF57E389), + elevation: 0, + ), + body: Container( + decoration: const BoxDecoration( + color: Color(0xFF57E389), + ), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MyText( + text: "Daftar Quiz", + fontSize: 18, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + const SizedBox(height: 20), + Expanded( + child: Obx(() { + if (quizC.isLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (quizC.isEmptyData.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.quiz_outlined, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + const MyText( + text: "Belum ada quiz yang dibuat", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + textAlign: TextAlign.center, + ), + ], + ), + ); + } else { + return ListView.builder( + itemCount: quizC.quizGuruM?.data.length ?? 0, + itemBuilder: (context, index) { + var data = quizC.quizGuruM?.data[index]; + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: InkWell( + onTap: () { + Get.toNamed(AppRoutes.quizDetailGuru, + arguments: { + 'quiz_id': data!.id.toString(), + 'kelas': Get.arguments['kelas'], + 'tahun_ajaran': + Get.arguments['tahun_ajaran'], + 'judul': data.judul, + }); + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.1), + borderRadius: + BorderRadius.circular(12), + ), + child: const Icon( + Icons.quiz, + color: Colors.orange, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + MyText( + text: data!.judul, + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + const SizedBox(height: 4), + MyText( + text: + "Dibuat pada: ${data.createdAt.simpleDateRevers()}", + fontSize: 12, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ), + const Icon( + Icons.arrow_forward_ios, + color: Colors.grey, + size: 16, + ), + ], + ), + ), + ), + ); + }, + ); + } + }), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/guru/quiz/quiz_detail.dart b/lib/views/guru/quiz/quiz_detail.dart new file mode 100644 index 0000000..88fb95e --- /dev/null +++ b/lib/views/guru/quiz/quiz_detail.dart @@ -0,0 +1,154 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/views/guru/quiz/controllers/quiz_detail_guru_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class QuizDetailGuru extends StatelessWidget { + QuizDetailGuru({super.key}); + QuizDetailGuruController quizDetailGuruC = + Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + // backgroundColor: Colors.amber, + title: const Text("Quiz"), + ), + body: Column( + children: [ + Container( + width: Get.width, + decoration: BoxDecoration( + color: Colors.green.shade200, + ), + child: Padding( + padding: const EdgeInsets.all(15), + child: Center( + child: MyText( + text: Get.arguments['judul'], + textAlign: TextAlign.center, + fontSize: 15, + color: Colors.black, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(height: 20), + Obx( + () { + if (quizDetailGuruC.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + if (quizDetailGuruC.isEmptyData.value) { + return const Padding( + padding: EdgeInsets.only(top: 50), + child: Center( + child: MyText( + text: "Quiz Masih Kosong", + fontSize: 14, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + ), + ); + } + return ListView.builder( + itemCount: quizDetailGuruC.data.length, + shrinkWrap: true, + padding: const EdgeInsets.only(bottom: 7), + itemBuilder: (context, index) { + var data = quizDetailGuruC.data[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Container( + width: Get.width, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white, + boxShadow: const [ + BoxShadow( + color: Colors.black38, + offset: Offset(0, 1), + blurRadius: 2, + ), + ]), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SizedBox( + width: 30, + child: CircleAvatar( + child: Text("${index + 1}", + style: const TextStyle(fontSize: 14)), + ), + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: Get.width * 0.6, + child: MyText( + maxLines: 1, + text: data['nama'] + + "asd;kas;dklasd as;dlkas;l dk;lk", + fontSize: 14, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + ), + MyText( + text: + "Skor : ${data['skor']} | Nilai : ${data['persentase']} | KKM : ${data['kkm']}", + fontSize: 12, + color: Colors.black, + fontWeight: FontWeight.w800, + ), + ], + ), + ], + ), + Container( + decoration: BoxDecoration( + color: data['persentase'] < data['kkm'] + ? Colors.red + : Colors.green, + borderRadius: BorderRadius.circular(5), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 1), + child: MyText( + text: data['persentase'] < data['kkm'] + ? "Remidi" + : "Lulus", + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/views/guru/tugas/bindings/detail_submit_tugas_siswa_binding.dart b/lib/views/guru/tugas/bindings/detail_submit_tugas_siswa_binding.dart new file mode 100644 index 0000000..2d6ed55 --- /dev/null +++ b/lib/views/guru/tugas/bindings/detail_submit_tugas_siswa_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import 'package:ui/views/guru/tugas/controllers/detail_submit_tugas_siswa_controller.dart'; + +class DetailSubmitTugasSiswaBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => DetailSubmitTugasSiswaController()); + } +} diff --git a/lib/views/guru/tugas/bindings/tugas_detail_guru_binding.dart b/lib/views/guru/tugas/bindings/tugas_detail_guru_binding.dart new file mode 100644 index 0000000..70e1243 --- /dev/null +++ b/lib/views/guru/tugas/bindings/tugas_detail_guru_binding.dart @@ -0,0 +1,13 @@ +import 'package:get/get.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/kelas_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart'; +import 'package:ui/views/guru/tugas/controllers/tugas_detail_guru_controller.dart'; + +class TugasDetailGuruBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => KelasController()); + Get.lazyPut(() => TahunAjaranController()); + Get.lazyPut(() => TugasDetailGuruController()); + } +} diff --git a/lib/views/guru/tugas/bindings/tugas_guru_binding.dart b/lib/views/guru/tugas/bindings/tugas_guru_binding.dart new file mode 100644 index 0000000..f8e7639 --- /dev/null +++ b/lib/views/guru/tugas/bindings/tugas_guru_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart'; + +class TugasGuruBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => MataPelajaranSimpleController()); + } +} diff --git a/lib/views/guru/tugas/controllers/detail_submit_tugas_siswa_controller.dart b/lib/views/guru/tugas/controllers/detail_submit_tugas_siswa_controller.dart new file mode 100644 index 0000000..f59386b --- /dev/null +++ b/lib/views/guru/tugas/controllers/detail_submit_tugas_siswa_controller.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/detail_submit_tugas_siswa_mode.dart'; +import 'package:http/http.dart' as http; + +class DetailSubmitTugasSiswaController extends GetxController { + DetailSubmitTugasSiswaModel? detailSubmitTugasSiswaM; + var isLoading = false.obs; + + Future getSubmitTugas( + {required id, + required type, + required kelas, + required tahunAjaran}) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse( + "${ApiConstants.getDetailSubmitTugasSiswaEnpoint}?tugas_id=$id&kelas=$kelas&tahun_ajaran=$tahunAjaran&type_tugas=$type"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + log(json.toString()); + detailSubmitTugasSiswaM = DetailSubmitTugasSiswaModel.fromJson(json); + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + } + } catch (e) { + log("Error get matpel simple: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/guru/tugas/controllers/review_submit_tugas_controller.dart b/lib/views/guru/tugas/controllers/review_submit_tugas_controller.dart new file mode 100644 index 0000000..3d18233 --- /dev/null +++ b/lib/views/guru/tugas/controllers/review_submit_tugas_controller.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/widgets/my_snackbar.dart'; +import 'package:http/http.dart' as http; + +class ReviewSubmitTugasController extends GetxController { + var isLoading = false.obs; + var nilai = 0.0.obs; + var taskName = ''.obs; + var taskId = ''.obs; + + @override + void onInit() { + super.onInit(); + final args = Get.arguments; + taskId.value = args['id_tugas']?.toString() ?? ''; + taskName.value = args['nama']?.toString() ?? 'Tidak ada nama tugas'; + log('Task ID: ${taskId.value}'); + log('Task Name: ${taskName.value}'); + } + + Future updateNilai() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + try { + isLoading(true); + final response = await http.post( + Uri.parse("${ApiConstants.baseUrlApi}/update-nilai"), + headers: headers, + body: jsonEncode({ + 'id': Get.arguments['id'], + 'nilai': nilai.value.toInt(), + }), + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json['status']) { + snackbarSuccess(json['message']); + Get.back(); + } else { + snackbarfailed(json['message']); + } + } else { + snackbarfailed("Terjadi kesalahan saat mengupdate nilai"); + } + } catch (e) { + log("Error update nilai: $e"); + snackbarfailed("Terjadi kesalahan saat mengupdate nilai"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/guru/tugas/controllers/tugas_detail_guru_controller.dart b/lib/views/guru/tugas/controllers/tugas_detail_guru_controller.dart new file mode 100644 index 0000000..313039e --- /dev/null +++ b/lib/views/guru/tugas/controllers/tugas_detail_guru_controller.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/tugas_model.dart'; +import 'package:http/http.dart' as http; + +class TugasDetailGuruController extends GetxController { + TugasModel? tugasM; + var isLoading = false.obs; + var selectedTypeTugas = Rxn(); + var isEmptyData = true.obs; + var isFetchData = false.obs; + + Future getTugas({ + required id, + required kelas, + required tahunAjaran, + }) async { + // var req = { + // 'id': id, + // 'type': type, + // 'kelas': kelas, + // 'tahunAjaran': tahunAjaran, + // }; + // log(req.toString()); + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isFetchData(true); + isLoading(true); + final response = await http.get( + Uri.parse( + "${ApiConstants.tugasEnpoint}?id_matpel=$id&kelas=$kelas&tahun_ajaran=$tahunAjaran"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + tugasM = TugasModel.fromJson(json); + if (tugasM?.data.isEmpty ?? true) { + isEmptyData(true); + } else { + isEmptyData(false); + } + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + } + } catch (e) { + log("Error get matpel simple: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/guru/tugas/detail.dart b/lib/views/guru/tugas/detail.dart new file mode 100644 index 0000000..99fb049 --- /dev/null +++ b/lib/views/guru/tugas/detail.dart @@ -0,0 +1,182 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/kelas_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart'; +import 'package:ui/views/guru/tugas/controllers/tugas_detail_guru_controller.dart'; +import 'package:ui/views/guru/tugas/filter_tugas.dart'; +import 'package:ui/widgets/my_date_format.dart'; +import 'package:ui/widgets/my_text.dart'; + +class DetailTugasGuru extends StatelessWidget { + DetailTugasGuru({super.key}); + KelasController kelasC = Get.find(); + TahunAjaranController tahunAjaranC = Get.find(); + TugasDetailGuruController tugasDetailGuruC = + Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Detail Tugas"), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + FilterTugas( + idMatpel: Get.arguments['id'].toString(), + kelasC: kelasC, + tahunAjaranC: tahunAjaranC, + tugasSubmitDetailGuruC: tugasDetailGuruC), + Obx( + () { + if (tugasDetailGuruC.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } else if (!tugasDetailGuruC.isFetchData.value) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 100), + child: Center( + child: Text( + "Silahkan Filter untuk\nMelihat Data.", + textAlign: TextAlign.center, + )), + ); + } else if (tugasDetailGuruC.isEmptyData.value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 100), + child: Center( + child: Text( + "Data Tugas untuk kelas ${kelasC.selectedKelas.value}\nPada tahun ajaran ${tahunAjaranC.selectedTahun.value}\nDengan Tipe Tugas ${tugasDetailGuruC.selectedTypeTugas.value}\nMasih Kosong", + textAlign: TextAlign.center, + ), + ), + ); + } + return ListView.builder( + shrinkWrap: true, + itemCount: tugasDetailGuruC.tugasM?.data.length ?? 0, + itemBuilder: (context, index) { + var data = tugasDetailGuruC.tugasM!.data[index]; + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(15), + onTap: () async { + Get.toNamed( + AppRoutes.detailSubmitTugasDetailGuru, + arguments: { + "id": data.id, + "kelas": kelasC.selectedKelas.value, + "tahun_ajaran": + tahunAjaranC.selectedTahun.value, + }, + ); + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFF57E389) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.assignment, + color: Color(0xFF57E389), + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + MyText( + text: data.nama, + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + const SizedBox(height: 4), + MyText( + text: + "Dibuat: ${data.tanggal.simpleDateRevers()}", + fontSize: 12, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.timer_outlined, + color: Colors.red, + size: 16, + ), + const SizedBox(width: 4), + MyText( + text: + "Tenggat: ${data.tenggat.simpleDateRevers()}", + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w600, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ) + ], + ), + ), + ); + } +} diff --git a/lib/views/guru/tugas/detail_submit_tugas.dart b/lib/views/guru/tugas/detail_submit_tugas.dart new file mode 100644 index 0000000..93ef56d --- /dev/null +++ b/lib/views/guru/tugas/detail_submit_tugas.dart @@ -0,0 +1,369 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ntp/ntp.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/guru/tugas/controllers/detail_submit_tugas_siswa_controller.dart'; +import 'package:ui/widgets/my_date_format.dart'; +import 'package:ui/widgets/my_snackbar.dart'; +import 'package:ui/widgets/my_text.dart'; + +class DetailSubmitTugas extends StatefulWidget { + const DetailSubmitTugas({super.key}); + + @override + State createState() => _DetailSubmitTugasState(); +} + +class _DetailSubmitTugasState extends State { + // TugasController tugasC = Get.find(); + DetailSubmitTugasSiswaController detailSubTugasSiswaC = + Get.find(); + + var isActive = "selesai"; + DateTime? dateNow; + + @override + void initState() { + super.initState(); + detailSubTugasSiswaC.getSubmitTugas( + id: Get.arguments['id'], + type: "selesai", + kelas: Get.arguments['kelas'], + tahunAjaran: Get.arguments['tahun_ajaran'], + ); + } + + Future getCurrentTime() async { + DateTime now = await NTP.now(); + dateNow = DateTime(now.year, now.month, now.day); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const MyText( + text: "Detail Pengumpulan Tugas", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + backgroundColor: const Color(0xFF57E389), + elevation: 0, + ), + body: Container( + decoration: const BoxDecoration( + color: Color(0xFF57E389), + ), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: buttonTab(), + ), + Expanded( + child: isActive == "selesai" ? tugasSelesai() : tugasBelum(), + ), + ], + ), + ), + ), + ); + } + + Widget buttonTab() { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(30), + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isActive = "selesai"; + }); + detailSubTugasSiswaC.getSubmitTugas( + id: Get.arguments['id'], + type: "selesai", + kelas: Get.arguments['kelas'], + tahunAjaran: Get.arguments['tahun_ajaran'], + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isActive == "selesai" + ? const Color(0xFF57E389) + : Colors.transparent, + borderRadius: BorderRadius.circular(30), + ), + child: Center( + child: MyText( + text: "Selesai", + fontSize: 16, + color: isActive == "selesai" ? Colors.white : Colors.grey, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isActive = "belum"; + }); + detailSubTugasSiswaC.getSubmitTugas( + id: Get.arguments['id'], + type: "belum", + kelas: Get.arguments['kelas'], + tahunAjaran: Get.arguments['tahun_ajaran'], + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isActive == "belum" + ? const Color(0xFF57E389) + : Colors.transparent, + borderRadius: BorderRadius.circular(30), + ), + child: Center( + child: MyText( + text: "Belum", + fontSize: 16, + color: isActive == "belum" ? Colors.white : Colors.grey, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget tugasBelum() { + return Obx(() { + if (detailSubTugasSiswaC.isLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (detailSubTugasSiswaC.detailSubmitTugasSiswaM?.data.isEmpty ?? + true) { + return emptyData(); + } else { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: + detailSubTugasSiswaC.detailSubmitTugasSiswaM?.data.length ?? 0, + itemBuilder: (context, index) { + var data = + detailSubTugasSiswaC.detailSubmitTugasSiswaM?.data[index]; + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.all(16), + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: MyText( + text: "${index + 1}", + fontSize: 16, + color: Colors.red, + fontWeight: FontWeight.w600, + ), + ), + title: MyText( + text: data!.nama, + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + trailing: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const MyText( + text: "Belum", + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w600, + ), + ), + onTap: () async { + snackbarfailed("Siswa ini tidak mengumpulkan tugas"); + }, + ), + ); + }, + ); + } + }); + } + + Widget emptyData() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.assignment_outlined, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + const MyText( + text: "Tidak Ada Siswa", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ); + } + + Widget tugasSelesai() { + return Obx(() { + if (detailSubTugasSiswaC.isLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (detailSubTugasSiswaC.detailSubmitTugasSiswaM?.data.isEmpty ?? + true) { + return emptyData(); + } else { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: + detailSubTugasSiswaC.detailSubmitTugasSiswaM?.data.length ?? 0, + itemBuilder: (context, index) { + var data = + detailSubTugasSiswaC.detailSubmitTugasSiswaM?.data[index]; + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.all(16), + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF57E389).withOpacity(0.1), + shape: BoxShape.circle, + ), + child: MyText( + text: "${index + 1}", + fontSize: 16, + color: const Color(0xFF57E389), + fontWeight: FontWeight.w600, + ), + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText( + text: data!.nama, + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + const SizedBox(height: 4), + MyText( + text: data.submitTugas!.tanggal.simpleDateRevers(), + fontSize: 12, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + trailing: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: data.submitTugas!.nilai != null + ? const Color(0xFF57E389).withOpacity(0.1) + : Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: MyText( + text: data.submitTugas!.nilai != null + ? "Nilai: ${data.submitTugas!.nilai}" + : "Belum dinilai", + fontSize: 12, + color: data.submitTugas!.nilai != null + ? const Color(0xFF57E389) + : Colors.orange, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + Get.toNamed( + AppRoutes.reviewSubmitTugasSiswaOnGuru, + arguments: { + 'id': data.submitTugas!.id, + 'id_tugas': data.submitTugas!.tugasId, + 'nama': data.submitTugas!.tugas.nama, + 'text': data.submitTugas!.text, + 'file': data.submitTugas!.file, + 'tanggal_pengumpulan': data.submitTugas!.tanggal, + 'nisn': data.nisn, + }, + ); + }, + ), + ); + }, + ); + } + }); + } +} diff --git a/lib/views/guru/tugas/filter_tugas.dart b/lib/views/guru/tugas/filter_tugas.dart new file mode 100644 index 0000000..e5bfda2 --- /dev/null +++ b/lib/views/guru/tugas/filter_tugas.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/kelas_controller.dart'; +import 'package:ui/views/guru/mata_pelajaran/controllers/tahun_ajaran_controller.dart'; +import 'package:ui/views/guru/tugas/controllers/tugas_detail_guru_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class FilterTugas extends StatelessWidget { + const FilterTugas({ + super.key, + required this.idMatpel, + required this.kelasC, + required this.tahunAjaranC, + required this.tugasSubmitDetailGuruC, + }); + + final String idMatpel; + final KelasController kelasC; + final TahunAjaranController tahunAjaranC; + final TugasDetailGuruController tugasSubmitDetailGuruC; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MyText( + text: "Filter Tugas", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Obx(() { + if (kelasC.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (kelasC.kelasM?.data == null || + kelasC.kelasM!.data.isEmpty) { + return const Text('Tidak ada data kelas'); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: DropdownButtonFormField( + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 8), + isDense: true, + ), + icon: const Icon(Icons.arrow_drop_down, + color: Colors.grey, size: 20), + dropdownColor: Colors.white, + isExpanded: true, + value: kelasC.selectedKelas.value, + items: kelasC.kelasM!.data.map((kelas) { + return DropdownMenuItem( + value: kelas.nama, + child: Text( + kelas.nama, + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + onChanged: (value) { + kelasC.selectedKelas.value = value; + }, + ), + ); + }), + ), + const SizedBox(width: 12), + Expanded( + child: Obx(() { + if (tahunAjaranC.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (tahunAjaranC.tahunAjaranM?.data == null || + tahunAjaranC.tahunAjaranM!.data.isEmpty) { + return const Text('Tidak ada data Tahun'); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: DropdownButtonFormField( + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 8), + isDense: true, + ), + icon: const Icon(Icons.arrow_drop_down, + color: Colors.grey, size: 20), + dropdownColor: Colors.white, + isExpanded: true, + value: tahunAjaranC.selectedTahun.value, + items: tahunAjaranC.tahunAjaranM!.data.map((tahun) { + return DropdownMenuItem( + value: tahun.tahun, + child: Text( + tahun.tahun, + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + onChanged: (value) { + tahunAjaranC.selectedTahun.value = value; + }, + ), + ); + }), + ), + const SizedBox(width: 12), + Material( + color: const Color(0xFF57E389), + borderRadius: BorderRadius.circular(12), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () async { + await tugasSubmitDetailGuruC.getTugas( + id: idMatpel, + kelas: kelasC.selectedKelas.value, + tahunAjaran: tahunAjaranC.selectedTahun.value, + ); + }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.filter_list, + color: Colors.white, + size: 24, + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/views/guru/tugas/index.dart b/lib/views/guru/tugas/index.dart new file mode 100644 index 0000000..c7206ab --- /dev/null +++ b/lib/views/guru/tugas/index.dart @@ -0,0 +1,193 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class TugasGuruPage extends StatelessWidget { + TugasGuruPage({super.key}); + MataPelajaranSimpleController matapelajaranSimpleC = + Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const MyText( + text: "Tugas", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + backgroundColor: const Color(0xFF57E389), + elevation: 0, + ), + body: Container( + decoration: const BoxDecoration( + color: Color(0xFF57E389), + ), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MyText( + text: "Pilih Mata Pelajaran", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 20), + Expanded( + child: Obx( + () { + if (matapelajaranSimpleC.isLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (matapelajaranSimpleC + .mataPelajaranSimpleM?.data.isEmpty ?? + true) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.assignment, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + const MyText( + text: "Tidak Ada Mata Pelajaran", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ); + } + return ListView.builder( + itemCount: matapelajaranSimpleC + .mataPelajaranSimpleM?.data.length ?? + 0, + itemBuilder: (context, index) { + var data = matapelajaranSimpleC + .mataPelajaranSimpleM?.data[index]; + return TaskItem( + id: data!.id.toString(), + title: data.nama, + guru: data.guru.nama, + mataPelajaranId: data.id.toString(), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class TaskItem extends StatelessWidget { + final String id; + final String title; + final String guru; + final String mataPelajaranId; + + const TaskItem({ + super.key, + required this.id, + required this.title, + required this.guru, + required this.mataPelajaranId, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + Get.toNamed(AppRoutes.tugasDetailGuru, arguments: { + 'id': id, + }); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFF57E389).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.assignment, + color: Color(0xFF57E389), + size: 30, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText( + text: title, + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + const SizedBox(height: 4), + MyText( + text: "Guru: $guru", + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ), + const Icon( + Icons.chevron_right, + color: Colors.grey, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/guru/tugas/review_submit_tugas.dart b/lib/views/guru/tugas/review_submit_tugas.dart new file mode 100644 index 0000000..e3c9084 --- /dev/null +++ b/lib/views/guru/tugas/review_submit_tugas.dart @@ -0,0 +1,353 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/widgets/my_date_format.dart'; +import 'package:ui/widgets/my_text.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:ui/views/guru/tugas/controllers/review_submit_tugas_controller.dart'; + +class ReviewSubmitTugas extends StatelessWidget { + const ReviewSubmitTugas({super.key}); + + Future _launchUrl(String url) async { + final uri = Uri.parse(url); + if (!await launchUrl( + uri, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration( + enableJavaScript: true, + ), + )) { + throw Exception('Could not launch $url'); + } + } + + @override + Widget build(BuildContext context) { + final controller = Get.put(ReviewSubmitTugasController()); + + // Get arguments with null safety + final args = Get.arguments ?? {}; + final text = args['text']?.toString(); + final file = args['file']?.toString(); + final tanggalPengumpulan = args['tanggal_pengumpulan']?.toString(); + final id = args['id']?.toString(); + + return Scaffold( + appBar: AppBar( + title: const MyText( + text: "Review Tugas Siswa", + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + backgroundColor: const Color(0xFF57E389), + elevation: 0, + ), + body: Container( + decoration: const BoxDecoration( + color: Color(0xFF57E389), + ), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: Get.width, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF57E389).withOpacity(0.1), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MyText( + text: "Informasi Tugas", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 12), + Obx(() { + if (controller.isLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF57E389).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.assignment, + color: Color(0xFF57E389), + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText( + text: controller.taskName.value, + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + const SizedBox(height: 4), + MyText( + text: "ID: ${controller.taskId.value}", + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ], + ), + ), + ], + ); + }), + ], + ), + ), + const SizedBox(height: 20), + Container( + width: Get.width, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MyText( + text: "Jawaban Siswa", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 12), + if (text == null && file != null) + InkWell( + onTap: () { + _launchUrl("${ApiConstants.baseUrl}/storage/$file"); + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon( + Icons.file_present, + color: Colors.blue, + ), + const SizedBox(width: 12), + const MyText( + text: "Lihat File", + fontSize: 16, + color: Colors.blue, + fontWeight: FontWeight.w600, + ), + const Spacer(), + const Icon( + Icons.arrow_forward_ios, + color: Colors.blue, + size: 16, + ), + ], + ), + ), + ), + if (file == null && text != null) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: MyText( + text: text, + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + Container( + width: Get.width, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MyText( + text: "Informasi Pengumpulan", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 12), + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF57E389).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.calendar_today, + color: Color(0xFF57E389), + size: 20, + ), + ), + const SizedBox(width: 12), + MyText( + text: tanggalPengumpulan != null + ? DateTime.parse(tanggalPengumpulan) + .simpleDateRevers() + : 'Tanggal tidak tersedia', + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + Container( + width: Get.width, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const MyText( + text: "Nilai", + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + const SizedBox(height: 16), + Obx(() => Slider( + value: controller.nilai.value.toDouble(), + min: 0, + max: 100, + divisions: 100, + activeColor: const Color(0xFF57E389), + inactiveColor: + const Color(0xFF57E389).withOpacity(0.2), + label: controller.nilai.value.toString(), + onChanged: (value) { + controller.nilai.value = value; + }, + )), + const SizedBox(height: 8), + Obx(() => Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: const Color(0xFF57E389).withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: MyText( + text: "Nilai: ${controller.nilai.value}", + fontSize: 16, + color: const Color(0xFF57E389), + fontWeight: FontWeight.w600, + ), + )), + const SizedBox(height: 20), + SizedBox( + width: Get.width, + child: ElevatedButton( + onPressed: controller.isLoading.value || id == null + ? null + : () { + controller.updateNilai(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF57E389), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Obx(() => controller.isLoading.value + ? const CircularProgressIndicator( + color: Colors.white) + : const MyText( + text: "Simpan Nilai", + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w600, + )), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/siswa/bindings/notifikasi_binding.dart b/lib/views/siswa/bindings/notifikasi_binding.dart new file mode 100644 index 0000000..6338b6a --- /dev/null +++ b/lib/views/siswa/bindings/notifikasi_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/controllers/notifikasi_controller.dart'; + +class NotifikasiBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => NotifikasiController()); + } +} diff --git a/lib/views/siswa/bindings/siswa_binding.dart b/lib/views/siswa/bindings/siswa_binding.dart new file mode 100644 index 0000000..beb7658 --- /dev/null +++ b/lib/views/siswa/bindings/siswa_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/controllers/notifikasi_count_controller.dart'; +import 'package:ui/views/siswa/controllers/siswa_controller.dart'; + +class SiswaBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SiswaController()); + Get.lazyPut(() => NotifikasiCountController()); + } +} diff --git a/lib/views/siswa/bindings/ubah_password_binding.dart b/lib/views/siswa/bindings/ubah_password_binding.dart new file mode 100644 index 0000000..d0ad8e6 --- /dev/null +++ b/lib/views/siswa/bindings/ubah_password_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/controllers/ubah_password_controller.dart'; + +class UbahPasswordBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => UbahPasswordController()); + } +} diff --git a/lib/views/siswa/controllers/notifikasi_controller.dart b/lib/views/siswa/controllers/notifikasi_controller.dart new file mode 100644 index 0000000..43c45b4 --- /dev/null +++ b/lib/views/siswa/controllers/notifikasi_controller.dart @@ -0,0 +1,80 @@ +import 'package:get/get.dart'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; + +class NotifikasiController extends GetxController { + var isLoading = false.obs; + var dataNotif = [].obs; + + @override + void onInit() { + super.onInit(); + getNotif(); + } + + Future getNotif() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse(ApiConstants.notifikasiEnpoit), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + dataNotif.value = data['notifications']; + } else { + log("Terjadi kesalahan get notifikasi: ${response.statusCode}"); + } + } catch (e) { + log("Error Notif: $e"); + } finally { + isLoading(false); + } + } + + Future readNotif({required id}) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.post( + Uri.parse("${ApiConstants.notifikasiEnpoit}/$id/baca"), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + log(data.toString()); + } else { + log("Terjadi kesalahan Read notifikasi: ${response.statusCode}"); + } + } catch (e) { + log("Error Read Notif: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/siswa/controllers/notifikasi_count_controller.dart b/lib/views/siswa/controllers/notifikasi_count_controller.dart new file mode 100644 index 0000000..cce453e --- /dev/null +++ b/lib/views/siswa/controllers/notifikasi_count_controller.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; + +class NotifikasiCountController extends GetxController { + var notifCount = 0.obs; + var isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + getNotifCount(); + } + + Future getNotifCount() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse(ApiConstants.notifikasiCountEnpoit), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + notifCount.value = data['unread_count']; + } else { + log("Terjadi kesalahan get notifikasi: ${response.statusCode}"); + } + } catch (e) { + log("Error Notif: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/siswa/controllers/siswa_controller.dart b/lib/views/siswa/controllers/siswa_controller.dart new file mode 100644 index 0000000..2781d85 --- /dev/null +++ b/lib/views/siswa/controllers/siswa_controller.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/widgets/my_snackbar.dart'; + +class SiswaController extends GetxController { + var isLoading = false.obs; + var isLoadingAnalysis = false.obs; + var dataUser = {}.obs; + var dataAnalysis = {}.obs; + var kekuranganIsEmpty = true.obs; + var kelebihanIsEmpty = true.obs; + + @override + void onInit() { + super.onInit(); + getMe(); + } + + Future getMe() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + // final attemptId = prefs.getString('attempt_id'); + await prefs.remove('attempt_id'); + // log("TOKEN : $token"); + // log("Attempt ID: $attemptId"); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse(ApiConstants.getMeEnpoint), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + dataUser.value = data['data']; + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + } + } catch (e) { + log("Error getMe: $e"); + } finally { + isLoading(false); + } + } + + Future logout({required role}) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + var headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + if (token == null) { + throw Exception("Token not found"); + } + + http.Response response = await http.post( + Uri.parse(ApiConstants.logoutEnpoint), + headers: headers, + ); + + if (response.statusCode == 200) { + await prefs.remove('nama'); + await prefs.remove('token'); + await prefs.remove('role'); + await prefs.remove('nisn'); + await prefs.remove('nip'); + await prefs.clear(); + + if (role == "siswa") { + Get.offAllNamed(AppRoutes.login, arguments: "siswa"); + } else { + Get.offAllNamed(AppRoutes.login, arguments: "guru"); + } + + snackbarSuccess("Berhasil Logout"); + } else { + log(response.body.toString()); + } + } catch (e) { + log(e.toString()); + } + } + + Future getAnalysis() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoadingAnalysis(true); + final response = await http.get( + Uri.parse(ApiConstants.analysisSiswaEnpoint), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + dataAnalysis.value = data; + if (dataAnalysis['kelebihan'].length != 0) { + kelebihanIsEmpty(false); + } else { + kelebihanIsEmpty(true); + } + if (dataAnalysis['kekurangan'].length != 0) { + kekuranganIsEmpty(false); + } else { + kekuranganIsEmpty(true); + } + log(dataAnalysis.toString()); + } else { + log("Terjadi kesalahan get data analysis: ${response.statusCode}"); + } + } catch (e) { + log("Error anlysis: $e"); + } finally { + isLoadingAnalysis(false); + } + } +} diff --git a/lib/views/siswa/controllers/ubah_password_controller.dart b/lib/views/siswa/controllers/ubah_password_controller.dart new file mode 100644 index 0000000..35c6074 --- /dev/null +++ b/lib/views/siswa/controllers/ubah_password_controller.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/widgets/my_snackbar.dart'; + +class UbahPasswordController extends GetxController { + var isLoading = false.obs; + final oldPasswordC = TextEditingController().obs; + final newPasswordC = TextEditingController().obs; + final confirmPasswordC = TextEditingController().obs; + + Future ubahPassword() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + try { + isLoading(true); + Map body = { + 'old_password': oldPasswordC.value.text, + 'new_password': newPasswordC.value.text, + 'new_password_confirmation': confirmPasswordC.value.text, + }; + log(body.toString()); + final response = await http.post( + Uri.parse(ApiConstants.ubahPasswordEnpoint), + headers: headers, + body: jsonEncode(body), + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json['status'] == false) { + snackbarAlert("Gagal", json['message'], Colors.red.shade800); + return; + } else { + Get.back(); + snackbarAlert("Berhasil", json['message'], Colors.green.shade800); + } + } else { + log("Terjadi kesalahan ubah password : ${response.statusCode}"); + snackbarAlert( + "Berhasil", + "Terjadi kesalahan ubah password : ${response.statusCode}", + Colors.green.shade800); + } + } catch (e) { + log("Error update password simple: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/siswa/matapelajaran/bindings/mata_pelajaran_binding.dart b/lib/views/siswa/matapelajaran/bindings/mata_pelajaran_binding.dart new file mode 100644 index 0000000..f257ef3 --- /dev/null +++ b/lib/views/siswa/matapelajaran/bindings/mata_pelajaran_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_controller.dart'; + +class MataPelajaranBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => MataPelajaranController()); + } +} diff --git a/lib/views/siswa/matapelajaran/controllers/mata_pelajaran_controller.dart b/lib/views/siswa/matapelajaran/controllers/mata_pelajaran_controller.dart new file mode 100644 index 0000000..de5ed78 --- /dev/null +++ b/lib/views/siswa/matapelajaran/controllers/mata_pelajaran_controller.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/mata_pelajaran_model.dart'; + +class MataPelajaranController extends GetxController { + var isLoading = false.obs; + MataPelajaranModel? mataPelajaranM; + var isEmptyData = true.obs; + + @override + void onInit() { + super.onInit(); + getMatPel(); + } + + Future getMatPel() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse(ApiConstants.mataPelajaranEnpoint), + headers: headers, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + mataPelajaranM = MataPelajaranModel.fromJson(data); + if (mataPelajaranM!.data.isEmpty) { + isEmptyData(true); + } else { + isEmptyData(false); + } + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + } + } catch (e) { + log("Error getMe: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart b/lib/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart new file mode 100644 index 0000000..47017d1 --- /dev/null +++ b/lib/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/mata_pelajaran_simple_model.dart'; + +class MataPelajaranSimpleController extends GetxController { + var isLoading = false.obs; + MataPelajaranSimpleModel? mataPelajaranSimpleM; + var isEmptyData = true.obs; + + @override + void onInit() { + super.onInit(); + getMatPelSimple(); + } + + Future getMatPelSimple() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse(ApiConstants.mataPelajaranSimpleEnpoint), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + mataPelajaranSimpleM = MataPelajaranSimpleModel.fromJson(json); + if (mataPelajaranSimpleM!.data.isEmpty) { + isEmptyData(true); + } else { + isEmptyData(false); + } + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + } + } catch (e) { + log("Error get matpel simple: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/siswa/matapelajaran/mata_pelajaran.dart b/lib/views/siswa/matapelajaran/mata_pelajaran.dart new file mode 100644 index 0000000..0c96121 --- /dev/null +++ b/lib/views/siswa/matapelajaran/mata_pelajaran.dart @@ -0,0 +1,321 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/constans/constansts_export.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_controller.dart'; +import 'package:ui/widgets/my_date_format.dart'; + +class KelasMataPelajaranPage extends StatelessWidget { + KelasMataPelajaranPage({super.key}); + + MataPelajaranController mataPelajaranC = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + "Mata Pelajaran", + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppTheme.neutralWhite, + ), + ), + backgroundColor: AppTheme.primaryGreen, + elevation: 0, + ), + body: Column( + children: [ + // Header Section with green background + Container( + padding: const EdgeInsets.only( + left: 20, + right: 20, + bottom: 30, + ), + decoration: const BoxDecoration( + gradient: AppTheme.greenGradient, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + ), + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Pilih Mata Pelajaran", + style: TextStyle( + color: AppTheme.neutralWhite, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 5), + Text( + "Kumpulan mata pelajaran yang dapat kamu akses", + style: TextStyle( + color: AppTheme.neutralWhite.withOpacity(0.8), + fontSize: 14, + ), + ), + ], + ), + ), + ), + + // List of Subjects + Expanded( + child: Obx(() { + if (mataPelajaranC.isLoading.value) { + return const Center( + child: CircularProgressIndicator( + color: AppTheme.primaryGreen, + ), + ); + } + + if (mataPelajaranC.mataPelajaranM == null || + mataPelajaranC.mataPelajaranM!.data.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.book_outlined, + size: 80, + color: AppTheme.neutralDarkGrey.withOpacity(0.5), + ), + const SizedBox(height: 16), + const Text( + "Tidak ada mata pelajaran", + style: TextStyle( + fontSize: 16, + color: AppTheme.neutralDarkGrey, + ), + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + itemCount: mataPelajaranC.mataPelajaranM!.data.length, + itemBuilder: (context, index) { + final data = mataPelajaranC.mataPelajaranM!.data; + // Alternate card colors for visual appeal + final bool isEven = index % 2 == 0; + + return GestureDetector( + onTap: () => Get.toNamed( + AppRoutes.materiSiswa, + arguments: data[index].id + ), + child: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + gradient: isEven + ? AppTheme.greenGradient + : null, + color: isEven ? null : AppTheme.neutralWhite, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Subject Header + Container( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Subject Icon + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isEven + ? AppTheme.neutralWhite.withOpacity(0.2) + : AppTheme.primaryGreenLight.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.book, + color: isEven + ? AppTheme.neutralWhite + : AppTheme.primaryGreen, + size: 28, + ), + ), + const SizedBox(width: 16), + // Subject Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data[index].nama, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isEven + ? AppTheme.neutralWhite + : AppTheme.neutralBlack, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.person, + size: 16, + color: isEven + ? AppTheme.neutralWhite.withOpacity(0.8) + : AppTheme.neutralDarkGrey, + ), + const SizedBox(width: 4), + Text( + data[index].guru.nama, + style: TextStyle( + color: isEven + ? AppTheme.neutralWhite.withOpacity(0.8) + : AppTheme.neutralDarkGrey, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + + // Subject Footer + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isEven + ? AppTheme.neutralWhite.withOpacity(0.1) + : AppTheme.neutralGrey.withOpacity(0.3), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Material Count + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppTheme.accentOrange, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Icon( + Icons.book_outlined, + color: Colors.white, + size: 14, + ), + const SizedBox(width: 4), + Text( + "${data[index].jumlahBuku}", + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + // Video Count + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppTheme.accentBlue, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Icon( + Icons.video_library, + color: Colors.white, + size: 14, + ), + const SizedBox(width: 4), + Text( + "${data[index].jumlahVideo}", + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + // Updated Date + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "Diperbarui:", + style: TextStyle( + fontSize: 10, + color: isEven + ? AppTheme.neutralWhite.withOpacity(0.8) + : AppTheme.neutralDarkGrey, + ), + ), + Text( + data[index].materi.isNotEmpty + ? data[index].materi.last.tanggal.getSimpleDayAndDate() + : data[index].createdAt.getSimpleDayAndDate(), + style: TextStyle( + fontWeight: FontWeight.w500, + color: isEven + ? AppTheme.neutralWhite + : AppTheme.neutralBlack, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + }), + ), + ], + ), + ); + } +} diff --git a/lib/views/siswa/materi/bindings/materi_binding.dart b/lib/views/siswa/materi/bindings/materi_binding.dart new file mode 100644 index 0000000..3eba3cb --- /dev/null +++ b/lib/views/siswa/materi/bindings/materi_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/materi/controllers/materi_controller.dart'; + +class MateriBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => MateriController()); + } +} diff --git a/lib/views/siswa/materi/controllers/materi_controller.dart b/lib/views/siswa/materi/controllers/materi_controller.dart new file mode 100644 index 0000000..392152a --- /dev/null +++ b/lib/views/siswa/materi/controllers/materi_controller.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/materi_buku_model.dart'; +import 'package:ui/models/materi_video_model.dart'; +import 'package:http/http.dart' as http; +import 'package:ui/widgets/my_snackbar.dart'; + +class MateriController extends GetxController { + var isMateriLoaded = false.obs; + var isVideoLoaded = false.obs; + MateriBukuModel? materiBuku; + MateriVideoModel? materiVideo; + var isLoadingBuku = false.obs; + var isLoadingVideo = false.obs; + + Future getMateriBuku({required idMatpel, required semester}) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + log(semester.toString()); + + try { + isLoadingBuku(true); + final response = await http.get( + Uri.parse( + "${ApiConstants.getMateriEnpoint}?id_matpel=$idMatpel&semester=$semester&type=buku"), + headers: headers, + ); + + final json = jsonDecode(response.body); + log(json.toString()); + if (response.statusCode == 200) { + materiBuku = MateriBukuModel.fromJson(json); + isMateriLoaded(true); + } else { + log("terjadi kesalahan get data materi Buku : ${response.statusCode}"); + } + } catch (e) { + log(e.toString()); + } finally { + isLoadingBuku(false); + } + } + + Future getMateriVideo({required idMatpel, required semester}) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + try { + isLoadingVideo(true); + final response = await http.get( + Uri.parse( + "${ApiConstants.getMateriEnpoint}?id_matpel=$idMatpel&semester=$semester&type=video"), + headers: headers, + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + materiVideo = MateriVideoModel.fromJson(json); + isVideoLoaded(true); + } else { + log("terjadi kesalahan get data materi Video : ${response.statusCode}"); + } + } catch (e) { + log(e.toString()); + } finally { + isLoadingVideo(false); + } + } + + Future downloadPdfWithHttp( + String filePath, String? judul, String? smt) async { + try { + // premision + if (Platform.isAndroid) { + var status = await Permission.manageExternalStorage.request(); + if (!status.isGranted) { + throw Exception("Izin ditolak"); + } + } + + final fullUrl = "${ApiConstants.baseUrl}/storage/$filePath"; + log(fullUrl.toString()); + + final response = await http.get(Uri.parse(fullUrl)); + if (response.statusCode == 200) { + // Simpan ke folder Downloads + final downloadsDir = Directory('/storage/emulated/0/Download'); + if (!await downloadsDir.exists()) { + await downloadsDir.create(recursive: true); + } + + final filename = judul != null + ? "${judul}_semester_$smt.pdf" + : filePath.split('/').last; + final file = File('${downloadsDir.path}/$filename'); + await file.writeAsBytes(response.bodyBytes); + log("File disimpan di: ${file.path}"); + snackbarAlert("Download...", "File berhasil disimpan sebagai $filename", + const Color(0xFF3C4D55)); + } else { + log("Gagal download: ${response.statusCode}"); + } + } catch (e) { + log("Terjadi error: $e"); + } + } +} diff --git a/lib/views/siswa/materi/index.dart b/lib/views/siswa/materi/index.dart new file mode 100644 index 0000000..01fcb9e --- /dev/null +++ b/lib/views/siswa/materi/index.dart @@ -0,0 +1,587 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/constans/constansts_export.dart'; +import 'package:ui/views/siswa/materi/controllers/materi_controller.dart'; +import 'package:ui/widgets/my_date_format.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MateriView extends StatefulWidget { + const MateriView({super.key}); + + @override + State createState() => _MateriViewState(); +} + +class _MateriViewState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + MateriController materiC = Get.find(); + + String? selectedSemester = "1"; + String? selectedSemesterVideo = "1"; + final List semesterList = ['1', '2']; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + log(Get.arguments.toString()); + + _tabController.addListener(() { + if (_tabController.indexIsChanging) return; + + if (_tabController.index == 0 && !materiC.isMateriLoaded.value) { + materiC.getMateriBuku( + idMatpel: Get.arguments, semester: selectedSemester); + log("Materi"); + } else if (_tabController.index == 1 && !materiC.isVideoLoaded.value) { + log("VIdeo"); + materiC.getMateriVideo( + idMatpel: Get.arguments, semester: selectedSemesterVideo); + } + }); + + materiC.getMateriBuku(idMatpel: Get.arguments, semester: selectedSemester); + } + + Future _launchUrl(String url) async { + final uri = Uri.parse(url); + if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { + throw Exception('Could not launch $url'); + } + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppTheme.neutralGrey.withOpacity(0.3), + appBar: AppBar( + title: const Text( + 'Materi & Video', + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppTheme.neutralWhite, + ), + ), + backgroundColor: AppTheme.primaryGreen, + elevation: 0, + bottom: TabBar( + controller: _tabController, + indicator: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: AppTheme.primaryGreenDark, + ), + labelColor: AppTheme.neutralWhite, + unselectedLabelColor: AppTheme.neutralWhite.withOpacity(0.7), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + tabs: const [ + Tab( + text: 'Materi', + icon: Icon(Icons.book, size: 20), + ), + Tab( + text: 'Video', + icon: Icon(Icons.video_library, size: 20), + ), + ], + ), + ), + body: Obx( + () => TabBarView( + controller: _tabController, + children: [ + // Tab Materi + Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + // Semester Selector with Custom Style + Container( + margin: const EdgeInsets.only(top: 20, bottom: 16), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppTheme.neutralWhite, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: DropdownButtonFormField( + decoration: InputDecoration( + labelText: 'Pilih Semester', + labelStyle: const TextStyle( + color: AppTheme.primaryGreen, + fontWeight: FontWeight.w500, + ), + prefixIcon: const Icon( + Icons.calendar_today, + color: AppTheme.primaryGreen, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + filled: true, + fillColor: AppTheme.neutralWhite, + ), + dropdownColor: AppTheme.neutralWhite, + value: selectedSemester ?? "1", + items: semesterList.map((semester) { + return DropdownMenuItem( + value: semester, + child: Text( + 'Semester $semester', + style: const TextStyle( + color: AppTheme.neutralBlack, + ), + ), + ); + }).toList(), + onChanged: (value) { + setState(() { + selectedSemester = value!; + }); + materiC.getMateriBuku( + idMatpel: Get.arguments, + semester: selectedSemester); + }, + ), + ), + + // Materials List + if (materiC.isLoadingBuku.value) + const Expanded( + child: Center( + child: CircularProgressIndicator( + color: AppTheme.primaryGreen, + ), + ), + ) + else if (materiC.materiBuku == null || + materiC.materiBuku!.data.isEmpty) + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.book_outlined, + size: 80, + color: AppTheme.neutralDarkGrey.withOpacity(0.5), + ), + const SizedBox(height: 16), + const Text( + 'Tidak ada materi', + style: TextStyle( + fontSize: 16, + color: AppTheme.neutralDarkGrey, + ), + ), + ], + ), + ), + ) + else + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 20), + itemCount: materiC.materiBuku?.data.length ?? 0, + itemBuilder: (context, index) { + final materi = materiC.materiBuku!.data; + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: AppTheme.neutralWhite, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Material Header + Container( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // Document Icon + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.primaryGreenLight + .withOpacity(0.1), + borderRadius: + BorderRadius.circular(10), + ), + child: const Icon( + Icons.description, + color: AppTheme.primaryGreen, + size: 28, + ), + ), + const SizedBox(width: 16), + // Material Title and Date + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + materi[index].judulMateri, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.neutralBlack, + ), + ), + const SizedBox(height: 6), + Row( + children: [ + const Icon( + Icons.calendar_month, + size: 14, + color: + AppTheme.neutralDarkGrey, + ), + const SizedBox(width: 4), + Text( + materi[index] + .tanggal + .fullDateTime(), + style: const TextStyle( + fontSize: 12, + color: AppTheme + .neutralDarkGrey, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + + // Download Button + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: const BoxDecoration( + color: Color(0xFFF5F7FA), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: ElevatedButton.icon( + onPressed: () async { + await materiC.downloadPdfWithHttp( + materi[index].path, + materi[index].judulMateri, + materi[index].semester, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryGreen, + foregroundColor: AppTheme.neutralWhite, + padding: const EdgeInsets.symmetric( + vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.download, size: 18), + label: const Text( + "Download Materi", + style: TextStyle( + fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + + // Tab Video + Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + // Semester Selector with Custom Style + Container( + margin: const EdgeInsets.only(top: 20, bottom: 16), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppTheme.neutralWhite, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: DropdownButtonFormField( + decoration: InputDecoration( + labelText: 'Pilih Semester', + labelStyle: const TextStyle( + color: AppTheme.primaryGreen, + fontWeight: FontWeight.w500, + ), + prefixIcon: const Icon( + Icons.calendar_today, + color: AppTheme.primaryGreen, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + filled: true, + fillColor: AppTheme.neutralWhite, + ), + dropdownColor: AppTheme.neutralWhite, + value: selectedSemesterVideo ?? "1", + items: semesterList.map((semester) { + return DropdownMenuItem( + value: semester, + child: Text( + 'Semester $semester', + style: const TextStyle( + color: AppTheme.neutralBlack, + ), + ), + ); + }).toList(), + onChanged: (value) { + setState(() { + selectedSemesterVideo = value!; + }); + materiC.getMateriVideo( + idMatpel: Get.arguments, + semester: selectedSemesterVideo); + }, + ), + ), + + // Video List + if (materiC.isLoadingVideo.value) + const Expanded( + child: Center( + child: CircularProgressIndicator( + color: AppTheme.primaryGreen, + ), + ), + ) + else if (materiC.materiVideo == null || + materiC.materiVideo!.data.isEmpty) + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.videocam_off, + size: 80, + color: AppTheme.neutralDarkGrey.withOpacity(0.5), + ), + const SizedBox(height: 16), + const Text( + 'Tidak ada materi video', + style: TextStyle( + fontSize: 16, + color: AppTheme.neutralDarkGrey, + ), + ), + ], + ), + ), + ) + else + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 20), + itemCount: materiC.materiVideo?.data.length ?? 0, + itemBuilder: (context, index) { + final video = materiC.materiVideo!.data[index]; + final videoId = + Uri.parse(video.path).queryParameters['v']; + final thumbnailUrl = + 'https://img.youtube.com/vi/$videoId/0.jpg'; + + return Container( + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: AppTheme.neutralWhite, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Video Thumbnail with Play Button + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: Stack( + children: [ + // Thumbnail + Container( + width: double.infinity, + height: 180, + decoration: BoxDecoration( + image: DecorationImage( + image: NetworkImage(thumbnailUrl), + fit: BoxFit.cover, + ), + ), + ), + // Play Button Overlay + Container( + width: double.infinity, + height: 180, + color: Colors.black.withOpacity(0.2), + child: Center( + child: InkWell( + onTap: () => _launchUrl(video.path), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.accentOrange, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black + .withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + Icons.play_arrow, + color: Colors.white, + size: 36, + ), + ), + ), + ), + ), + ], + ), + ), + + // Video Title and Description + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + video.judulMateri, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.neutralBlack, + ), + ), + const SizedBox(height: 6), + Row( + children: [ + const Icon(Icons.calendar_today, + size: 14, + color: AppTheme.neutralDarkGrey), + const SizedBox(width: 4), + Text( + video.tanggal.fullDateTime(), + style: const TextStyle( + fontSize: 12, + color: + AppTheme.neutralDarkGrey), + ), + ], + ), + ], + ), + ), + + // Watch Button + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: const BoxDecoration( + color: Color(0xFFF5F7FA), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: TextButton.icon( + onPressed: () => _launchUrl(video.path), + style: TextButton.styleFrom( + foregroundColor: AppTheme.accentBlue, + padding: const EdgeInsets.symmetric( + vertical: 12), + ), + icon: const Icon(Icons.play_circle_outline), + label: const Text( + "Tonton di YouTube", + style: TextStyle( + fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/siswa/notifikasi.dart b/lib/views/siswa/notifikasi.dart new file mode 100644 index 0000000..0e7fa6c --- /dev/null +++ b/lib/views/siswa/notifikasi.dart @@ -0,0 +1,352 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/controllers/notifikasi_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class NotifSiswa extends StatelessWidget { + NotifSiswa({super.key}); + final notifC = Get.find(); + + aksi(type, {String? matapelajaranId, String? matapelajaranNama}) { + // Jika id atau nama matapelajaran kosong/null, fallback ke daftar sesuai kategori + if (matapelajaranId != null && + matapelajaranId.isNotEmpty && + matapelajaranNama != null && + matapelajaranNama.isNotEmpty) { + switch (type) { + case "Tugas": + Get.toNamed(AppRoutes.tugasDetailSiswa, arguments: matapelajaranId); + return; + case "Quiz": + Get.toNamed(AppRoutes.matpelQuizDetail, arguments: { + 'matpel_id': matapelajaranId, + 'matpel': matapelajaranNama, + }); + return; + case "Materi": + Get.toNamed(AppRoutes.materiSiswa, arguments: matapelajaranId); + return; + } + } else { + // Fallback jika id/nama matapelajaran kosong/null + switch (type) { + case "Tugas": + Get.toNamed(AppRoutes.tugasSiswa); + return; + case "Quiz": + Get.toNamed(AppRoutes.matpelQuiz); + return; + case "Materi": + Get.toNamed(AppRoutes.materiSiswa); + return; + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: AppBar( + title: const Text( + "Notifikasi", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Color(0xFF1E293B), + ), + ), + backgroundColor: Colors.white, + elevation: 0, + iconTheme: const IconThemeData(color: Color(0xFF1E293B)), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Obx(() { + if (notifC.isLoading.value) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFF3B82F6)), + ), + ); + } + + if (notifC.dataNotif.isEmpty) { + return _buildEmptyState(); + } + + return ListView.builder( + itemCount: notifC.dataNotif.length, + itemBuilder: (context, index) { + var data = notifC.dataNotif[index]; + print('NOTIF DATA: ' + data.toString()); // Log data notifikasi + return _buildNotificationCard( + id: (data['id'] ?? '').toString(), + type: data['type'] ?? '', + judul: data['judul'] ?? '', + isActive: data['is_active'] ?? false, + waktu: data['created_at'] ?? '', + matapelajaranId: data['matapelajaran_id']?.toString() ?? '', + matapelajaranNama: data['matapelajaran_nama']?.toString() ?? '', + index: index, + ); + }, + ); + }), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.notifications_off_outlined, + size: 80, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + "Belum ada notifikasi", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + "Notifikasi akan muncul di sini", + style: TextStyle( + fontSize: 14, + color: Colors.grey[500], + ), + ), + ], + ), + ); + } + + Widget _buildNotificationCard({ + required String id, + required String type, + required String judul, + required bool isActive, + required String waktu, + String matapelajaranId = '', + String matapelajaranNama = '', + required int index, + }) { + // Animation delay based on index + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300 + (index * 100)), + builder: (context, value, child) { + return Transform.translate( + offset: Offset(0, 20 * (1 - value)), + child: Opacity( + opacity: value, + child: child, + ), + ); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 12), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + notifC.readNotif(id: id); + aksi(type, + matapelajaranId: matapelajaranId, + matapelajaranNama: matapelajaranNama); + }, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: isActive + ? Border.all( + color: _getTypeColor(type).withOpacity(0.3), width: 2) + : null, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // Icon Container + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: _getTypeColor(type).withOpacity(0.1), + borderRadius: BorderRadius.circular(14), + ), + child: Icon( + _getTypeIcon(type), + color: _getTypeColor(type), + size: 28, + ), + ), + const SizedBox(width: 16), + + // Content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Badge and Title Row + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: _getTypeColor(type), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + type, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 8), + if (isActive) + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Color(0xFF3B82F6), + shape: BoxShape.circle, + ), + ), + ], + ), + const SizedBox(height: 8), + + // Title + Text( + judul, + style: TextStyle( + fontSize: 16, + fontWeight: + isActive ? FontWeight.w700 : FontWeight.w600, + color: const Color(0xFF1E293B), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (matapelajaranNama != null && + matapelajaranNama.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + matapelajaranNama, + style: const TextStyle( + fontSize: 12, color: Colors.grey), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + const SizedBox(height: 4), + + // Time + Row( + children: [ + Icon( + Icons.access_time, + size: 14, + color: Colors.grey[500], + ), + const SizedBox(width: 4), + Text( + _formatTime(waktu), + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ), + + // Arrow Icon + Icon( + Icons.chevron_right, + color: Colors.grey[400], + size: 20, + ), + ], + ), + ), + ), + ), + ), + ); + } + + Color _getTypeColor(String type) { + switch (type) { + case "Quiz": + return const Color(0xFFEF4444); // Red + case "Materi": + return const Color(0xFF10B981); // Green + case "Tugas": + return const Color(0xFFF59E0B); // Amber + default: + return const Color(0xFF6B7280); // Gray + } + } + + IconData _getTypeIcon(String type) { + switch (type) { + case "Quiz": + return Icons.quiz_outlined; + case "Materi": + return Icons.menu_book_outlined; + case "Tugas": + return Icons.assignment_outlined; + default: + return Icons.notifications_outlined; + } + } + + String _formatTime(String waktu) { + // Simple time formatting - you can enhance this based on your needs + try { + final DateTime dateTime = DateTime.parse(waktu); + final DateTime now = DateTime.now(); + final Duration difference = now.difference(dateTime); + + if (difference.inDays > 0) { + return '${difference.inDays} hari lalu'; + } else if (difference.inHours > 0) { + return '${difference.inHours} jam lalu'; + } else if (difference.inMinutes > 0) { + return '${difference.inMinutes} menit lalu'; + } else { + return 'Baru saja'; + } + } catch (e) { + return waktu; + } + } +} diff --git a/lib/views/siswa/profile.dart b/lib/views/siswa/profile.dart new file mode 100644 index 0000000..a81b20a --- /dev/null +++ b/lib/views/siswa/profile.dart @@ -0,0 +1,708 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/controllers/siswa_controller.dart'; + +class ProfileSiswa extends StatelessWidget { + ProfileSiswa({super.key}); + SiswaController siswaC = Get.find(); + + @override + Widget build(BuildContext context) { + siswaC.getAnalysis(); + final size = MediaQuery.of(context).size; + final isTablet = size.width > 600; + + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: AppBar( + title: const Text( + 'Profil Siswa', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + centerTitle: true, + elevation: 0, + backgroundColor: Colors.transparent, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + ), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: () => Get.back(), + ), + ), + body: RefreshIndicator( + onRefresh: () async { + await siswaC.getAnalysis(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + // Header profil dengan gradient + _buildProfileHeader(isTablet), + + // Konten utama + Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? size.width * 0.1 : 20, vertical: 20), + child: Column( + children: [ + // Card untuk konten analisis + _buildAnalysisCard(context, isTablet), + + SizedBox(height: isTablet ? 32 : 24), + + // Tombol-tombol aksi + _buildActionButtons(context, isTablet), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildProfileHeader(bool isTablet) { + return Container( + padding: EdgeInsets.symmetric( + vertical: isTablet ? 40 : 30, horizontal: isTablet ? 40 : 20), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + boxShadow: [ + BoxShadow( + color: Color(0xFF667EEA), + blurRadius: 20, + offset: Offset(0, 10), + ), + ], + ), + child: Obx( + () => Column( + children: [ + // Avatar dengan animasi + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 3, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: CircleAvatar( + radius: isTablet ? 70 : 60, + backgroundColor: Colors.white, + child: Icon( + Icons.person, + size: isTablet ? 70 : 60, + color: const Color(0xFF667EEA), + ), + ), + ), + const SizedBox(height: 20), + Text( + siswaC.dataUser['user']['nama'], + style: TextStyle( + fontSize: isTablet ? 28 : 24, + fontWeight: FontWeight.w700, + color: Colors.white, + fontFamily: 'Poppins', + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: Text( + 'Kelas ${siswaC.dataUser['user']['kelas']}', + style: TextStyle( + fontSize: isTablet ? 18 : 16, + color: Colors.white, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + ), + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.email_outlined, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), + Text( + siswaC.dataUser['user']['user']['email'].toString(), + style: TextStyle( + fontSize: isTablet ? 16 : 14, + color: Colors.white, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildAnalysisCard(BuildContext context, bool isTablet) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(isTablet ? 24 : 20), + child: Obx( + () { + if (siswaC.isLoadingAnalysis.value) { + return Container( + height: 200, + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF6366F1), + ), + strokeWidth: 3, + ), + ), + const SizedBox(height: 20), + const Text( + 'Memuat data analisis...', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + fontFamily: 'Poppins', + ), + ) + ], + ), + ); + } + + var kelebihan = siswaC.dataAnalysis['kelebihan']; + var kekurangan = siswaC.dataAnalysis['kekurangan']; + + return Column( + children: [ + if (siswaC.kelebihanIsEmpty.value) + const SizedBox() + else + Column(children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF10B981).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.quiz, + color: Color(0xFF10B981), + size: 24, + ), + ), + const SizedBox(width: 12), + const Text( + 'Rata rata Kuis', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black87, + fontFamily: 'Poppins', + ), + ), + ], + ), + const SizedBox(height: 16), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: siswaC.dataAnalysis['kelebihan'].length, + itemBuilder: (context, index) { + return AnimatedProgressItem( + title: kelebihan[index]['mapel'], + percentage: kelebihan[index]['persentase'], + color: const Color(0xFF10B981), + isTablet: isTablet, + animationDelay: (index * 200), + ); + }, + ), + ]), + if (siswaC.kekuranganIsEmpty.value) + const SizedBox() + else + Column(children: [ + const SizedBox(height: 24), + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFFEF4444).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.trending_down, + color: Color(0xFFEF4444), + size: 24, + ), + ), + const SizedBox(width: 12), + const Text( + 'Perlu Perbaikan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black87, + fontFamily: 'Poppins', + ), + ), + ], + ), + const SizedBox(height: 16), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: siswaC.dataAnalysis['kekurangan'].length, + itemBuilder: (context, index) { + return AnimatedProgressItem( + title: kekurangan[index]['mapel'], + percentage: kekurangan[index]['persentase'], + color: const Color(0xFFEF4444), + isTablet: isTablet, + animationDelay: (index * 200) + 500, + ); + }, + ), + ]), + if (siswaC.kelebihanIsEmpty.value && + siswaC.kekuranganIsEmpty.value) + Container( + padding: const EdgeInsets.all(40), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF6366F1).withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.analytics_outlined, + size: 60, + color: Color(0xFF6366F1), + ), + ), + const SizedBox(height: 20), + const Text( + 'Belum ada data analisis', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 8), + const Text( + 'Data akan muncul setelah mengerjakan kuis', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontFamily: 'Poppins', + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ); + }, + ), + ), + ); + } + + Widget _buildActionButtons(BuildContext context, bool isTablet) { + return Column( + children: [ + // Tombol Ubah Password + GestureDetector( + onTap: () { + Get.toNamed(AppRoutes.ubahPassord); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 18), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFF6366F1).withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.lock_reset, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + 'Ubah Password', + style: TextStyle( + fontSize: isTablet ? 18 : 16, + fontWeight: FontWeight.w600, + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ), + + SizedBox(height: isTablet ? 20 : 16), + + // Tombol Logout + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + title: const Text( + 'Konfirmasi Logout', + style: TextStyle( + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + ), + ), + content: const Text( + 'Apakah Anda yakin ingin logout?', + style: TextStyle( + fontFamily: 'Poppins', + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + }, + child: const Text( + 'Batal', + style: TextStyle( + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ), + ElevatedButton( + onPressed: () { + siswaC.logout(role: "siswa"); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFEF4444), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: const Text( + 'Logout', + style: TextStyle( + fontFamily: 'Poppins', + ), + ), + ), + ], + ), + ); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 18), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFEF4444), Color(0xFFDC2626)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFFEF4444).withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.logout, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + 'Logout', + style: TextStyle( + fontSize: isTablet ? 18 : 16, + fontWeight: FontWeight.w600, + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ), + ], + ); + } +} + +// Widget untuk progress item dengan animasi +class AnimatedProgressItem extends StatefulWidget { + final String title; + final int percentage; + final Color color; + final bool isTablet; + final int animationDelay; + + const AnimatedProgressItem({ + super.key, + required this.title, + required this.percentage, + required this.color, + required this.isTablet, + this.animationDelay = 0, + }); + + @override + State createState() => _AnimatedProgressItemState(); +} + +class _AnimatedProgressItemState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + _animation = Tween(begin: 0, end: widget.percentage / 100) + .animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeOutQuart, + )); + + // Tunda animasi sesuai dengan delay yang diberikan + Future.delayed(Duration(milliseconds: widget.animationDelay), () { + if (mounted) { + _controller.forward(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textStyle = TextStyle( + fontSize: widget.isTablet ? 16 : 14, + fontWeight: FontWeight.w500, + color: Colors.black87, + fontFamily: 'Poppins', + ); + + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: widget.color.withOpacity(0.1), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + widget.title, + style: textStyle.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: widget.color.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${(_animation.value * 100).toInt()}%', + style: textStyle.copyWith( + color: widget.color, + fontWeight: FontWeight.w700, + fontSize: 12, + ), + ), + ); + }, + ), + ], + ), + const SizedBox(height: 12), + AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return ClipRRect( + borderRadius: BorderRadius.circular(10), + child: LinearProgressIndicator( + value: _animation.value, + backgroundColor: widget.color.withOpacity(0.1), + color: widget.color, + minHeight: widget.isTablet ? 12 : 10, + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/views/siswa/quiz/bindings/matpel_quiz_binding.dart b/lib/views/siswa/quiz/bindings/matpel_quiz_binding.dart new file mode 100644 index 0000000..5c0832d --- /dev/null +++ b/lib/views/siswa/quiz/bindings/matpel_quiz_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart'; + +class MatpelQuizBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => MataPelajaranSimpleController()); + } +} diff --git a/lib/views/siswa/quiz/bindings/quiz_binding.dart b/lib/views/siswa/quiz/bindings/quiz_binding.dart new file mode 100644 index 0000000..80dadd8 --- /dev/null +++ b/lib/views/siswa/quiz/bindings/quiz_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_controller.dart'; + +class QuizBinding extends Bindings { + @override + void dependencies() { + Get.put(QuizController()); + } +} diff --git a/lib/views/siswa/quiz/bindings/quiz_finish_binding.dart b/lib/views/siswa/quiz/bindings/quiz_finish_binding.dart new file mode 100644 index 0000000..84f999d --- /dev/null +++ b/lib/views/siswa/quiz/bindings/quiz_finish_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_finish_controller.dart'; + +class QuizFinishBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => QuizFinishController()); + } +} diff --git a/lib/views/siswa/quiz/bindings/soal_quiz_binding.dart b/lib/views/siswa/quiz/bindings/soal_quiz_binding.dart new file mode 100644 index 0000000..90e94da --- /dev/null +++ b/lib/views/siswa/quiz/bindings/soal_quiz_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_attempt_controller.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_question_controller.dart'; + +class SoalQuizBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => QuizAttemptController()); + Get.lazyPut(() => QuizQuestionController()); + } +} diff --git a/lib/views/siswa/quiz/controllers/quiz_attempt_controller.dart b/lib/views/siswa/quiz/controllers/quiz_attempt_controller.dart new file mode 100644 index 0000000..be3d725 --- /dev/null +++ b/lib/views/siswa/quiz/controllers/quiz_attempt_controller.dart @@ -0,0 +1,604 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/quiz_answer_model.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_question_controller.dart'; +import 'package:ui/widgets/my_snackbar.dart'; +import 'dart:async'; + +class QuizAttemptController extends GetxController { + var isLoadingAttempt = false.obs; + var isLoadingAnswer = false.obs; + var isLastQuestion = false.obs; + var quizIdRx = "".obs; + var attemptId = "".obs; + var token = "".obs; + var nisn = "".obs; + var waktuQuiz = 0.obs; // Menambahkan waktu quiz dalam menit + var waktuTersisa = 0.obs; // Menambahkan waktu tersisa dalam detik + var waktuMulai = DateTime.now().obs; // Menambahkan waktu mulai + Timer? timer; // Timer untuk countdown + QuizAnswerModel? quizAnswerM; + QuizQuestionController questionC = Get.find(); + var remainingTime = Duration.zero.obs; + var isTimerRunning = false.obs; + var isQuizFinished = false.obs; + + // Menambahkan tracking jawaban yang telah dijawab + var answeredQuestions = + {}.obs; // questionId -> selectedAnswer + var currentQuestionId = "".obs; + + // Menambahkan tracking semua question IDs untuk auto-finish + List allQuestionIds = []; + + @override + void onInit() async { + super.onInit(); + final prefs = await SharedPreferences.getInstance(); + token.value = prefs.getString('token') ?? ""; + nisn.value = prefs.getString('nisn') ?? ""; + var quizId = Get.arguments['quiz_id']; + + // Test model parsing + testModelParsing(); + + // Reset all data for fresh start (especially for retake) + await resetQuizData(); + + await postQuizAttemptStart(quizId); + attemptId.value = prefs.getString('attempt_id') ?? ""; + } + + @override + void onClose() { + timer?.cancel(); // Cancel timer when controller is disposed + super.onClose(); + } + + // Method untuk memulai timer + void startTimer() { + timer?.cancel(); // Cancel existing timer + isTimerRunning.value = true; + timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (waktuTersisa.value > 0) { + waktuTersisa.value--; + } else { + timer.cancel(); + isTimerRunning.value = false; + // Waktu habis, auto finish quiz + autoFinishQuiz(); + } + }); + } + + // Method untuk initialize timer dengan validasi yang lebih robust + void initializeTimer() { + // Validasi waktu quiz + if (waktuQuiz.value > 0 && waktuQuiz.value <= 1440) { + // Max 24 jam + waktuTersisa.value = waktuQuiz.value * 60; + startTimer(); + log("Timer initialized with ${waktuQuiz.value} minutes (${waktuTersisa.value} seconds)"); + } else if (waktuQuiz.value > 1440) { + // Jika waktu terlalu besar, reset ke unlimited + waktuQuiz.value = 0; + waktuTersisa.value = 0; + log("Quiz time too large, reset to unlimited"); + } else { + log("No time limit for this quiz"); + } + } + + // Method untuk auto finish quiz ketika waktu habis + Future autoFinishQuiz() async { + try { + // Kirim jawaban kosong untuk semua soal yang belum dijawab + for (String qid in allQuestionIds) { + if (!answeredQuestions.containsKey(qid)) { + // Kirim jawaban kosong + await postQuizAttemptAnswer( + quizAttemptId: attemptId.value, + questionId: qid, + jawabanSiswa: "", + ); + } + } + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${token.value}', + }; + + log("Auto finishing quiz for attempt ID: ${attemptId.value}"); + log("[DEBUG] Auto finish URL: ${ApiConstants.quizAutoFinishEnpoint}/${attemptId.value}"); + + final response = await http.post( + Uri.parse("${ApiConstants.quizAutoFinishEnpoint}/${attemptId.value}"), + headers: headers, + ); + + log("Auto finish response status: ${response.statusCode}"); + log("Auto finish response body: ${response.body}"); + if (response.statusCode != 200) { + log("[DEBUG] Gagal auto-finish quiz. Status: ${response.statusCode}, Body: ${response.body}, Attempt ID: ${attemptId.value}"); + } + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + log("Auto finish success: $json"); + + // Tampilkan alert waktu habis + Get.dialog( + AlertDialog( + title: const Text('Waktu Habis'), + content: const Text( + 'Waktu pengerjaan quiz telah habis. Quiz akan diselesaikan otomatis.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/quiz-selesai', + arguments: {'quiz_id': quizIdRx.value}); + }, + child: const Text('OK'), + ), + ], + ), + ); + } else { + log("Auto finish failed: ${response.statusCode} - ${response.body}"); + // Handle auto finish failure + Get.dialog( + AlertDialog( + title: const Text('Error'), + content: const Text( + 'Gagal menyelesaikan quiz otomatis. Silakan hubungi admin.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali ke Dashboard'), + ), + ], + ), + ); + } + } catch (e) { + log("Error auto finish quiz: $e"); + // Handle network error + Get.dialog( + AlertDialog( + title: const Text('Error Koneksi'), + content: const Text( + 'Gagal menyelesaikan quiz. Silakan cek internet Anda.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali ke Dashboard'), + ), + ], + ), + ); + } + } + + // Method untuk menyimpan semua question IDs + void saveAllQuestionIds(List questionIds) { + allQuestionIds = List.from(questionIds); + log("Saved ${allQuestionIds.length} question IDs for auto-finish"); + } + + // Method untuk menghentikan timer quiz + void stopQuizTimer() { + timer?.cancel(); + isTimerRunning.value = false; + log("Quiz timer stopped manually"); + } + + Future postQuizAttemptStart(var quizId) async { + final prefs = await SharedPreferences.getInstance(); + if (token.value == "") { + throw Exception("Token not found"); + } + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${token.value}', + }; + + try { + isLoadingAttempt(true); + Map body = { + 'quiz_id': quizId, + 'nisn': nisn.value, + }; + + log("Starting quiz with body: $body"); + log("Requesting URL: ${ApiConstants.quizAttemptStartEnpoint}"); + + final response = await http.post( + Uri.parse(ApiConstants.quizAttemptStartEnpoint), + headers: headers, + body: jsonEncode(body), + ); + + log("Start quiz response status: ${response.statusCode}"); + log("Start quiz response body: ${response.body}"); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + + if (json.containsKey('attempt_id')) { + // Clear all previous attempt data + await prefs.remove('attempt_id'); + await prefs.remove('quiz_answers'); + await prefs.remove('current_question_index'); + await prefs.remove('quiz_start_time'); + await prefs.remove('quiz_end_time'); + await prefs.remove('remaining_time'); + + // Save new attempt ID + await prefs.setString('attempt_id', json['attempt_id'].toString()); + log("New attempt ID saved: ${json['attempt_id']}"); + + // Reset controller state + attemptId.value = json['attempt_id'].toString(); + waktuTersisa.value = 0; + waktuQuiz.value = 0; + waktuMulai.value = DateTime.now(); + } else { + log("JSON tidak memiliki 'attempt_id'"); + Get.dialog( + AlertDialog( + title: const Text('Error'), + content: const Text('Gagal memulai quiz. Silakan coba lagi.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali'), + ), + ], + ), + ); + return; + } + + // Set waktu quiz dan mulai timer + if (json.containsKey('waktu_quiz') && json['waktu_quiz'] != null) { + waktuQuiz.value = json['waktu_quiz']; + initializeTimer(); // Use the new initializeTimer method + log("Timer started with ${waktuQuiz.value} minutes"); + } else { + // Jika backend tidak mengirim waktu_quiz, ambil dari argumen + final waktuQuizArg = Get.arguments['waktu_quiz']; + if (waktuQuizArg != null && + waktuQuizArg != "null" && + int.tryParse(waktuQuizArg.toString()) != null) { + waktuQuiz.value = int.parse(waktuQuizArg.toString()); + initializeTimer(); // Use the new initializeTimer method + log("Timer started with ${waktuQuiz.value} minutes (from arg)"); + } else { + log("No time limit for this quiz"); + } + } + + // Set waktu mulai + if (json.containsKey('waktu_mulai')) { + waktuMulai.value = DateTime.parse(json['waktu_mulai']); + log("Start time set: ${waktuMulai.value}"); + } + + // Log response backend + log("[DEBUG] Start quiz response body: ${jsonEncode(json)}"); + + // Baru panggil getQuizQuestion setelah timer di-set + questionC.getQuizQuestion(json['attempt_id']); + + snackbarAlert(json['message'] ?? "Quiz", + "Tidak boleh keluar dari quiz ini!.", Colors.green); + + log("New attempt started with ID: ${json['attempt_id'].toString()}"); + } else if (response.statusCode == 500) { + log("Backend error 500 on quiz start: ${response.body}"); + Get.dialog( + AlertDialog( + title: const Text('Error Server'), + content: const Text( + 'Terjadi kesalahan pada server saat memulai quiz. Silakan coba lagi.\n\nError: 500 Internal Server Error'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali'), + ), + TextButton( + onPressed: () { + Get.back(); + // Retry starting quiz + postQuizAttemptStart(quizId); + }, + child: const Text('Coba Lagi'), + ), + ], + ), + ); + } else if (response.statusCode == 404) { + log("Quiz not found: ${response.body}"); + Get.dialog( + AlertDialog( + title: const Text('Quiz Tidak Ditemukan'), + content: const Text( + 'Quiz yang dipilih tidak ditemukan atau tidak tersedia.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali'), + ), + ], + ), + ); + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + log("Error response: ${response.body}"); + Get.dialog( + AlertDialog( + title: const Text('Error'), + content: Text('Gagal memulai quiz. Status: ${response.statusCode}'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali'), + ), + ], + ), + ); + } + } catch (e) { + log("Error get matpel simple: $e"); + Get.dialog( + AlertDialog( + title: const Text('Error Koneksi'), + content: const Text( + 'Terjadi kesalahan koneksi saat memulai quiz. Silakan cek internet Anda.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali'), + ), + TextButton( + onPressed: () { + Get.back(); + // Retry starting quiz + postQuizAttemptStart(quizId); + }, + child: const Text('Coba Lagi'), + ), + ], + ), + ); + } finally { + isLoadingAttempt(false); + } + } + + Future postQuizAttemptAnswer({ + required String quizAttemptId, + required String questionId, + required String jawabanSiswa, + }) async { + if (token.value == "") { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${token.value}', + }; + + try { + isLoadingAnswer(true); + Map body = { + 'question_id': questionId, + 'jawaban_siswa': jawabanSiswa, + }; + + final url = + "${ApiConstants.baseUrlApi}/quiz-attempts/$quizAttemptId/answer"; + log("Posting answer to URL: $url"); + log("Request body: $body"); + + final response = await http.post( + Uri.parse(url), + headers: headers, + body: jsonEncode(body), + ); + + log("Answer response status: ${response.statusCode}"); + log("Answer response body: ${response.body}"); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + log("Parsed answer JSON: $json"); + + // Log the raw data structure + log("Raw JSON structure:"); + log("- status: ${json['status']}"); + log("- message: ${json['message']}"); + log("- data: ${json['data']}"); + + if (json['data'] != null) { + var data = json['data']; + log("Data fields:"); + log("- quiz_id: ${data['quiz_id']} (type: ${data['quiz_id'].runtimeType})"); + log("- correct: ${data['correct']} (type: ${data['correct'].runtimeType})"); + log("- fase: ${data['fase']} (type: ${data['fase'].runtimeType})"); + log("- new_level: ${data['new_level']} (type: ${data['new_level'].runtimeType})"); + log("- skor_sementara: ${data['skor_sementara']} (type: ${data['skor_sementara'].runtimeType})"); + log("- selesai: ${data['selesai']} (type: ${data['selesai'].runtimeType})"); + log("- waktu_tersisa: ${data['waktu_tersisa']} (type: ${data['waktu_tersisa']?.runtimeType})"); + } + + try { + isLastQuestion.value = json['data']['selesai'] ?? false; + quizIdRx.value = json['data']['quiz_id'].toString(); + quizAnswerM = QuizAnswerModel.fromJson(json); + + // Update waktu tersisa dari response + if (json['data']['waktu_tersisa'] != null) { + int serverTime = json['data']['waktu_tersisa']; + // Validasi waktu dari server + if (serverTime >= 0 && serverTime <= 86400) { + // Max 24 jam dalam detik + waktuTersisa.value = serverTime; + log("Updated remaining time from server: $serverTime seconds"); + } else { + log("Invalid server time received: $serverTime, keeping local timer"); + } + } + + // Log the parsed data + if (quizAnswerM?.data != null) { + var data = quizAnswerM!.data; + log("Parsed answer data:"); + log("- Quiz ID: ${data.quizId}"); + log("- Correct: ${data.correct}"); + log("- Fase: ${data.fase}"); + log("- New Level: ${data.newLevel}"); + log("- Skor Sementara: ${data.skorSementara}"); + log("- Selesai: ${data.selesai}"); + log("- Waktu Tersisa: ${data.waktuTersisa}"); + } + + // Also log raw data for comparison + log("Raw data comparison:"); + var rawData = json['data']; + log("- Raw correct: ${rawData['correct']}"); + log("- Raw skor_sementara: ${rawData['skor_sementara']}"); + log("- Raw quiz_id: ${rawData['quiz_id']}"); + + log("DATA API = ${json['data']}"); + } catch (parseError) { + log("Error parsing answer response: $parseError"); + log("Parse error stack trace: ${StackTrace.current}"); + // Handle parsing error gracefully + quizAnswerM = null; + } + } else { + log("Terjadi kesalahan post answer quiz: ${response.statusCode}"); + log("Error response: ${response.body}"); + + // Show error dialog for non-200 responses + Get.dialog( + AlertDialog( + title: const Text('Error'), + content: Text( + 'Gagal mengirim jawaban. Status: ${response.statusCode}\n\nResponse: ${response.body}'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('OK'), + ), + ], + ), + ); + } + } catch (e) { + log("Error post Answer quiz simple: $e"); + } finally { + isLoadingAnswer(false); + } + } + + // Method untuk reset semua data quiz (untuk mengerjakan ulang) + Future resetQuizData() async { + final prefs = await SharedPreferences.getInstance(); + + // Clear all attempt-related data + await prefs.remove('attempt_id'); + await prefs.remove('quiz_answers'); + await prefs.remove('current_question_index'); + await prefs.remove('quiz_start_time'); + await prefs.remove('quiz_end_time'); + await prefs.remove('remaining_time'); + + // Reset controller state + attemptId.value = ""; + waktuTersisa.value = 0; + waktuQuiz.value = 0; + waktuMulai.value = DateTime.now(); + + // Cancel any running timer + timer?.cancel(); + isTimerRunning.value = false; + + log("All quiz data has been reset for retake"); + } + + // Method untuk test parsing model + void testModelParsing() { + // Test data yang seharusnya berhasil + Map testData = { + "status": true, + "message": "Success", + "data": { + "quiz_id": 1, + "correct": 1, + "fase": 1, + "new_level": 1, + "skor_sementara": 10, + "selesai": false, + "waktu_tersisa": 300 + } + }; + + try { + var testModel = QuizAnswerModel.fromJson(testData); + log("Test parsing successful:"); + log("- Correct: ${testModel.data.correct}"); + log("- Skor Sementara: ${testModel.data.skorSementara}"); + } catch (e) { + log("Test parsing failed: $e"); + } + } + + // Method untuk menyimpan jawaban yang dipilih + void saveAnswer(String questionId, String selectedAnswer) { + answeredQuestions[questionId] = selectedAnswer; + currentQuestionId.value = questionId; + log("Saved answer for question $questionId: $selectedAnswer"); + } + + // Method untuk mendapatkan jawaban yang dipilih + String? getSelectedAnswer(String questionId) { + return answeredQuestions[questionId]; + } + + // Method untuk mengecek apakah soal sudah dijawab + bool isQuestionAnswered(String questionId) { + return answeredQuestions.containsKey(questionId); + } +} diff --git a/lib/views/siswa/quiz/controllers/quiz_controller.dart b/lib/views/siswa/quiz/controllers/quiz_controller.dart new file mode 100644 index 0000000..127ee89 --- /dev/null +++ b/lib/views/siswa/quiz/controllers/quiz_controller.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/quiz_mode.dart'; + +class QuizController extends GetxController { + var isLoading = false.obs; + QuizModel? quizM; + var isEmptyData = true.obs; + + @override + void onInit() { + super.onInit(); + // Reset data when controller is initialized + resetData(); + // Delay to ensure arguments are available + Future.delayed(const Duration(milliseconds: 100), () { + getQuiz(); + }); + } + + void resetData() { + quizM = null; + isEmptyData.value = true; + isLoading.value = false; + } + + Future getQuiz() async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + log("Get.arguments: ${Get.arguments.toString()}"); + + // Check if arguments are available + if (Get.arguments == null || !Get.arguments.containsKey('matpel_id')) { + log("Arguments not available or missing 'matpel_id'"); + isEmptyData.value = true; + return; + } + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final url = + "${ApiConstants.quizEnpoint}?matapelajaran_id=${Get.arguments['matpel_id'].toString()}"; + log("Requesting URL: $url"); + + final response = await http.get( + Uri.parse(url), + headers: headers, + ); + + log("Response status: ${response.statusCode}"); + log("Response body: ${response.body}"); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + log("Parsed JSON: $json"); + + try { + quizM = QuizModel.fromJson(json); + log("Quiz model created successfully"); + log("Quiz data length: ${quizM?.data.length ?? 0}"); + + if (quizM!.data.isEmpty) { + isEmptyData(true); + log("Quiz data is empty"); + } else { + isEmptyData(false); + log("Quiz data has ${quizM!.data.length} items"); + } + } catch (parseError) { + log("Error parsing QuizModel: $parseError"); + // Fallback: try to create model without waktu field + isEmptyData(true); + } + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + log("Error response: ${response.body}"); + isEmptyData(true); + } + } catch (e) { + log("Error get quiz: $e"); + isEmptyData(true); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/siswa/quiz/controllers/quiz_finish_controller.dart b/lib/views/siswa/quiz/controllers/quiz_finish_controller.dart new file mode 100644 index 0000000..88b3127 --- /dev/null +++ b/lib/views/siswa/quiz/controllers/quiz_finish_controller.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/quiz_attempt_model.dart'; + +class QuizFinishController extends GetxController { + var isLoading = false.obs; + QuizAttemptModel? quizAttemptM; + var skorMe = "".obs; + + @override + void onInit() { + super.onInit(); + var quizId = Get.arguments['quiz_id']; + getQuizAttempt(quizId); + getSkorMe(quizId); + } + + Future getQuizAttempt(quizId) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + await prefs.remove('attempt_id'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse("${ApiConstants.quizAttemptFinishEnpoint}?quiz_id=$quizId"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + quizAttemptM = QuizAttemptModel.fromJson(json); + } else { + log("Terjadi kesalahan get data attempt: ${response.statusCode}"); + } + } catch (e) { + log("Error get quiz attempt simple: $e"); + } finally { + isLoading(false); + } + } + + // Method untuk mengambil skor yang sama seperti di ranking + Future getSkorMe(quizId) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + final response = await http.get( + Uri.parse("${ApiConstants.quizTopFiveEnpoint}?quiz_id=$quizId"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + skorMe.value = json['skor_me']['skor']; + } else { + log("Terjadi kesalahan get skor me: ${response.statusCode}"); + } + } catch (e) { + log("Error get skor me: $e"); + } + } + + // Method untuk manually finish quiz + Future finishQuiz(String quizId) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + log("Manually finishing quiz for quiz ID: $quizId"); + + // Use POST method for finishing quiz + final response = await http.post( + Uri.parse("${ApiConstants.quizAttemptFinishEnpoint}?quiz_id=$quizId"), + headers: headers, + ); + + log("Manual finish response status: ${response.statusCode}"); + log("Manual finish response body: ${response.body}"); + + if (response.statusCode == 200) { + log("Quiz finished successfully"); + // Refresh the data + await getQuizAttempt(quizId); + } else { + log("Failed to finish quiz: ${response.statusCode} - ${response.body}"); + } + } catch (e) { + log("Error manually finishing quiz: $e"); + } + } +} diff --git a/lib/views/siswa/quiz/controllers/quiz_question_controller.dart b/lib/views/siswa/quiz/controllers/quiz_question_controller.dart new file mode 100644 index 0000000..962e7d6 --- /dev/null +++ b/lib/views/siswa/quiz/controllers/quiz_question_controller.dart @@ -0,0 +1,364 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/quiz_question_model.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter/material.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_attempt_controller.dart'; + +class QuizQuestionController extends GetxController { + var isLoading = false.obs; + QuizQuestionModel? quizQuestionM; + + Future getQuizQuestion(attemptId) async { + var url = + "${ApiConstants.baseUrlApi}/quiz-attempts/$attemptId/next-question"; + + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + log("Requesting quiz question URL: $url"); + log("Attempt ID: $attemptId"); + + final response = await http.get( + Uri.parse(url), + headers: headers, + ); + + log("Response status: ${response.statusCode}"); + log("Response body: ${response.body}"); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + log("Parsed JSON: $json"); + + // Cek pesan soal habis di data.original.message + if (json['data'] != null && + json['data']['original'] != null && + json['data']['original']['message'] != null) { + String msg = + json['data']['original']['message'].toString().toLowerCase(); + if (msg.contains('tidak ada soal lagi di level ini') || + msg.contains('soal habis') || + msg.contains('questions exhausted') || + msg.contains('no more questions')) { + log('Detected questions exhausted in data.original.message'); + await handleQuestionsExhausted(attemptId); + return; + } + } + + // Cek apakah response mengandung pesan soal habis + if (json.containsKey('message')) { + String message = json['message'].toString().toLowerCase(); + if (message.contains("tidak ada soal lagi di level ini") || + message.contains("soal habis") || + message.contains("questions exhausted") || + message.contains("no more questions")) { + log("200 response with questions exhausted message detected"); + await handleQuestionsExhausted(attemptId); + return; + } + } + + // Cek apakah waktu sudah habis + if (json.containsKey('waktu_habis') && json['waktu_habis'] == true) { + log("Waktu habis detected"); + // Waktu habis, redirect ke halaman selesai + Get.offAllNamed('/quiz-selesai', + arguments: {'quiz_id': json['quiz_id'] ?? ''}); + return; + } + + // Cek apakah quiz sudah selesai + if (json.containsKey('selesai') && json['selesai'] == true) { + log("Quiz selesai detected"); + // Quiz selesai, redirect ke halaman selesai + Get.offAllNamed('/quiz-selesai', + arguments: {'quiz_id': json['quiz_id'] ?? ''}); + return; + } + + try { + quizQuestionM = QuizQuestionModel.fromJson(json); + log("Quiz question model created successfully"); + + // Reset tracking jawaban untuk soal baru + QuizAttemptController quizAttemptC = + Get.find(); + quizAttemptC.answeredQuestions.clear(); + quizAttemptC.currentQuestionId.value = ""; + log("Reset answer tracking for new question"); + } catch (parseError) { + log("Error parsing quiz question: $parseError"); + // Handle parsing error gracefully + Get.dialog( + AlertDialog( + title: const Text('Error'), + content: const Text('Gagal memuat soal quiz. Silakan coba lagi.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali ke Dashboard'), + ), + ], + ), + ); + } + } else if (response.statusCode == 204) { + log("204 - No Content - Soal habis di level ini detected"); + // 204 No Content biasanya mengindikasikan tidak ada data/soal + await handleQuestionsExhausted(attemptId); + } else if (response.statusCode == 404) { + log("404 - Soal habis di level ini detected"); + // Cek apakah response body mengandung pesan spesifik + String responseBody = response.body.toLowerCase(); + if (responseBody.contains("tidak ada soal lagi di level ini") || + responseBody.contains("soal habis") || + responseBody.contains("questions exhausted")) { + log("Confirmed: Questions exhausted at this level"); + // Soal habis di level ini, hentikan quiz otomatis + await handleQuestionsExhausted(attemptId); + } else { + // 404 lainnya, tampilkan error umum + log("404 error but not questions exhausted: ${response.body}"); + Get.dialog( + AlertDialog( + title: const Text('Error'), + content: const Text('Gagal memuat soal quiz. Silakan coba lagi.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali ke Dashboard'), + ), + ], + ), + ); + } + } else if (response.statusCode == 500) { + log("Backend error 500: ${response.body}"); + // Handle 500 error - mungkin quiz tidak ada atau error di backend + Get.dialog( + AlertDialog( + title: const Text('Error Server'), + content: const Text( + 'Terjadi kesalahan pada server saat memuat soal. Silakan coba lagi atau hubungi admin.\n\nError: 500 Internal Server Error'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali ke Dashboard'), + ), + TextButton( + onPressed: () { + Get.back(); + // Retry loading question + getQuizQuestion(attemptId); + }, + child: const Text('Coba Lagi'), + ), + ], + ), + ); + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + log("Error response: ${response.body}"); + Get.dialog( + AlertDialog( + title: const Text('Error'), + content: + Text('Gagal memuat soal quiz. Status: ${response.statusCode}'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali ke Dashboard'), + ), + ], + ), + ); + } + } catch (e) { + log("Error get quiz question: $e"); + // Handle network or other errors + Get.dialog( + AlertDialog( + title: const Text('Error Koneksi'), + content: const Text( + 'Terjadi kesalahan koneksi saat memuat soal. Silakan cek internet Anda.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali ke Dashboard'), + ), + TextButton( + onPressed: () { + Get.back(); + // Retry loading question + getQuizQuestion(attemptId); + }, + child: const Text('Coba Lagi'), + ), + ], + ), + ); + } finally { + isLoading(false); + } + } + + // Method untuk menangani ketika soal habis di level tertentu + Future handleQuestionsExhausted(String attemptId) async { + try { + log("Handling questions exhausted for attempt ID: $attemptId"); + + // Hentikan timer quiz + QuizAttemptController quizAttemptC = Get.find(); + quizAttemptC.stopQuizTimer(); + quizAttemptC.isQuizFinished.value = true; + quizQuestionM = null; + update(); + log("Quiz timer stopped, quizQuestionM set to null, UI updated"); + + // Tampilkan pesan ke user + Get.dialog( + AlertDialog( + title: const Text('Soal Habis'), + content: const Text('Soal sudah habis di level ini, quiz selesai.'), + actions: [ + TextButton( + onPressed: () async { + Get.back(); + // Auto finish quiz + await autoFinishQuiz(attemptId); + }, + child: const Text('OK'), + ), + ], + ), + ); + } catch (e) { + log("Error handling questions exhausted: $e"); + // Fallback: langsung redirect ke halaman selesai + Get.offAllNamed('/quiz-selesai', arguments: {'quiz_id': ''}); + } + } + + // Method untuk auto finish quiz ketika soal habis + Future autoFinishQuiz(String attemptId) async { + try { + QuizAttemptController quizAttemptC = Get.find(); + quizAttemptC.isQuizFinished.value = true; + quizQuestionM = null; + update(); + log("autoFinishQuiz: isQuizFinished set, quizQuestionM set to null, UI updated"); + + // Kirim jawaban kosong untuk semua soal yang belum dijawab + for (String qid in quizAttemptC.allQuestionIds) { + if (!quizAttemptC.answeredQuestions.containsKey(qid)) { + // Kirim jawaban kosong + await quizAttemptC.postQuizAttemptAnswer( + quizAttemptId: attemptId, + questionId: qid, + jawabanSiswa: "", + ); + } + } + + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + + log("Auto finishing quiz for attempt ID: $attemptId"); + log("[DEBUG] Auto finish URL: ${ApiConstants.quizAutoFinishEnpoint}/$attemptId"); + + final response = await http.post( + Uri.parse("${ApiConstants.quizAutoFinishEnpoint}/$attemptId"), + headers: headers, + ); + + log("Auto finish response status: ${response.statusCode}"); + log("Auto finish response body: ${response.body}"); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + log("Auto finish success: $json"); + + // Redirect ke halaman hasil quiz + String quizId = json['quiz_id']?.toString() ?? ''; + Get.offAllNamed('/quiz-selesai', arguments: {'quiz_id': quizId}); + } else { + log("Auto finish failed: ${response.statusCode} - ${response.body}"); + // Handle auto finish failure + Get.dialog( + AlertDialog( + title: const Text('Error'), + content: const Text( + 'Gagal menyelesaikan quiz otomatis. Silakan hubungi admin.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali ke Dashboard'), + ), + ], + ), + ); + } + } catch (e) { + log("Error auto finish quiz: $e"); + // Handle network error + Get.dialog( + AlertDialog( + title: const Text('Error Koneksi'), + content: const Text( + 'Gagal menyelesaikan quiz. Silakan cek internet Anda.'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + Get.offAllNamed('/siswa-dashboard'); + }, + child: const Text('Kembali ke Dashboard'), + ), + ], + ), + ); + } + } +} diff --git a/lib/views/siswa/quiz/matpel_quiz.dart b/lib/views/siswa/quiz/matpel_quiz.dart new file mode 100644 index 0000000..7eb967a --- /dev/null +++ b/lib/views/siswa/quiz/matpel_quiz.dart @@ -0,0 +1,416 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class MatpelQuiz extends StatelessWidget { + MatpelQuiz({super.key}); + MataPelajaranSimpleController matapelajaranSimpleC = + Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF3F4F6), + appBar: AppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.quiz_outlined, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 10), + const Text( + "Quiz Challenge", + style: TextStyle( + fontWeight: FontWeight.w800, + fontSize: 22, + color: Colors.white, + letterSpacing: 0.5, + ), + ), + ], + ), + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF6366F1), + Color(0xFF8B5CF6), + Color(0xFFEC4899), + ], + ), + ), + ), + centerTitle: true, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Quiz List + Expanded( + child: Obx(() { + if (matapelajaranSimpleC.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF6366F1), + ), + strokeWidth: 3, + ), + ), + const SizedBox(height: 20), + const Text( + "Memuat data...", + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ); + } else if (matapelajaranSimpleC.isEmptyData.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(30), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF6366F1) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.quiz_outlined, + size: 60, + color: Color(0xFF6366F1), + ), + ), + const SizedBox(height: 20), + const MyText( + text: "Tidak Ada Mata Pelajaran", + fontSize: 18, + color: Colors.black87, + fontWeight: FontWeight.w700, + ), + const SizedBox(height: 8), + const Text( + "Belum ada mata pelajaran yang tersedia", + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + ); + } else { + return ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: matapelajaranSimpleC + .mataPelajaranSimpleM?.data.length ?? + 0, + itemBuilder: (context, index) { + var data = matapelajaranSimpleC + .mataPelajaranSimpleM?.data[index]; + return QuizSubjectCard( + id: data!.id.toString(), + title: data.nama, + guru: data.guru.nama, + mataPelajaranId: data.id.toString(), + index: index, + ); + }, + ); + } + }), + ), + ], + ), + ), + ), + ); + } +} + +class QuizSubjectCard extends StatefulWidget { + final String id; + final String title; + final String guru; + final String mataPelajaranId; + final int index; + + const QuizSubjectCard({ + super.key, + required this.id, + required this.title, + required this.guru, + required this.mataPelajaranId, + required this.index, + }); + + @override + State createState() => _QuizSubjectCardState(); +} + +class _QuizSubjectCardState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + List _getGradientColors(int index) { + List> gradients = [ + [const Color(0xFF667EEA), const Color(0xFF764BA2)], + [const Color(0xFFF093FB), const Color(0xFFF5576C)], + [const Color(0xFF4FACFE), const Color(0xFF00F2FE)], + [const Color(0xFF43E97B), const Color(0xFF38F9D7)], + [const Color(0xFFFA709A), const Color(0xFFFEE140)], + [const Color(0xFFA8EDEA), const Color(0xFFFED6E3)], + [const Color(0xFFFF9A9E), const Color(0xFFFECFEF)], + [const Color(0xFFA8CABA), const Color(0xFF5D4E75)], + ]; + return gradients[index % gradients.length]; + } + + IconData _getSubjectQuizIcon(String subject) { + String subjectLower = subject.toLowerCase(); + if (subjectLower.contains('matematika') || subjectLower.contains('math')) { + return Icons.functions; + } else if (subjectLower.contains('fisika') || + subjectLower.contains('physics')) { + return Icons.science_outlined; + } else if (subjectLower.contains('kimia') || + subjectLower.contains('chemistry')) { + return Icons.biotech_outlined; + } else if (subjectLower.contains('biologi') || + subjectLower.contains('biology')) { + return Icons.nature_people; + } else if (subjectLower.contains('bahasa') || + subjectLower.contains('language')) { + return Icons.translate_outlined; + } else if (subjectLower.contains('sejarah') || + subjectLower.contains('history')) { + return Icons.history_edu_outlined; + } else if (subjectLower.contains('geografi') || + subjectLower.contains('geography')) { + return Icons.public_outlined; + } else if (subjectLower.contains('seni') || subjectLower.contains('art')) { + return Icons.palette_outlined; + } else if (subjectLower.contains('olahraga') || + subjectLower.contains('sport')) { + return Icons.sports_outlined; + } else { + return Icons.quiz_outlined; + } + } + + @override + Widget build(BuildContext context) { + final gradientColors = _getGradientColors(widget.index); + + return GestureDetector( + onTapDown: (_) { + _animationController.forward(); + }, + onTapUp: (_) { + _animationController.reverse(); + Get.toNamed(AppRoutes.matpelQuizDetail, arguments: { + 'matpel_id': widget.mataPelajaranId, + 'matpel': widget.title, + }); + }, + onTapCancel: () { + _animationController.reverse(); + }, + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: gradientColors, + ), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: gradientColors[0].withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Container( + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.1), + Colors.white.withOpacity(0.05), + ], + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.4), + width: 2, + ), + ), + child: Icon( + _getSubjectQuizIcon(widget.title), + color: Colors.white, + size: 32, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + fontFamily: 'Poppins', + color: Colors.white, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + "Quiz Level", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + color: Colors.white, + ), + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + size: 16, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/views/siswa/quiz/matpel_quiz_detail.dart b/lib/views/siswa/quiz/matpel_quiz_detail.dart new file mode 100644 index 0000000..dfb5b93 --- /dev/null +++ b/lib/views/siswa/quiz/matpel_quiz_detail.dart @@ -0,0 +1,659 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class MatpelQuizDetail extends StatelessWidget { + MatpelQuizDetail({super.key}); + QuizController quizC = Get.find(); + + @override + Widget build(BuildContext context) { + print("QuizDetail build called"); + print("Arguments received: " + Get.arguments.toString()); + print("Arguments type: " + Get.arguments.runtimeType.toString()); + return Scaffold( + backgroundColor: const Color(0xFFF3F4F6), + appBar: AppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.quiz_outlined, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + "Quiz ${Get.arguments['matpel']}", + style: const TextStyle( + fontWeight: FontWeight.w800, + fontSize: 20, + color: Colors.white, + letterSpacing: 0.5, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF6366F1), + Color(0xFF8B5CF6), + Color(0xFFEC4899), + ], + ), + ), + ), + centerTitle: false, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Obx(() { + if (quizC.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF6366F1), + ), + strokeWidth: 3, + ), + ), + const SizedBox(height: 20), + const Text( + "Memuat quiz...", + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ); + } else if (quizC.isEmptyData.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(30), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF6366F1) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.quiz_outlined, + size: 60, + color: Color(0xFF6366F1), + ), + ), + const SizedBox(height: 20), + const MyText( + text: "Tidak Ada Quiz", + fontSize: 18, + color: Colors.black87, + fontWeight: FontWeight.w700, + ), + const SizedBox(height: 8), + const Text( + "Belum ada quiz yang tersedia", + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + quizC.resetData(); + quizC.getQuiz(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6366F1), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + "Coba Lagi", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } else { + return ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: quizC.quizM?.data.length ?? 0, + itemBuilder: (context, index) { + var data = quizC.quizM?.data[index]; + // Check if quiz is completed based on waktu_selesai + bool isCompleted = + data?.quizAttempt?.waktuSelesai != null; + + return SizedBox( + child: TaskItem( + id: data!.id.toString(), + title: data.judul, + total: data.totalSoalTampil.toString(), + waktu: data.waktu?.toString() ?? "null", + status: isCompleted, + index: index, + ), + ); + }, + ); + } + }), + ), + ], + ), + ), + ), + ); + } +} + +class TaskItem extends StatefulWidget { + final String id; + final String title; + final String total; + final String waktu; + final bool status; + final int index; + + const TaskItem({ + super.key, + required this.id, + required this.title, + required this.total, + required this.waktu, + required this.status, + required this.index, + }); + + @override + State createState() => _TaskItemState(); +} + +class _TaskItemState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + print("QuizDetail initState called"); + print("Arguments received: " + Get.arguments.toString()); + print("Arguments type: " + Get.arguments.runtimeType.toString()); + _animationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + List _getGradientColors(int index, bool isCompleted) { + if (isCompleted) { + return [const Color(0xFF2E7D8F), const Color(0xFF1B5E7A)]; + } + + List> gradients = [ + [const Color(0xFF4A5568), const Color(0xFF2D3748)], + [const Color(0xFF805AD5), const Color(0xFF553C9A)], + [const Color(0xFFE53E3E), const Color(0xFFC53030)], + [const Color(0xFF38A169), const Color(0xFF2F855A)], + [const Color(0xFFDD6B20), const Color(0xFFC05621)], + [const Color(0xFF3182CE), const Color(0xFF2C5282)], + [const Color(0xFFD69E2E), const Color(0xFFB7791F)], + [const Color(0xFF667EEA), const Color(0xFF5A67D8)], + ]; + return gradients[index % gradients.length]; + } + + IconData _getQuizIcon(bool isCompleted) { + if (isCompleted) { + return Icons.check_circle; + } + return Icons.quiz_outlined; + } + + @override + Widget build(BuildContext context) { + final gradientColors = _getGradientColors(widget.index, widget.status); + bool isCompleted = widget.status; + + return GestureDetector( + onTapDown: (_) { + _animationController.forward(); + }, + onTapUp: (_) { + _animationController.reverse(); + if (isCompleted) { + // Show options for completed quiz + _showQuizOptions(context); + } else { + // Start quiz, kirim juga waktu_quiz + Get.offAllNamed(AppRoutes.soalQuiz, arguments: { + "quiz_id": widget.id, + "waktu_quiz": widget.waktu, + }); + } + }, + onTapCancel: () { + _animationController.reverse(); + }, + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: gradientColors, + ), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: gradientColors[0].withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Container( + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.1), + Colors.white.withOpacity(0.05), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.4), + width: 2, + ), + ), + child: Icon( + _getQuizIcon(isCompleted), + color: Colors.white, + size: 32, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + fontFamily: 'Poppins', + color: Colors.white, + letterSpacing: 0.5, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + isCompleted ? "Selesai" : "Belum Dikerjakan", + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + color: Colors.white, + ), + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.25), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + size: 16, + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + children: [ + Icon( + Icons.help_outline, + size: 16, + color: Colors.white.withOpacity(0.9), + ), + const SizedBox(width: 8), + Text( + "${widget.total} Soal", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + color: Colors.white.withOpacity(0.9), + ), + ), + ], + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + children: [ + Icon( + Icons.timer, + size: 16, + color: Colors.white.withOpacity(0.9), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.waktu == "null" + ? "Tidak dibatasi" + : "${widget.waktu} menit", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + color: Colors.white.withOpacity(0.9), + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ), + ); + } + + void _showQuizOptions(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => Container( + padding: const EdgeInsets.all(24), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(25), + topRight: Radius.circular(25), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 20), + const Text( + "Pilih Aksi", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () { + Get.back(); + Get.toNamed(AppRoutes.quizSelesai, + arguments: {'quiz_id': widget.id}); + }, + icon: const Icon(Icons.visibility), + label: const Text("Lihat Hasil"), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF4FACFE), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + Get.back(); + _showRetakeConfirmation(context); + }, + icon: const Icon(Icons.refresh), + label: const Text("Kerjakan Ulang"), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF43E97B), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + void _showRetakeConfirmation(BuildContext context) { + Get.dialog( + AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + title: const Text( + 'Kerjakan Ulang Quiz', + style: TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + ), + ), + content: const Text( + 'Apakah Anda yakin ingin mengerjakan quiz ini lagi?\n\n• Skor sebelumnya akan tetap tersimpan\n• Anda akan mendapatkan attempt baru\n• Timer akan dimulai ulang dari awal', + style: TextStyle( + fontFamily: 'Poppins', + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text( + 'Batal', + style: TextStyle( + color: Colors.grey, + fontWeight: FontWeight.w600, + ), + ), + ), + ElevatedButton( + onPressed: () { + Get.back(); + Get.offAllNamed(AppRoutes.soalQuiz, arguments: { + "quiz_id": widget.id, + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF43E97B), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Ya, Kerjakan Ulang', + style: TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/siswa/quiz/soal_quiz.dart b/lib/views/siswa/quiz/soal_quiz.dart new file mode 100644 index 0000000..2c12cc0 --- /dev/null +++ b/lib/views/siswa/quiz/soal_quiz.dart @@ -0,0 +1,707 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_attempt_controller.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_question_controller.dart'; +import 'package:ui/widgets/my_snackbar.dart'; + +class SoalQuiz extends StatefulWidget { + const SoalQuiz({super.key}); + + @override + State createState() => _SoalQuizState(); +} + +class _SoalQuizState extends State { + int currentQuestion = 0; + QuizAttemptController quizAttemptC = Get.find(); + QuizQuestionController quizQuestionC = Get.find(); + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + return await _showExitConfirmationDialog(context); + }, + child: Scaffold( + appBar: AppBar( + title: const Text( + 'Quiz', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: Colors.green.shade300, + elevation: 0, + iconTheme: const IconThemeData(color: Colors.white), + actions: [ + // Timer kecil di pojok kanan + Container( + margin: const EdgeInsets.only(right: 16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white.withOpacity(0.9), + Colors.white.withOpacity(0.7) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx(() { + // Handle negative time and ensure valid display + final displayTime = quizAttemptC.waktuTersisa.value < 0 + ? 0 + : quizAttemptC.waktuTersisa.value; + final minutes = displayTime ~/ 60; + final seconds = displayTime % 60; + final isWarning = displayTime <= 300; + final isCritical = displayTime <= 60; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isCritical ? Icons.warning : Icons.access_time, + color: isCritical + ? Colors.red.shade600 + : isWarning + ? Colors.orange.shade600 + : Colors.blue.shade600, + size: 16, + ), + const SizedBox(width: 4), + Text( + '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: isCritical + ? Colors.red.shade600 + : isWarning + ? Colors.orange.shade600 + : Colors.blue.shade600, + ), + ), + ], + ); + }), + ], + ), + ), + ], + ), + body: Obx(() { + if (quizAttemptC.isQuizFinished.value) { + log('Redirecting to hasil quiz: isQuizFinished=true'); + Future.microtask(() { + if (Get.currentRoute != AppRoutes.quizSelesai) { + Get.offAllNamed(AppRoutes.quizSelesai, + arguments: {'quiz_id': quizAttemptC.quizIdRx.value}); + } + }); + return const Center(child: CircularProgressIndicator()); + } + if (quizQuestionC.isLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + var data = quizQuestionC.quizQuestionM?.data; + + if (data == null) { + return const Center( + child: Text( + 'Soal tidak tersedia atau sudah habis. Silakan kembali ke dashboard.', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ); + } + + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Container( + constraints: const BoxConstraints( + minHeight: 400, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.green.shade400, + Colors.green.shade300, + Colors.green.shade200, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.green.withOpacity(0.3), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // Kotak Soal + Container( + margin: const EdgeInsets.all(20), + padding: const EdgeInsets.all(20), + width: double.infinity, + constraints: const BoxConstraints( + minHeight: 120, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white, + Colors.grey.shade50, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(15), + ), + child: const Text( + "Soal", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ), + const SizedBox(height: 15), + Text( + data.pertanyaan, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + height: 1.4, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.visible, + softWrap: true, + ), + ], + ), + ), + const SizedBox(height: 20), + + buildJawaban( + context, data.id.toString(), "a", data.opsiA), + buildJawaban( + context, data.id.toString(), "b", data.opsiB), + buildJawaban( + context, data.id.toString(), "c", data.opsiC), + buildJawaban( + context, data.id.toString(), "d", data.opsiD), + + // Tambahan padding di bawah untuk menghindari overflow + const SizedBox(height: 40), + ], + ), + ), + ), + ), + ); + }), + ), + ); + } + + Widget buildJawaban( + BuildContext context, + String questionId, + String opsi, + String label, + ) { + return Obx(() { + if (quizAttemptC.isQuizFinished.value) { + return const SizedBox.shrink(); + } + bool isAnswered = quizAttemptC.isQuestionAnswered(questionId); + String? selectedAnswer = quizAttemptC.getSelectedAnswer(questionId); + bool isThisOptionSelected = selectedAnswer == opsi; + + // Tentukan warna berdasarkan status + Color backgroundColor; + Color textColor; + List gradientColors; + + if (isAnswered) { + if (isThisOptionSelected) { + // Jawaban yang dipilih - hijau + backgroundColor = Colors.green.shade100; + textColor = Colors.green.shade800; + gradientColors = [Colors.green.shade400, Colors.green.shade300]; + } else { + // Jawaban lain yang tidak dipilih - abu-abu + backgroundColor = Colors.grey.shade100; + textColor = Colors.grey.shade600; + gradientColors = [Colors.grey.shade400, Colors.grey.shade300]; + } + } else { + // Belum dijawab - putih + backgroundColor = Colors.white; + textColor = Colors.black87; + gradientColors = [Colors.green.shade400, Colors.green.shade300]; + } + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: ElevatedButton( + onPressed: isAnswered + ? null + : () async { + // Simpan jawaban yang dipilih + quizAttemptC.saveAnswer(questionId, opsi); + + log({ + "attempt_id": quizAttemptC.attemptId.value, + "question_id": questionId, + "opsi": opsi, + }.toString()); + + try { + await quizAttemptC.postQuizAttemptAnswer( + quizAttemptId: quizAttemptC.attemptId.value.toString(), + questionId: questionId, + jawabanSiswa: opsi, + ); + + // Log detailed information for debugging + log("Answer response received:"); + log("- Quiz Answer Model: ${quizAttemptC.quizAnswerM}"); + log("- Correct value: ${quizAttemptC.quizAnswerM?.data.correct}"); + log("- Quiz ID: ${quizAttemptC.quizIdRx.value}"); + log("- Is Last Question: ${quizAttemptC.isLastQuestion.value}"); + + // Ensure we have valid data before showing dialog + if (quizAttemptC.quizAnswerM != null) { + var correctValue = quizAttemptC.quizAnswerM!.data.correct; + log("About to show result dialog:"); + log("- Correct value: $correctValue (type: ${correctValue.runtimeType})"); + log("- Correct as string: '${correctValue.toString()}'"); + log("- Is correct == 1: ${correctValue == 1}"); + log("- Is correct == '1': ${correctValue.toString() == '1'}"); + + showResultDialog( + context, + correctValue.toString(), + "${opsi.toUpperCase()}. $label", + quizAttemptC.isLastQuestion.value, + quizAttemptC.quizIdRx.value.toString(), + ); + } else { + // Handle case where answer model is null + log("Quiz answer model is null, showing error dialog"); + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text("Error"), + content: const Text( + "Gagal memproses jawaban. Silakan coba lagi."), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("OK"), + ), + ], + ), + ); + } + } catch (e) { + log("Error posting answer: $e"); + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text("Error"), + content: const Text( + "Terjadi kesalahan saat mengirim jawaban. Silakan coba lagi."), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("OK"), + ), + ], + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: backgroundColor, + foregroundColor: textColor, + padding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + elevation: isAnswered ? 1 : 3, + shadowColor: Colors.transparent, // Shadow sudah di Container + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Container untuk option label (A, B, C, D) + Container( + width: 45, + height: 45, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: gradientColors, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(22.5), + boxShadow: [ + BoxShadow( + color: gradientColors[0].withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Center( + child: Text( + opsi.toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + ), + const SizedBox(width: 20), + // Answer text aligned with the option label + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 12), + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: textColor, + height: 1.3, + ), + overflow: TextOverflow.visible, + softWrap: true, + ), + ), + ), + // Icon untuk menunjukkan status + if (isAnswered && isThisOptionSelected) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Icon( + Icons.check_circle, + color: Colors.green.shade600, + size: 24, + ), + ), + ], + ), + ), + ); + }); + } + + void showLoadingDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AlertDialog( + backgroundColor: Colors.white, + content: SizedBox( + width: 50, + height: 50, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + ], + ), + ), + ), + ); + } + + void showResultDialog( + BuildContext context, + String isCorrect, + String answer, + bool isLastQuestion, + String quizId, + ) { + // Log the values for debugging + log("showResultDialog called with:"); + log("- isCorrect: '$isCorrect'"); + log("- answer: '$answer'"); + log("- isLastQuestion: $isLastQuestion"); + log("- quizId: '$quizId'"); + + // Determine if answer is correct + bool isAnswerCorrect = + isCorrect == "1" || isCorrect == "true" || isCorrect == "True"; + + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + backgroundColor: Colors.transparent, + elevation: 0, + content: Container( + width: double.maxFinite, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: isAnswerCorrect + ? [ + Colors.green.shade400, + Colors.green.shade300, + Colors.green.shade200, + ] + : [ + Colors.red.shade400, + Colors.red.shade300, + Colors.red.shade200, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: (isAnswerCorrect ? Colors.green : Colors.red) + .withOpacity(0.4), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Icon dan Status + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Icon( + isAnswerCorrect ? Icons.check_circle : Icons.cancel, + size: 50, + color: isAnswerCorrect + ? Colors.green.shade600 + : Colors.red.shade600, + ), + ), + const SizedBox(height: 20), + + // Text Status + Container( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + isAnswerCorrect ? 'Jawaban Benar!' : 'Jawaban Salah!', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: isAnswerCorrect + ? Colors.green.shade700 + : Colors.red.shade700, + ), + ), + ), + const SizedBox(height: 20), + + // Jawaban yang dipilih + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: isAnswerCorrect + ? Colors.green.shade300 + : Colors.red.shade300, + width: 2, + ), + ), + child: Column( + children: [ + Text( + 'Jawaban Anda:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isAnswerCorrect + ? Colors.green.shade600 + : Colors.red.shade600, + ), + ), + const SizedBox(height: 8), + Text( + answer, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + const SizedBox(height: 20), + + // Tombol + Container( + width: double.infinity, + child: ElevatedButton( + onPressed: () async { + Get.back(); + if (isLastQuestion) { + log("Quiz selesai, redirecting to finish page"); + log("Quiz ID: $quizId"); + log("Attempt ID: ${quizAttemptC.attemptId.value}"); + + snackbarSuccess("Quiz Selesai"); + Get.offAllNamed( + AppRoutes.quizSelesai, + arguments: {'quiz_id': quizId}, + ); + } else { + final prefs = await SharedPreferences.getInstance(); + final attemptId = prefs.getString('attempt_id'); + await quizQuestionC.getQuizQuestion(attemptId); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: isAnswerCorrect + ? Colors.green.shade600 + : Colors.red.shade600, + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + elevation: 3, + ), + child: Text( + isLastQuestion ? 'Lihat Hasil' : 'Lanjutkan', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + // Menampilkan konfirmasi saat pengguna mencoba keluar sebelum quiz selesai + Future _showExitConfirmationDialog(BuildContext context) async { + return await showDialog( + context: context, + barrierDismissible: + false, // Tidak bisa ditutup dengan mengetuk di luar dialog + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Konfirmasi'), + content: const Text( + 'Anda belum menyelesaikan quiz. Apakah Anda yakin ingin keluar?'), + actions: [ + TextButton( + child: const Text('Batal'), + onPressed: () { + Navigator.of(context).pop(false); // Jangan keluar + }, + ), + TextButton( + child: const Text('Keluar'), + onPressed: () { + Get.offAllNamed(AppRoutes.siswaDashboard); + }, + ), + ], + ); + }, + ) ?? + false; + } +} diff --git a/lib/views/siswa/quiz/soal_quiz_selesai.dart b/lib/views/siswa/quiz/soal_quiz_selesai.dart new file mode 100644 index 0000000..dc4acf6 --- /dev/null +++ b/lib/views/siswa/quiz/soal_quiz_selesai.dart @@ -0,0 +1,300 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_finish_controller.dart'; + +class SoalQuizSelesai extends StatelessWidget { + SoalQuizSelesai({super.key}); + QuizFinishController quizFinishC = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Quiz Selesai"), + backgroundColor: Colors.green.shade600, + foregroundColor: Colors.white, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.green.shade300, + borderRadius: BorderRadius.circular(30), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Container utama untuk hasil quiz + Container( + margin: const EdgeInsets.all(20), + padding: const EdgeInsets.all(20), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Obx( + () { + if (quizFinishC.isLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + var data = quizFinishC.quizAttemptM?.data; + + if (data == null) { + return const Center( + child: Text( + 'Data tidak tersedia', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + // Validasi agar benar + salah = total soal + int totalSoal = int.tryParse(data.jumlahSoal) ?? 0; + int jawabanBenar = int.tryParse(data.jawabanBenar) ?? 0; + int jawabanSalah = int.tryParse(data.jawabanSalah) ?? 0; + if (jawabanBenar + jawabanSalah != totalSoal) { + jawabanSalah = totalSoal - jawabanBenar; + } + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Header dengan icon + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.emoji_events, + size: 32, + color: Colors.amber.shade600, + ), + const SizedBox(width: 10), + const Text( + 'HASIL QUIZ', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + const SizedBox(height: 20), + + // Statistik pengerjaan + Container( + width: Get.width, + decoration: BoxDecoration( + color: const Color.fromARGB(255, 241, 235, 224), + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Statistik Pengerjaan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 12), + Text( + 'Total Soal Quiz: ${data.jumlahSoal}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF9CFFBA), + borderRadius: + BorderRadius.circular(15), + ), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + child: Column( + children: [ + const Icon( + Icons.check_circle, + color: Colors.green, + size: 24, + ), + const SizedBox(height: 4), + Text( + "${jawabanBenar}", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + const Text( + "Benar", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.green, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Container( + decoration: BoxDecoration( + color: const Color(0xFFFF8D85), + borderRadius: + BorderRadius.circular(15), + ), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + child: Column( + children: [ + const Icon( + Icons.cancel, + color: Colors.red, + size: 24, + ), + const SizedBox(height: 4), + Text( + "${jawabanSalah}", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + const Text( + "Salah", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.red, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 20), + + // Skor utama + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFF667EEA).withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + const Text( + 'Skor Anda', + style: TextStyle( + color: Colors.white70, + fontSize: 16, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 10), + Text( + data.skor, + style: const TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ); + }, + ), + ), + + const SizedBox(height: 40), + + // Tombol Kembali + ElevatedButton( + onPressed: () { + Get.offAllNamed(AppRoutes.siswaDashboard); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: + const EdgeInsets.symmetric(horizontal: 50, vertical: 15), + ), + child: const Text( + 'Kembali', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/siswa/ranking/bindings/matpel_rank_binding.dart b/lib/views/siswa/ranking/bindings/matpel_rank_binding.dart new file mode 100644 index 0000000..32c81ad --- /dev/null +++ b/lib/views/siswa/ranking/bindings/matpel_rank_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart'; + +class MatpelRankBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => MataPelajaranSimpleController()); + } +} diff --git a/lib/views/siswa/ranking/bindings/quiz_rank_binding.dart b/lib/views/siswa/ranking/bindings/quiz_rank_binding.dart new file mode 100644 index 0000000..c466704 --- /dev/null +++ b/lib/views/siswa/ranking/bindings/quiz_rank_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_controller.dart'; + +class QuizRankBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => QuizController()); + } +} diff --git a/lib/views/siswa/ranking/bindings/ranking_binding.dart b/lib/views/siswa/ranking/bindings/ranking_binding.dart new file mode 100644 index 0000000..21a0141 --- /dev/null +++ b/lib/views/siswa/ranking/bindings/ranking_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/ranking/controllers/ranking_controller.dart'; + +class RankingBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => RankingController()); + } +} diff --git a/lib/views/siswa/ranking/controllers/ranking_controller.dart b/lib/views/siswa/ranking/controllers/ranking_controller.dart new file mode 100644 index 0000000..3e85270 --- /dev/null +++ b/lib/views/siswa/ranking/controllers/ranking_controller.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:http/http.dart' as http; + +class RankingController extends GetxController { + var isLoading = false.obs; + var leaderboard = [].obs; + var myScore = ''.obs; + var myScoreTime = ''.obs; + var isEmpty = true.obs; + + @override + void onInit() { + super.onInit(); + var quizId = Get.arguments['quiz_id']; + getLeaderboard(quizId); + getMyScore(quizId); + } + + Future getLeaderboard(String quizId) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + final response = await http.get( + Uri.parse('${ApiConstants.baseUrlApi}/quiz/$quizId/ranking'), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json['success'] == true && json['ranking'] is List) { + leaderboard.value = json['ranking']; + isEmpty.value = leaderboard.isEmpty; + } else { + leaderboard.value = []; + isEmpty.value = true; + } + } else { + leaderboard.value = []; + isEmpty.value = true; + } + } catch (e) { + leaderboard.value = []; + isEmpty.value = true; + } finally { + isLoading(false); + } + } + + Future getMyScore(String quizId) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + final response = await http.get( + Uri.parse('${ApiConstants.baseUrlApi}/quiz/$quizId/skor-saya'), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json['success'] == true) { + myScore.value = json['skor'].toString(); + myScoreTime.value = json['waktu_selesai'] ?? ''; + } else { + myScore.value = '-'; + myScoreTime.value = ''; + } + } else { + myScore.value = '-'; + myScoreTime.value = ''; + } + } catch (e) { + myScore.value = '-'; + myScoreTime.value = ''; + } + } + + Future refreshData() async { + var quizId = Get.arguments['quiz_id']; + await getLeaderboard(quizId); + await getMyScore(quizId); + } +} diff --git a/lib/views/siswa/ranking/index.dart b/lib/views/siswa/ranking/index.dart new file mode 100644 index 0000000..4785bf8 --- /dev/null +++ b/lib/views/siswa/ranking/index.dart @@ -0,0 +1,480 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/views/siswa/ranking/controllers/ranking_controller.dart'; +import 'package:ui/widgets/my_text.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class RankSiswa extends StatelessWidget { + // final List> ranking = [ + // {"name": "Febry", "rank": 1, "score": 93}, + // {"name": "Udin", "rank": 2}, + // {"name": "Umar", "rank": 3}, + // {"name": "Dina", "rank": 4}, + // {"name": "Rafi", "rank": 5}, + // ]; + + RankSiswa({super.key}); + + Color getMedalColor(int rank) { + switch (rank) { + case 1: + return const Color(0xFFFFD700); // Gold + case 2: + return const Color(0xFFC0C0C0); // Silver + case 3: + return const Color(0xFFCD7F32); // Bronze + default: + return const Color(0xFF667EEA); // Default blue + } + } + + String getMedalEmoji(int rank) { + switch (rank) { + case 1: + return '🥇'; + case 2: + return '🥈'; + case 3: + return '🥉'; + default: + return '🎯'; + } + } + + RankingController rankingC = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: AppBar( + title: Text( + "Ranking Siswa", + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: Colors.white, + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + ), + ), + centerTitle: true, + ), + body: SafeArea( + child: RefreshIndicator( + onRefresh: () async { + await rankingC.refreshData(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Section + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFF667EEA).withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.emoji_events, + size: 32, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + const Text( + "🏆 Ranking Siswa", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18, + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 8), + Text( + Get.arguments['judul'] ?? "Quiz Ranking", + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: Colors.white, + fontFamily: 'Poppins', + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Ranking List + Expanded( + child: Obx(() { + if (rankingC.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF667EEA), + ), + strokeWidth: 3, + ), + ), + const SizedBox(height: 20), + const Text( + "Memuat data ranking...", + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ); + } + + if (rankingC.isEmpty.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(30), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF667EEA) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.emoji_events_outlined, + size: 60, + color: Color(0xFF667EEA), + ), + ), + const SizedBox(height: 20), + const MyText( + text: "Belum Ada Ranking", + fontSize: 18, + color: Colors.black87, + fontWeight: FontWeight.w700, + ), + const SizedBox(height: 8), + const Text( + "Belum ada siswa yang mengerjakan quiz ini", + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + ); + } + + var data = rankingC.leaderboard; + return Column( + children: [ + Expanded( + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: data.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + getMedalColor(index + 1) + .withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: getMedalColor(index + 1) + .withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + border: Border.all( + color: getMedalColor(index + 1) + .withOpacity(0.1), + width: 1, + ), + ), + child: Container( + padding: const EdgeInsets.all(15), + child: Row( + children: [ + // Rank Badge + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + getMedalColor(index + 1), + getMedalColor(index + 1) + .withOpacity(0.7), + ], + ), + borderRadius: + BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: getMedalColor(index + 1) + .withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Center( + child: Text( + getMedalEmoji(index + 1), + style: + const TextStyle(fontSize: 24), + ), + ), + ), + const SizedBox(width: 15), + // Student Info + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + data[index]['nama'], + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Poppins', + color: Color(0xFF2D3748), + ), + ), + const SizedBox(height: 4), + Text( + "Peringkat " + + (index + 1).toString(), + style: TextStyle( + fontSize: 14, + color: getMedalColor(index + 1), + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + // Score + Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 8, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + getMedalColor(index + 1), + getMedalColor(index + 1) + .withOpacity(0.7), + ], + ), + borderRadius: + BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: getMedalColor(index + 1) + .withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + children: [ + const Text( + "Skor", + style: TextStyle( + color: Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 2), + Text( + "${data[index]['skor']}", + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w700, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + const SizedBox(height: 20), + // Skor Pribadi - tampilan profesional + Container( + margin: const EdgeInsets.only(top: 8), + padding: const EdgeInsets.symmetric( + vertical: 24, horizontal: 24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.08), + blurRadius: 16, + offset: const Offset(0, 8), + ), + ], + border: Border.all( + color: + const Color(0xFF667EEA).withOpacity(0.12)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: + const Color(0xFF667EEA).withOpacity(0.12), + borderRadius: BorderRadius.circular(16), + ), + child: const Icon(Icons.person, + color: Color(0xFF667EEA), size: 36), + ), + const SizedBox(width: 18), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Skor Anda', + style: TextStyle( + color: Color(0xFF667EEA), + fontSize: 18, + fontWeight: FontWeight.bold, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 6), + Obx(() => Text( + rankingC.myScore.value, + style: const TextStyle( + color: Color(0xFF222B45), + fontSize: 36, + fontWeight: FontWeight.w900, + fontFamily: 'Poppins', + letterSpacing: 1.2, + ), + )), + ], + ), + ), + ], + ), + ), + ], + ); + }), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/siswa/ranking/matpel_rank.dart b/lib/views/siswa/ranking/matpel_rank.dart new file mode 100644 index 0000000..55082fe --- /dev/null +++ b/lib/views/siswa/ranking/matpel_rank.dart @@ -0,0 +1,481 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class MatpelRank extends StatelessWidget { + MatpelRank({super.key}); + MataPelajaranSimpleController matapelajaranSimpleC = + Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: AppBar( + title: const Text( + "Matapelajaran Quiz", + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: Colors.white, + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + ), + ), + centerTitle: true, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Section + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFF667EEA).withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.quiz, + color: Colors.white, + size: 30, + ), + ), + const SizedBox(width: 15), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Quiz Ranking", + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w700, + fontFamily: 'Poppins', + ), + ), + Text( + "Lihat peringkat quiz mata pelajaran", + style: TextStyle( + color: Colors.white70, + fontSize: 14, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 25), + + Expanded( + child: Obx( + () { + if (matapelajaranSimpleC.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF667EEA), + ), + strokeWidth: 3, + ), + ), + const SizedBox(height: 20), + const Text( + "Memuat data...", + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ); + } else if (matapelajaranSimpleC + .mataPelajaranSimpleM?.data.isEmpty ?? + true) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(30), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF667EEA) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.quiz_outlined, + size: 60, + color: Color(0xFF667EEA), + ), + ), + const SizedBox(height: 20), + const MyText( + text: "Tidak Ada Mata Pelajaran", + fontSize: 18, + color: Colors.black87, + fontWeight: FontWeight.w700, + ), + const SizedBox(height: 8), + const Text( + "Belum ada mata pelajaran yang tersedia", + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + ); + } else { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + ), + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: matapelajaranSimpleC + .mataPelajaranSimpleM?.data.length ?? + 0, + itemBuilder: (context, index) { + var data = matapelajaranSimpleC + .mataPelajaranSimpleM?.data[index]; + return TaskItem( + id: data!.id.toString(), + title: data.nama, + guru: data.guru.nama, + mataPelajaranId: data.id.toString(), + index: index, + ); + }, + ), + ); + } + }, + ), + ), + ], + ), + ), + ), + ); + } +} + +class TaskItem extends StatefulWidget { + final String id; + final String title; + final String guru; + final String mataPelajaranId; + final int index; + + const TaskItem({ + super.key, + required this.id, + required this.title, + required this.guru, + required this.mataPelajaranId, + required this.index, + }); + + @override + State createState() => _TaskItemState(); +} + +class _TaskItemState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + List _getGradientColors(int index) { + List> gradients = [ + [const Color(0xFF667EEA), const Color(0xFF764BA2)], + [const Color(0xFF43E97B), const Color(0xFF38F9D7)], + [const Color(0xFFFA709A), const Color(0xFFFEE140)], + [const Color(0xFF30CFD0), const Color(0xFFA8EDEA)], + [const Color(0xFFFFCCDB), const Color(0xFFFFEFBA)], + [const Color(0xFFFF9A9E), const Color(0xFFFECFEF)], + ]; + return gradients[index % gradients.length]; + } + + IconData _getSubjectIcon(String subject) { + String subjectLower = subject.toLowerCase(); + if (subjectLower.contains('matematika') || subjectLower.contains('math')) { + return Icons.calculate; + } else if (subjectLower.contains('fisika') || + subjectLower.contains('physics')) { + return Icons.science; + } else if (subjectLower.contains('kimia') || + subjectLower.contains('chemistry')) { + return Icons.biotech; + } else if (subjectLower.contains('biologi') || + subjectLower.contains('biology')) { + return Icons.eco; + } else if (subjectLower.contains('bahasa') || + subjectLower.contains('language')) { + return Icons.translate; + } else if (subjectLower.contains('sejarah') || + subjectLower.contains('history')) { + return Icons.history_edu; + } else if (subjectLower.contains('geografi') || + subjectLower.contains('geography')) { + return Icons.public; + } else if (subjectLower.contains('seni') || subjectLower.contains('art')) { + return Icons.palette; + } else if (subjectLower.contains('olahraga') || + subjectLower.contains('sport')) { + return Icons.sports; + } else { + return Icons.quiz; + } + } + + @override + Widget build(BuildContext context) { + final gradientColors = _getGradientColors(widget.index); + + return GestureDetector( + onTapDown: (_) { + _animationController.forward(); + }, + onTapUp: (_) { + _animationController.reverse(); + Get.toNamed(AppRoutes.matpelQuizRankDetail, arguments: { + 'matpel_id': widget.id, + 'matpel': widget.title, + }); + }, + onTapCancel: () { + _animationController.reverse(); + }, + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: gradientColors, + ), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: gradientColors[0].withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Container( + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.1), + Colors.white.withOpacity(0.05), + ], + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: Icon( + _getSubjectIcon(widget.title), + color: Colors.white, + size: 28, + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + fontFamily: 'Poppins', + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.person, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 6), + Flexible( + child: Text( + "Guru : ${widget.guru}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.leaderboard, + color: Colors.white, + size: 16, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/views/siswa/ranking/matpel_rank_detail.dart b/lib/views/siswa/ranking/matpel_rank_detail.dart new file mode 100644 index 0000000..b0e57e2 --- /dev/null +++ b/lib/views/siswa/ranking/matpel_rank_detail.dart @@ -0,0 +1,396 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/quiz/controllers/quiz_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class MatpelRankDetail extends StatelessWidget { + MatpelRankDetail({super.key}); + QuizController quizC = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: AppBar( + title: Text( + "Quiz ${Get.arguments['matpel']}", + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: Colors.white, + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + ), + ), + centerTitle: true, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Section + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFF667EEA).withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.quiz, + color: Colors.white, + size: 30, + ), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Quiz ${Get.arguments['matpel']}", + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w700, + fontFamily: 'Poppins', + ), + ), + const Text( + "Lihat peringkat quiz", + style: TextStyle( + color: Colors.white70, + fontSize: 14, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 25), + + Expanded( + child: Obx(() { + if (quizC.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF667EEA), + ), + strokeWidth: 3, + ), + ), + const SizedBox(height: 20), + const Text( + "Memuat data...", + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ); + } else if (quizC.isEmptyData.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(30), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF667EEA) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.quiz_outlined, + size: 60, + color: Color(0xFF667EEA), + ), + ), + const SizedBox(height: 20), + const MyText( + text: "Tidak Ada Quiz", + fontSize: 18, + color: Colors.black87, + fontWeight: FontWeight.w700, + ), + const SizedBox(height: 8), + const Text( + "Belum ada quiz yang tersedia", + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + ); + } else { + return ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: quizC.quizM?.data.length ?? 0, + itemBuilder: (context, index) { + var data = quizC.quizM?.data[index]; + return TaskItem( + id: data!.id.toString(), + title: data.judul, + index: index, + ); + }, + ); + } + }), + ), + ], + ), + ), + ), + ); + } +} + +class TaskItem extends StatefulWidget { + final String id; + final String title; + final int index; + + const TaskItem({ + super.key, + required this.id, + required this.title, + required this.index, + }); + + @override + State createState() => _TaskItemState(); +} + +class _TaskItemState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + List _getGradientColors(int index) { + List> gradients = [ + [const Color(0xFF667EEA), const Color(0xFF764BA2)], + [const Color(0xFF43E97B), const Color(0xFF38F9D7)], + [const Color(0xFFFA709A), const Color(0xFFFEE140)], + [const Color(0xFF30CFD0), const Color(0xFFA8EDEA)], + [const Color(0xFFFFCCDB), const Color(0xFFFFEFBA)], + [const Color(0xFFFF9A9E), const Color(0xFFFECFEF)], + ]; + return gradients[index % gradients.length]; + } + + @override + Widget build(BuildContext context) { + final gradientColors = _getGradientColors(widget.index); + + return GestureDetector( + onTapDown: (_) { + _animationController.forward(); + }, + onTapUp: (_) { + _animationController.reverse(); + Get.toNamed(AppRoutes.rankSiswa, arguments: { + 'quiz_id': widget.id, + 'matpel': Get.arguments['matpel'], + 'judul': widget.title, + }); + }, + onTapCancel: () { + _animationController.reverse(); + }, + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: gradientColors, + ), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: gradientColors[0].withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Container( + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.1), + Colors.white.withOpacity(0.05), + ], + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: const Icon( + Icons.quiz, + color: Colors.white, + size: 28, + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + fontFamily: 'Poppins', + color: Colors.white, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + size: 16, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/views/siswa/ruang_diskusi.dart b/lib/views/siswa/ruang_diskusi.dart new file mode 100644 index 0000000..d70a008 --- /dev/null +++ b/lib/views/siswa/ruang_diskusi.dart @@ -0,0 +1,125 @@ +// import 'package:flutter/material.dart'; +// import 'package:ui/services/user_services.dart'; +// import 'package:ui/models/user.dart'; + +// class RuangDiskusi extends StatefulWidget { +// @override +// _RuangDiskusiState createState() => _RuangDiskusiState(); +// } + +// class _RuangDiskusiState extends State { +// final UserService userService = UserService(); +// late Future> futureUsers; + +// @override +// void initState() { +// super.initState(); +// futureUsers = userService.getUsers(); +// } + +// void refreshUsers() { +// setState(() { +// futureUsers = userService.getUsers(); +// }); +// } + +// void _showForm({User? user}) { +// final TextEditingController nameController = TextEditingController(text: user?.name ?? ""); +// final TextEditingController emailController = TextEditingController(text: user?.email ?? ""); +// final TextEditingController roleController = TextEditingController(text: user?.role ?? "siswa"); + +// showDialog( +// context: context, +// builder: (context) => AlertDialog( +// title: Text(user == null ? "Tambah User" : "Edit User"), +// content: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// TextField(controller: nameController, decoration: InputDecoration(labelText: "Nama")), +// TextField(controller: emailController, decoration: InputDecoration(labelText: "Email")), +// TextField(controller: roleController, decoration: InputDecoration(labelText: "Role")), +// ], +// ), +// actions: [ +// ElevatedButton( +// onPressed: () async { +// Navigator.pop(context); +// if (user == null) { +// await userService.addUser(User(id: "", name: nameController.text, email: emailController.text, role: roleController.text)); +// } else { +// await userService.updateUser(user.id, User(id: user.id, name: nameController.text, email: emailController.text, role: roleController.text)); +// } +// refreshUsers(); +// }, +// child: Text(user == null ? "Tambah" : "Update"), +// ), +// ], +// ), +// ); +// } + +// void _confirmDelete(String id) { +// showDialog( +// context: context, +// builder: (context) => AlertDialog( +// title: Text("Hapus User?"), +// content: Text("Apakah kamu yakin ingin menghapus user ini?"), +// actions: [ +// TextButton( +// onPressed: () => Navigator.pop(context), +// child: Text("Batal"), +// ), +// ElevatedButton( +// onPressed: () async { +// Navigator.pop(context); +// await userService.deleteUser(id); +// refreshUsers(); +// }, +// child: Text("Hapus"), +// ), +// ], +// ), +// ); +// } + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar(title: Text("Ruang Diskusi")), +// body: FutureBuilder>( +// future: futureUsers, +// builder: (context, snapshot) { +// if (snapshot.connectionState == ConnectionState.waiting) { +// return Center(child: CircularProgressIndicator()); +// } else if (snapshot.hasError) { +// return Center(child: Text("Gagal memuat data")); +// } else if (!snapshot.hasData || snapshot.data!.isEmpty) { +// return Center(child: Text("Tidak ada data siswa")); +// } else { +// return ListView.builder( +// itemCount: snapshot.data!.length, +// itemBuilder: (context, index) { +// User user = snapshot.data![index]; +// return ListTile( +// title: Text(user.name), +// subtitle: Text("${user.email} - ${user.role}"), +// trailing: Row( +// mainAxisSize: MainAxisSize.min, +// children: [ +// IconButton(icon: Icon(Icons.edit), onPressed: () => _showForm(user: user)), +// IconButton(icon: Icon(Icons.delete), onPressed: () => _confirmDelete(user.id)), +// ], +// ), +// ); +// }, +// ); +// } +// }, +// ), +// floatingActionButton: FloatingActionButton( +// onPressed: () => _showForm(), +// child: Icon(Icons.add), +// ), +// ); +// } +// } diff --git a/lib/views/siswa/siswaDashboard.dart b/lib/views/siswa/siswaDashboard.dart new file mode 100644 index 0000000..0627513 --- /dev/null +++ b/lib/views/siswa/siswaDashboard.dart @@ -0,0 +1,413 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'dart:math' as math; +import 'dart:math' show Random; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/controllers/notifikasi_count_controller.dart'; +import 'package:ui/views/siswa/controllers/siswa_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class AnimatedBackground extends StatefulWidget { + const AnimatedBackground({super.key}); + + @override + State createState() => _AnimatedBackgroundState(); +} + +class _AnimatedBackgroundState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + final List _shapes = []; + final int _numberOfShapes = 4; + final Random _random = Random(); + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 15), + )..repeat(); + + // Initialize shapes with random angles and distances + for (int i = 0; i < _numberOfShapes; i++) { + _shapes.add(Shape( + angle: _random.nextDouble() * math.pi * 2, + distance: 150 + _random.nextDouble() * 250, + baseSize: 150 + _random.nextDouble() * 200, + speed: 0.1 + _random.nextDouble() * 0.2, + type: _random.nextBool() ? ShapeType.curve : ShapeType.triangle, + opacity: 0.1, + opacitySpeed: 0.1 + _random.nextDouble() * 0.3, + sizeSpeed: 0.2 + _random.nextDouble() * 0.3, + angleSpeed: 0.05 + _random.nextDouble() * 0.2, + )); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return CustomPaint( + painter: BackgroundPainter( + shapes: _shapes, + animation: _controller.value, + ), + child: Container(), + ); + }, + ); + } +} + +class Shape { + double angle; + double distance; + double size; + final double baseSize; + final double speed; + final ShapeType type; + double opacity; + double opacitySpeed; + double sizeSpeed; + double angleSpeed; + + Shape({ + required this.angle, + required this.distance, + required this.baseSize, + required this.speed, + required this.type, + required this.opacity, + required this.opacitySpeed, + required this.sizeSpeed, + required this.angleSpeed, + }) : size = baseSize; +} + +enum ShapeType { + curve, + triangle, +} + +class BackgroundPainter extends CustomPainter { + final List shapes; + final double animation; + + BackgroundPainter({ + required this.shapes, + required this.animation, + }); + + @override + void paint(Canvas canvas, Size size) { + if (size.isEmpty) return; + + final centerX = size.width / 2; + final centerY = size.height / 2; + + for (var shape in shapes) { + try { + // Update opacity with sine wave for smooth fading + shape.opacity = 0.1 + + (math.sin(animation * math.pi * 2 * shape.opacitySpeed) * 0.15); + shape.opacity = + shape.opacity.clamp(0.0, 1.0); // Ensure opacity is between 0 and 1 + + // Update size with sine wave for pulsing effect + shape.size = shape.baseSize * + (0.8 + (math.sin(animation * math.pi * 2 * shape.sizeSpeed) * 0.3)); + shape.size = + shape.size.clamp(1.0, double.infinity); // Ensure size is positive + + // Update angle for rotation with varying speed + shape.angle += shape.angleSpeed * 0.02; + + final paint = Paint() + ..color = Colors.green.withOpacity(shape.opacity) + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + // Calculate position based on angle and distance from center + final x = centerX + math.cos(shape.angle) * shape.distance; + final y = centerY + math.sin(shape.angle) * shape.distance; + + // Ensure coordinates are within bounds + if (x.isFinite && y.isFinite) { + // Draw shape based on type + final path = Path(); + if (shape.type == ShapeType.curve) { + path + ..moveTo(x, y) + ..quadraticBezierTo( + x + shape.size * 0.5, + y - shape.size * 0.5, + x + shape.size, + y, + ) + ..quadraticBezierTo( + x + shape.size * 1.5, + y + shape.size * 0.5, + x, + y, + ); + } else { + // Triangle + final triangleSize = shape.size * 1.2; + path + ..moveTo(x, y - triangleSize) + ..lineTo(x + triangleSize, y + triangleSize * 0.5) + ..lineTo(x - triangleSize, y + triangleSize * 0.5) + ..close(); + } + + canvas.drawPath(path, paint); + } + } catch (e) { + debugPrint('Error painting shape: $e'); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} + +class SiswaDashboardPage extends StatelessWidget { + const SiswaDashboardPage({super.key}); + + @override + Widget build(BuildContext context) { + final siswaC = Get.find(); + final notifC = Get.find(); + + final List<_DashboardMenu> menuItems = [ + _DashboardMenu( + title: "Mata Pelajaran", + icon: Icons.menu_book_rounded, + color: Colors.teal.shade300, + onTap: () => Get.toNamed(AppRoutes.kelasmatapelajarans), + ), + _DashboardMenu( + title: "#1 Ranking", + icon: Icons.emoji_events_rounded, + color: Colors.indigo.shade200, + onTap: () => Get.toNamed(AppRoutes.matpelRankQuiz), + ), + _DashboardMenu( + title: "Quiz", + icon: Icons.quiz_rounded, + color: Colors.green.shade300, + onTap: () => Get.toNamed(AppRoutes.matpelQuiz), + ), + _DashboardMenu( + title: "Profil", + icon: Icons.person_rounded, + color: Colors.amber.shade200, + onTap: () => Get.toNamed(AppRoutes.profileSiswa), + ), + _DashboardMenu( + title: "Tugas", + icon: Icons.assignment_rounded, + color: Colors.teal.shade400, + onTap: () => Get.toNamed(AppRoutes.tugasSiswa), + ), + _DashboardMenu( + title: "Notifikasi", + icon: Icons.notifications_active_rounded, + color: Colors.pink.shade200, + onTap: () => Get.toNamed(AppRoutes.notifikasiSiswa)?.then((_) { + siswaC.getMe(); + notifC.getNotifCount(); + }), + ), + ]; + + // return WillPopScope( + // onWillPop: () async { + // return snackbarAlert("a", "Klik 2x untuk keluar", Colors.black); + // final shouldLogout = await showDialog( + // context: context, + // builder: (context) => AlertDialog( + // title: const Text('Konfirmasi Logout'), + // content: const Text('Apakah anda ingin Logout?'), + // actions: [ + // TextButton( + // onPressed: () => Navigator.of(context).pop(false), + // child: const Text('Batal'), + // ), + // TextButton( + // onPressed: () => Navigator.of(context).pop(true), + // child: const Text( + // 'Logout', + // style: TextStyle(color: Colors.red), + // ), + // ), + // ], + // ), + // ); + // if (shouldLogout == true) { + // siswaC.logout(role: "siswa"); + // } + // return false; + // }, + return Scaffold( + backgroundColor: Colors.grey[100], + body: Stack( + children: [ + const AnimatedBackground(), + SafeArea( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Obx(() { + if (siswaC.isLoading.value) { + return const CircularProgressIndicator(); + } + var user = siswaC.dataUser['user']; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "E-Learning", + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.w600), + ), + Text( + "Hi, ${user?['nama'] ?? 'Siswa'}", + style: const TextStyle( + fontSize: 28, fontWeight: FontWeight.bold), + ), + Text( + "Kelas : ${user?['kelas']}", + style: const TextStyle( + fontSize: 15, fontWeight: FontWeight.bold), + ), + ], + ); + }), + const SizedBox(height: 30), + + // Menu Grid + Obx( + () { + if (notifC.isLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return Expanded( + child: RefreshIndicator( + onRefresh: () { + notifC.getNotifCount(); + siswaC.getMe(); + return Future.value(); + }, + child: GridView.count( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + children: menuItems.map((item) { + return GestureDetector( + onTap: item.onTap, + child: Stack( + fit: StackFit.expand, + clipBehavior: Clip.none, + children: [ + Positioned( + child: Container( + decoration: BoxDecoration( + color: item.color, + borderRadius: + BorderRadius.circular(20), + ), + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon(item.icon, + size: 40, color: Colors.white), + const SizedBox(height: 12), + Text( + item.title, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + if (item.title == "Notifikasi" && + notifC.notifCount.value > 0) + Positioned( + top: -7, + right: -5, + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: Colors.red, + borderRadius: + BorderRadius.circular(20), + ), + child: Center( + child: MyText( + text: notifC.notifCount.value + .toString(), + textAlign: TextAlign.center, + fontSize: 13, + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ), + ); + }, + ), + ], + ), + ), + ), + // ), + ], + ), + ); + } +} + +class _DashboardMenu { + final String title; + final IconData icon; + final Color color; + final VoidCallback onTap; + + _DashboardMenu({ + required this.title, + required this.icon, + required this.color, + required this.onTap, + }); +} diff --git a/lib/views/siswa/tugas/bindings/detail_tugas_binding.dart b/lib/views/siswa/tugas/bindings/detail_tugas_binding.dart new file mode 100644 index 0000000..1e3a780 --- /dev/null +++ b/lib/views/siswa/tugas/bindings/detail_tugas_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/tugas/controllers/tugas_controller.dart'; + +class DetailTugasBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => TugasController()); + } +} diff --git a/lib/views/siswa/tugas/bindings/submit_tugas_binding.dart b/lib/views/siswa/tugas/bindings/submit_tugas_binding.dart new file mode 100644 index 0000000..7f5e0e3 --- /dev/null +++ b/lib/views/siswa/tugas/bindings/submit_tugas_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/tugas/controllers/submit_tugas_controller.dart'; + +class SubmitTugasBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SubmitTugasController()); + } +} diff --git a/lib/views/siswa/tugas/bindings/tugas_binding.dart b/lib/views/siswa/tugas/bindings/tugas_binding.dart new file mode 100644 index 0000000..cacd0b2 --- /dev/null +++ b/lib/views/siswa/tugas/bindings/tugas_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart'; + +class TugasBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => MataPelajaranSimpleController()); + } +} diff --git a/lib/views/siswa/tugas/controllers/submit_tugas_controller.dart b/lib/views/siswa/tugas/controllers/submit_tugas_controller.dart new file mode 100644 index 0000000..63674b6 --- /dev/null +++ b/lib/views/siswa/tugas/controllers/submit_tugas_controller.dart @@ -0,0 +1,110 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/widgets/my_snackbar.dart'; + +class SubmitTugasController extends GetxController { + var isLoading = false.obs; + + Future postTugas({ + required String tugasId, + String? text, + File? file, + }) async { + log(tugasId.toString()); + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + final nisn = prefs.getString('nisn'); + + if (token == null) { + throw Exception("Token not found"); + } + + try { + isLoading(true); + + var uri = Uri.parse(ApiConstants.submitTugasEnpoint); + var request = http.MultipartRequest('POST', uri); + request.headers['Authorization'] = 'Bearer $token'; + + request.fields['tugas_id'] = tugasId; + request.fields['nisn'] = nisn!; + + if (text != null && text.isNotEmpty) { + request.fields['text'] = text; + } + + if (file != null) { + request.files.add(await http.MultipartFile.fromPath('file', file.path)); + } + + var streamedResponse = await request.send(); + var response = await http.Response.fromStream(streamedResponse); + + if (response.statusCode == 200) { + Get.back(result: true); + snackbarSuccess("Berhasil mengirim tugas"); + } else { + log("Submit error: ${response.statusCode} ${response.body}"); + snackbarfailed("Gagal mengirim tugas"); + } + } catch (e) { + log("Submit Exception: $e"); + snackbarfailed("Terjadi kesalahan saat submit"); + } finally { + isLoading(false); + } + } + + Future updateTugas({ + required int id, + String? text, + File? file, + }) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + final nisn = prefs.getString('nisn'); + + if (token == null) { + throw Exception("Token not found"); + } + + try { + isLoading(true); + + var uri = Uri.parse("${ApiConstants.updateTugasEnpoint}?id=$id"); + var request = http.MultipartRequest('POST', uri); + request.headers['Authorization'] = 'Bearer $token'; + + request.fields['nisn'] = nisn!; + + if (text != null && text.isNotEmpty) { + request.fields['text'] = text; + } + + if (file != null) { + request.files.add(await http.MultipartFile.fromPath('file', file.path)); + } + + var streamedResponse = await request.send(); + var response = await http.Response.fromStream(streamedResponse); + + if (response.statusCode == 200) { + Get.back(result: true); + snackbarSuccess("Berhasil update tugas"); + } else { + log("Submit error: ${response.statusCode} ${response.body}"); + snackbarfailed("Gagal update tugas"); + } + } catch (e) { + log("Submit Exception: $e"); + snackbarfailed("Terjadi kesalahan saat update"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/siswa/tugas/controllers/tugas_controller.dart b/lib/views/siswa/tugas/controllers/tugas_controller.dart new file mode 100644 index 0000000..b2bd4e9 --- /dev/null +++ b/lib/views/siswa/tugas/controllers/tugas_controller.dart @@ -0,0 +1,148 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/models/tugas_model.dart'; +import 'package:http/http.dart' as http; +import 'package:ui/widgets/my_snackbar.dart'; + +class TugasController extends GetxController { + TugasModel? tugasM; + var isLoading = false.obs; + + Future getTugas({required id, required type}) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString('token'); + + if (token == null) { + throw Exception("Token not found"); + } + final headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }; + try { + isLoading(true); + + final url = "${ApiConstants.tugasEnpoint}?id_matpel=$id&type_tugas=$type"; + log("Requesting tugas URL: $url"); + log("Parameters - id_matpel: $id, type_tugas: $type"); + + final response = await http.get( + Uri.parse(url), + headers: headers, + ); + + log("Response status: ${response.statusCode}"); + log("Response body: ${response.body}"); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + log("Parsed JSON: $json"); + + // Cek apakah response adalah object atau array + if (json is Map) { + // Format response dengan status dan message + log("Response is Map"); + log("Keys in response: ${json.keys.toList()}"); + + if (json['status'] == true) { + try { + // Log struktur data sebelum parsing + if (json['data'] != null) { + log("Data field type: ${json['data'].runtimeType}"); + if (json['data'] is List) { + log("Data list length: ${json['data'].length}"); + if (json['data'].isNotEmpty) { + log("First data item type: ${json['data'][0].runtimeType}"); + log("First data item: ${json['data'][0]}"); + + // Log detail submit_tugas + var firstItem = json['data'][0]; + if (firstItem['submit_tugas'] != null) { + log("submit_tugas type: ${firstItem['submit_tugas'].runtimeType}"); + log("submit_tugas value: ${firstItem['submit_tugas']}"); + if (firstItem['submit_tugas'] is List) { + log("submit_tugas list length: ${firstItem['submit_tugas'].length}"); + if (firstItem['submit_tugas'].isNotEmpty) { + log("First submit_tugas item: ${firstItem['submit_tugas'][0]}"); + } + } + } + } + } + } + + tugasM = TugasModel.fromJson(json); + log("Tugas model created successfully"); + log("Data length: ${tugasM?.data.length ?? 0}"); + + // Log deskripsi untuk setiap tugas + if (tugasM?.data.isNotEmpty == true) { + for (int i = 0; i < tugasM!.data.length; i++) { + var tugas = tugasM!.data[i]; + log("Tugas ${i + 1} - ID: ${tugas.id}, Nama: ${tugas.nama}"); + log("Tugas ${i + 1} - Deskripsi: ${tugas.deskripsi ?? 'null'}"); + } + } + } catch (parseError) { + log("Error parsing TugasModel: $parseError"); + log("JSON structure: $json"); + snackbarfailed("Gagal memparse data tugas: $parseError"); + } + } else { + log("API returned false status: ${json['message']}"); + snackbarfailed("Gagal memuat data tugas: ${json['message']}"); + } + } else if (json is List) { + // Format response langsung array + log("Response is List"); + log("List length: ${json.length}"); + if (json.isNotEmpty) { + log("First item type: ${json[0].runtimeType}"); + log("First item: ${json[0]}"); + } + + try { + // Validasi bahwa setiap item dalam array adalah Map + for (int i = 0; i < json.length; i++) { + if (json[i] is! Map) { + log("Item at index $i is not a Map: ${json[i].runtimeType}"); + throw Exception("Invalid data format at index $i"); + } + } + + final wrappedJson = { + "status": true, + "message": "Success", + "data": json + }; + tugasM = TugasModel.fromJson(wrappedJson); + log("Tugas model created from array response"); + log("Data length: ${tugasM?.data.length ?? 0}"); + } catch (parseError) { + log("Error parsing array response: $parseError"); + log("Array structure: $json"); + snackbarfailed("Gagal memparse data tugas: $parseError"); + } + } else { + log("Unexpected response format: ${json.runtimeType}"); + snackbarfailed("Format response tidak dikenali"); + } + } else { + log("Terjadi kesalahan get data: ${response.statusCode}"); + log("Error response: ${response.body}"); + snackbarfailed( + "Gagal memuat data tugas. Status: ${response.statusCode}"); + } + } catch (e) { + log("Error get tugas: $e"); + log("Error stack trace: ${StackTrace.current}"); + snackbarfailed("Terjadi kesalahan: $e"); + } finally { + isLoading(false); + } + } +} diff --git a/lib/views/siswa/tugas/tugas.dart b/lib/views/siswa/tugas/tugas.dart new file mode 100644 index 0000000..0808cde --- /dev/null +++ b/lib/views/siswa/tugas/tugas.dart @@ -0,0 +1,478 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/matapelajaran/controllers/mata_pelajaran_simple_controller.dart'; +import 'package:ui/widgets/my_text.dart'; + +class Tugas extends StatelessWidget { + Tugas({super.key}); + MataPelajaranSimpleController matapelajaranSimpleC = + Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: AppBar( + title: const Text( + "Tugas", + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: Colors.white, + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + ), + ), + centerTitle: true, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Section + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFF667EEA).withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.assignment, + color: Colors.white, + size: 30, + ), + ), + const SizedBox(width: 15), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Daftar Tugas", + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w700, + fontFamily: 'Poppins', + ), + ), + Text( + "Pilih mata pelajaran untuk melihat tugas", + style: TextStyle( + color: Colors.white70, + fontSize: 14, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 25), + + Expanded( + child: Obx( + () { + if (matapelajaranSimpleC.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF667EEA), + ), + strokeWidth: 3, + ), + ), + const SizedBox(height: 20), + const Text( + "Memuat data...", + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ); + } else if (matapelajaranSimpleC + .mataPelajaranSimpleM?.data.isEmpty ?? + true) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(30), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF667EEA) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon( + Icons.assignment_outlined, + size: 60, + color: Color(0xFF667EEA), + ), + ), + const SizedBox(height: 20), + const MyText( + text: "Tidak Ada Mata Pelajaran", + fontSize: 18, + color: Colors.black87, + fontWeight: FontWeight.w700, + ), + const SizedBox(height: 8), + const Text( + "Belum ada mata pelajaran yang tersedia", + style: TextStyle( + fontSize: 14, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ], + ), + ); + } else { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + ), + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: matapelajaranSimpleC + .mataPelajaranSimpleM?.data.length ?? + 0, + itemBuilder: (context, index) { + var data = matapelajaranSimpleC + .mataPelajaranSimpleM?.data[index]; + return TaskItem( + id: data!.id.toString(), + title: data.nama, + guru: data.guru.nama, + mataPelajaranId: data.id.toString(), + index: index, + ); + }, + ), + ); + } + }, + ), + ), + ], + ), + ), + ), + ); + } +} + +class TaskItem extends StatefulWidget { + final String id; + final String title; + final String guru; + final String mataPelajaranId; + final int index; + + const TaskItem({ + super.key, + required this.id, + required this.title, + required this.guru, + required this.mataPelajaranId, + required this.index, + }); + + @override + State createState() => _TaskItemState(); +} + +class _TaskItemState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + List _getGradientColors(int index) { + List> gradients = [ + [const Color(0xFF667EEA), const Color(0xFF764BA2)], + [const Color(0xFF43E97B), const Color(0xFF38F9D7)], + [const Color(0xFFFA709A), const Color(0xFFFEE140)], + [const Color(0xFF30CFD0), const Color(0xFFA8EDEA)], + [const Color(0xFFFFCCDB), const Color(0xFFFFEFBA)], + [const Color(0xFFFF9A9E), const Color(0xFFFECFEF)], + ]; + return gradients[index % gradients.length]; + } + + IconData _getSubjectIcon(String subject) { + String subjectLower = subject.toLowerCase(); + if (subjectLower.contains('matematika') || subjectLower.contains('math')) { + return Icons.calculate; + } else if (subjectLower.contains('fisika') || + subjectLower.contains('physics')) { + return Icons.science; + } else if (subjectLower.contains('kimia') || + subjectLower.contains('chemistry')) { + return Icons.biotech; + } else if (subjectLower.contains('biologi') || + subjectLower.contains('biology')) { + return Icons.eco; + } else if (subjectLower.contains('bahasa') || + subjectLower.contains('language')) { + return Icons.translate; + } else if (subjectLower.contains('sejarah') || + subjectLower.contains('history')) { + return Icons.history_edu; + } else if (subjectLower.contains('geografi') || + subjectLower.contains('geography')) { + return Icons.public; + } else if (subjectLower.contains('seni') || subjectLower.contains('art')) { + return Icons.palette; + } else if (subjectLower.contains('olahraga') || + subjectLower.contains('sport')) { + return Icons.sports; + } else { + return Icons.book; + } + } + + @override + Widget build(BuildContext context) { + final gradientColors = _getGradientColors(widget.index); + + return GestureDetector( + onTapDown: (_) { + _animationController.forward(); + }, + onTapUp: (_) { + _animationController.reverse(); + Get.toNamed(AppRoutes.tugasDetailSiswa, arguments: widget.id); + }, + onTapCancel: () { + _animationController.reverse(); + }, + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: gradientColors, + ), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: gradientColors[0].withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Container( + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.1), + Colors.white.withOpacity(0.05), + ], + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(18), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: Icon( + _getSubjectIcon(widget.title), + color: Colors.white, + size: 28, + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + fontFamily: 'Poppins', + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.person, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 6), + Flexible( + child: Text( + "Guru : ${widget.guru}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + size: 16, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/views/siswa/tugas/tugas_commit.dart b/lib/views/siswa/tugas/tugas_commit.dart new file mode 100644 index 0000000..3684c3f --- /dev/null +++ b/lib/views/siswa/tugas/tugas_commit.dart @@ -0,0 +1,789 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/constans/api_constans.dart'; +import 'package:ui/views/siswa/tugas/controllers/submit_tugas_controller.dart'; +import 'package:ui/widgets/my_text.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:ui/widgets/my_date_format.dart'; + +class TugasCommit extends StatefulWidget { + const TugasCommit({super.key}); + + @override + State createState() => _TugasCommitState(); +} + +enum SubmissionMethod { file, text } + +class _TugasCommitState extends State + with TickerProviderStateMixin { + String? fileName; + File? selectedFile; + SubmissionMethod _method = SubmissionMethod.file; + final TextEditingController _textController = TextEditingController(); + SubmitTugasController submitTugasC = Get.find(); + + late AnimationController _scaleAnimationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + + _scaleAnimationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _scaleAnimationController, + curve: Curves.easeInOut, + )); + + if (Get.arguments['submitTugas'] != null) { + if (Get.arguments['submitTugas']['text'] == null) { + _method = SubmissionMethod.file; + fileName = Get.arguments['submitTugas']['file']; + } else { + _method = SubmissionMethod.text; + _textController.text = Get.arguments['submitTugas']['text']; + } + setState(() {}); + } + } + + @override + void dispose() { + _scaleAnimationController.dispose(); + super.dispose(); + } + + Future _launchUrl(String url) async { + final uri = Uri.parse(url); + if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { + throw Exception('Could not launch $url'); + } + } + + Future pickFile() async { + FilePickerResult? result = await FilePicker.platform.pickFiles(); + + if (result != null && result.files.single.path != null) { + setState(() { + fileName = result.files.single.name; + selectedFile = File(result.files.single.path!); + }); + } + } + + void submitTask() { + if (_method == SubmissionMethod.file) { + if (fileName != null) { + if (Get.arguments['submitTugas'] != null) { + submitTugasC.updateTugas( + id: Get.arguments['submitTugas']['id'], + file: selectedFile, + text: _textController.text, + ); + } else { + submitTugasC.postTugas( + tugasId: Get.arguments['id'].toString(), + text: _textController.text, + file: selectedFile, + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Silakan tambahkan file terlebih dahulu")), + ); + } + } else { + if (_textController.text.trim().isNotEmpty) { + if (Get.arguments['submitTugas'] != null) { + submitTugasC.updateTugas( + id: Get.arguments['submitTugas']['id'], + file: selectedFile, + text: _textController.text, + ); + } else { + submitTugasC.postTugas( + tugasId: Get.arguments['id'].toString(), + text: _textController.text, + file: selectedFile, + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Silakan tulis tugas terlebih dahulu")), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: AppBar( + title: const Text( + "Pengumpulan Tugas", + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + ), + ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: () => Get.back(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Soal + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF6366F1), + Color(0xFF8B5CF6), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFF6366F1).withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.assignment, + size: 32, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + const Text( + "Soal Tugas", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 8), + Text( + Get.arguments['title'], + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 8), + Text( + Get.arguments['deskripsi'] ?? 'Tidak ada deskripsi tugas.', + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 12, + color: Colors.white70, + fontFamily: 'Poppins', + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ), + + if (Get.arguments['submitTugas'] != null && + Get.arguments['submitTugas']['created_at'] != null) + Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + child: Row( + children: [ + const Icon(Icons.access_time, size: 16, color: Colors.grey), + const SizedBox(width: 4), + Text( + DateTime.parse(Get.arguments['submitTugas']['created_at']) + .fullDateTime(), + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Pilihan Metode Pengumpulan + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF6366F1).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.upload_file, + color: Color(0xFF6366F1), + size: 20, + ), + ), + const SizedBox(width: 12), + const Text( + "Metode Pengumpulan", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.black87, + fontFamily: 'Poppins', + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _method = SubmissionMethod.file; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 12), + decoration: BoxDecoration( + gradient: _method == SubmissionMethod.file + ? const LinearGradient( + colors: [ + Color(0xFF6366F1), + Color(0xFF8B5CF6) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: _method == SubmissionMethod.file + ? null + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: _method == SubmissionMethod.file + ? Colors.transparent + : Colors.grey.shade300, + width: 1, + ), + ), + child: Column( + children: [ + Icon( + Icons.file_upload, + color: _method == SubmissionMethod.file + ? Colors.white + : Colors.grey.shade600, + size: 24, + ), + const SizedBox(height: 8), + Text( + "Upload File", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: _method == SubmissionMethod.file + ? Colors.white + : Colors.grey.shade700, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _method = SubmissionMethod.text; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 12), + decoration: BoxDecoration( + gradient: _method == SubmissionMethod.text + ? const LinearGradient( + colors: [ + Color(0xFF10B981), + Color(0xFF059669) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: _method == SubmissionMethod.text + ? null + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: _method == SubmissionMethod.text + ? Colors.transparent + : Colors.grey.shade300, + width: 1, + ), + ), + child: Column( + children: [ + Icon( + Icons.edit_note, + color: _method == SubmissionMethod.text + ? Colors.white + : Colors.grey.shade600, + size: 24, + ), + const SizedBox(height: 8), + Text( + "Tulis Tugas", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: _method == SubmissionMethod.text + ? Colors.white + : Colors.grey.shade700, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Area Input + if (_method == SubmissionMethod.file) + GestureDetector( + onTap: pickFile, + onTapDown: (_) => _scaleAnimationController.forward(), + onTapUp: (_) => _scaleAnimationController.reverse(), + onTapCancel: () => _scaleAnimationController.reverse(), + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.blue.shade50, + Colors.blue.shade100, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.blue.shade200, + width: 2, + style: BorderStyle.solid, + ), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade500, + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.cloud_upload, + size: 32, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + Text( + fileName ?? "Pilih File", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: fileName != null + ? Colors.blue.shade700 + : Colors.blue.shade600, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 8), + Text( + fileName != null + ? "File siap diupload" + : "Tap untuk memilih file", + style: TextStyle( + fontSize: 14, + color: Colors.blue.shade500, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ); + }, + ), + ) + else + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.green.shade50, + Colors.green.shade100, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade500, + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.edit_note, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + const Text( + "Tulis Jawaban Tugas", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.black87, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: TextField( + controller: _textController, + maxLines: 12, + style: const TextStyle( + fontSize: 14, + fontFamily: 'Poppins', + ), + decoration: InputDecoration( + hintText: "Tulis jawaban tugas Anda di sini...", + hintStyle: TextStyle( + color: Colors.grey.shade400, + fontFamily: 'Poppins', + ), + filled: true, + fillColor: Colors.grey.shade50, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: Colors.green.shade300, + width: 2, + ), + ), + ), + ), + ), + ], + ), + ), + + // File yang sudah diupload (untuk tugas selesai) + if (Get.arguments['tipe_tugas'] == "selesai" && + Get.arguments['submitTugas']['text'] == null) + Container( + margin: const EdgeInsets.only(top: 20), + child: GestureDetector( + onTap: () { + _launchUrl( + "${ApiConstants.baseUrl}/storage/${Get.arguments['submitTugas']['file']}"); + }, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.orange.shade50, + Colors.orange.shade100, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.orange.shade200, + width: 1, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade500, + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.file_present, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "File yang Sudah Diupload", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Colors.black87, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 4), + Text( + "Tap untuk melihat file", + style: TextStyle( + fontSize: 12, + color: Colors.orange.shade600, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + color: Colors.orange.shade600, + size: 16, + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 30), + + // Tombol Submit + Obx(() { + return GestureDetector( + onTap: submitTugasC.isLoading.value ? null : submitTask, + onTapDown: (_) { + if (!submitTugasC.isLoading.value) { + _scaleAnimationController.forward(); + } + }, + onTapUp: (_) { + if (!submitTugasC.isLoading.value) { + _scaleAnimationController.reverse(); + } + }, + onTapCancel: () { + if (!submitTugasC.isLoading.value) { + _scaleAnimationController.reverse(); + } + }, + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 18), + decoration: BoxDecoration( + gradient: submitTugasC.isLoading.value + ? null + : const LinearGradient( + colors: [ + Color(0xFF10B981), + Color(0xFF059669) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + color: submitTugasC.isLoading.value + ? Colors.grey.shade300 + : null, + borderRadius: BorderRadius.circular(20), + boxShadow: submitTugasC.isLoading.value + ? null + : [ + BoxShadow( + color: const Color(0xFF10B981) + .withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (submitTugasC.isLoading.value) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.grey, + ), + ), + ) + else + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.send, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + submitTugasC.isLoading.value + ? "Menyimpan..." + : "Kirim Tugas", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: submitTugasC.isLoading.value + ? Colors.grey.shade600 + : Colors.white, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ); + }, + ), + ); + }), + ], + ), + ), + ); + } +} diff --git a/lib/views/siswa/tugas/tugas_detail.dart b/lib/views/siswa/tugas/tugas_detail.dart new file mode 100644 index 0000000..97647d3 --- /dev/null +++ b/lib/views/siswa/tugas/tugas_detail.dart @@ -0,0 +1,1036 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ntp/ntp.dart'; +import 'package:ui/routes/app_routes.dart'; +import 'package:ui/views/siswa/tugas/controllers/tugas_controller.dart'; +import 'package:ui/widgets/my_date_format.dart'; +import 'package:ui/widgets/my_snackbar.dart'; +import 'package:ui/widgets/my_text.dart'; +import 'dart:developer'; + +class TugasDetail extends StatefulWidget { + const TugasDetail({super.key}); + + @override + State createState() => _TugasDetailState(); +} + +class _TugasDetailState extends State { + TugasController tugasC = Get.find(); + var isActive = "belum"; + DateTime? dateNow; + + @override + void initState() { + super.initState(); + log("TugasDetail initState called"); + log("Arguments received: " + Get.arguments.toString()); + log("Arguments type: " + Get.arguments.runtimeType.toString()); + dynamic arg = Get.arguments; + String? tugasId; + if (arg is String) { + tugasId = arg; + } else if (arg is Map && arg['id'] != null) { + tugasId = arg['id'].toString(); + } else if (arg is int) { + tugasId = arg.toString(); + } + print( + '[DETAIL] ID diterima di detail: $tugasId ( [36m${tugasId.runtimeType} [0m)'); + if (tugasId != null) { + tugasC.getTugas(id: tugasId, type: "belum"); + } + } + + Future getCurrentTime() async { + DateTime now = await NTP.now(); + dateNow = DateTime(now.year, now.month, now.day); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF3F4F6), + appBar: AppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.assignment, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + "Detail Tugas", + style: const TextStyle( + fontWeight: FontWeight.w800, + fontSize: 20, + color: Colors.white, + letterSpacing: 0.5, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF6366F1), + Color(0xFF8B5CF6), + Color(0xFFEC4899), + ], + ), + ), + ), + centerTitle: false, + ), + body: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: buttonTab(), + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(25), + topRight: Radius.circular(25), + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, -5), + ), + ], + ), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(25), + topRight: Radius.circular(25), + ), + child: Column( + children: [ + if (isActive == "belum") Expanded(child: tugasBelum()), + if (isActive == "selesai") + Expanded(child: tugasSelesai()), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget buttonTab() { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + log("Tab 'Belum' tapped"); + setState(() { + isActive = "belum"; + }); + log("Calling getTugas with id: ${Get.arguments}, type: belum"); + tugasC.getTugas(id: Get.arguments, type: "belum"); + }, + child: Container( + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + gradient: isActive == "belum" + ? const LinearGradient( + colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: isActive == "belum" ? null : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.pending_actions, + color: isActive == "belum" + ? Colors.white + : Colors.grey.shade600, + size: 18, + ), + const SizedBox(width: 6), + Text( + "Belum", + style: TextStyle( + fontWeight: FontWeight.w600, + color: isActive == "belum" + ? Colors.white + : Colors.grey.shade600, + fontSize: 14, + ), + ), + ], + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + log("Tab 'Selesai' tapped"); + setState(() { + isActive = "selesai"; + }); + log("Calling getTugas with id: ${Get.arguments}, type: selesai"); + tugasC.getTugas(id: Get.arguments, type: "selesai"); + }, + child: Container( + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + gradient: isActive == "selesai" + ? const LinearGradient( + colors: [Color(0xFF10B981), Color(0xFF059669)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: isActive == "selesai" ? null : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.check_circle_outline, + color: isActive == "selesai" + ? Colors.white + : Colors.grey.shade600, + size: 18, + ), + const SizedBox(width: 6), + Text( + "Selesai", + style: TextStyle( + fontWeight: FontWeight.w600, + color: isActive == "selesai" + ? Colors.white + : Colors.grey.shade600, + fontSize: 14, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget tugasBelum() { + return Obx( + () { + log("tugasBelum widget rebuilt"); + log("isLoading: ${tugasC.isLoading.value}"); + log("tugasM: ${tugasC.tugasM}"); + log("data length: ${tugasC.tugasM?.data.length ?? 0}"); + log("data isEmpty: ${tugasC.tugasM?.data.isEmpty ?? true}"); + + if (tugasC.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF6366F1), + ), + strokeWidth: 3, + ), + ), + const SizedBox(height: 20), + const Text( + "Memuat tugas...", + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ); + } else if (tugasC.tugasM?.data.isEmpty ?? true) { + log("Showing empty data widget"); + return emptyData(); + } + log("Showing data list with ${tugasC.tugasM?.data.length ?? 0} items"); + return ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: tugasC.tugasM?.data.length ?? 0, + itemBuilder: (context, index) { + var data = tugasC.tugasM?.data[index]; + return data?.submitTugas != null + ? const SizedBox.shrink() + : Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.blue.shade50, + Colors.blue.shade100, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () async { + var tenggat = data!.tenggat; + int year = int.parse(tenggat.getYear()); + int month = int.parse(tenggat.getMonthNumber()); + int day = int.parse(tenggat.getTgl()); + + await getCurrentTime(); + + DateTime batasTanggal = DateTime(year, month, day); + + if (dateNow!.isAfter(batasTanggal)) { + snackbarfailed( + "Batas waktu sudah lewat, tidak bisa mengumpulkan tugas."); + } else { + Get.toNamed( + AppRoutes.tugasCommitSiswa, + arguments: { + "id": data.id, + "tipe_tugas": "belum", + "title": data.nama, + "deskripsi": data.deskripsi, + "submitTugas": null + }, + )?.then( + (value) { + if (value == true) { + tugasC.getTugas( + id: Get.arguments.toString(), + type: "belum"); + } + }, + ); + } + }, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade500, + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.assignment, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + data!.nama, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + Text( + data!.deskripsi + ?.toString() + .isNotEmpty == + true + ? data!.deskripsi.toString() + : 'Tidak ada deskripsi tugas.', + style: const TextStyle( + fontSize: 15, + color: Colors.black54, + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(height: 6), + // Hapus created_at dan updated_at untuk tugas belum + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.calendar_today, + size: 16, + color: Colors.blue), + const SizedBox(width: 6), + Text( + 'Tanggal dibuat:', + style: TextStyle( + fontSize: 12, + color: Colors.blue, + fontWeight: + FontWeight.w600), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 22.0, + top: 2, + bottom: 6), + child: (() { + final t = data.createdAt; + final dt = t is! DateTime + ? DateTime.parse( + t.toString()) + : t; + return Text( + dt.fullDateTime(), + style: TextStyle( + fontSize: 12, + color: + Colors.blue.shade800, + fontWeight: + FontWeight.w600), + ); + })(), + ), + Row( + children: [ + Icon(Icons.schedule, + size: 16, + color: Colors.red), + const SizedBox(width: 6), + Text( + 'Tenggat tugas:', + style: TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: + FontWeight.w600), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 22.0, top: 2), + child: Text( + data.tenggat.fullDateTime(), + style: TextStyle( + fontSize: 12, + color: Colors.red.shade800, + fontWeight: + FontWeight.w600), + ), + ), + ], + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + color: Colors.blue.shade500, + size: 16, + ), + ], + ), + const SizedBox(height: 15), + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.7), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Colors.blue.shade600, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + data.tanggal.fullDateTime(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.blue.shade600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 6), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.schedule, + size: 14, + color: Colors.red.shade600, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + data.tenggat.fullDateTime(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.red.shade600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ); + } + + Widget emptyData() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(30), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: const Color(0xFF6366F1).withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + isActive == "belum" + ? Icons.pending_actions + : Icons.check_circle_outline, + size: 60, + color: const Color(0xFF6366F1), + ), + ), + const SizedBox(height: 20), + Text( + isActive == "belum" + ? "Tidak Ada Tugas" + : "Tidak Ada Tugas Selesai", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 8), + Text( + isActive == "belum" + ? "Belum ada tugas yang perlu dikerjakan" + : "Belum ada tugas yang telah selesai dikerjakan", + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + fontFamily: 'Poppins', + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ), + ); + } + + Widget tugasSelesai() { + return Obx( + () { + log("tugasSelesai widget rebuilt"); + log("isLoading: ${tugasC.isLoading.value}"); + log("tugasM: ${tugasC.tugasM}"); + log("data length: ${tugasC.tugasM?.data.length ?? 0}"); + log("data isEmpty: ${tugasC.tugasM?.data.isEmpty ?? true}"); + + if (tugasC.isLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF10B981), + ), + strokeWidth: 3, + ), + ), + const SizedBox(height: 20), + const Text( + "Memuat tugas...", + style: TextStyle( + fontSize: 16, + color: Colors.grey, + fontFamily: 'Poppins', + ), + ), + ], + ), + ); + } else if (tugasC.tugasM?.data.isEmpty ?? true) { + log("Showing empty data widget"); + return emptyData(); + } + log("Showing data list with ${tugasC.tugasM?.data.length ?? 0} items"); + return ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: tugasC.tugasM?.data.length ?? 0, + itemBuilder: (context, index) { + var data = tugasC.tugasM?.data[index]; + return data?.submitTugas == null + ? const SizedBox.shrink() + : Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.green.shade50, + Colors.green.shade100, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.green.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () async { + Get.toNamed( + AppRoutes.tugasCommitSiswa, + arguments: { + "id": data!.submitTugas!.id, + "tipe_tugas": "selesai", + "title": data.nama, + "deskripsi": data.deskripsi, + "submitTugas": { + "id": data.submitTugas!.id, + "tanggal": data.submitTugas!.tanggal, + "nisn": data.submitTugas!.nisn, + "tugas_id": data.submitTugas!.tugasId, + "text": data.submitTugas?.text, + "file": data.submitTugas?.file, + } + }, + ); + }, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade500, + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.check_circle, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + data!.nama, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + Text( + data!.deskripsi + ?.toString() + .isNotEmpty == + true + ? data!.deskripsi.toString() + : 'Tidak ada deskripsi tugas.', + style: const TextStyle( + fontSize: 15, + color: Colors.black54, + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: + BorderRadius.circular(12), + ), + child: Text( + "Sudah Dikerjakan", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + ), + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + color: Colors.green.shade500, + size: 16, + ), + ], + ), + const SizedBox(height: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.calendar_today, + size: 16, color: Colors.blue), + const SizedBox(width: 6), + Text( + 'Tanggal dibuat:', + style: TextStyle( + fontSize: 12, + color: Colors.blue, + fontWeight: FontWeight.w600), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 22.0, top: 2, bottom: 6), + child: (() { + final t = data.createdAt; + final dt = t is! DateTime + ? DateTime.parse(t.toString()) + : t; + return Text( + dt.fullDateTime(), + style: TextStyle( + fontSize: 12, + color: Colors.blue.shade800, + fontWeight: FontWeight.w600), + ); + })(), + ), + Row( + children: [ + Icon(Icons.schedule, + size: 16, color: Colors.red), + const SizedBox(width: 6), + Text( + 'Tenggat tugas:', + style: TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w600), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 22.0, top: 2, bottom: 6), + child: Text( + data.tenggat.fullDateTime(), + style: TextStyle( + fontSize: 12, + color: Colors.red.shade800, + fontWeight: FontWeight.w600), + ), + ), + if (data.submitTugas != null) ...[ + Row( + children: [ + Icon(Icons.done_all, + size: 16, color: Colors.green), + const SizedBox(width: 6), + Text( + 'Waktu submit:', + style: TextStyle( + fontSize: 12, + color: Colors.green, + fontWeight: FontWeight.w600), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 22.0, top: 2, bottom: 6), + child: (() { + final t = data.submitTugas!.createdAt; + final dt = t is! DateTime + ? DateTime.parse(t.toString()) + : t; + return Text( + dt.fullDateTime(), + style: TextStyle( + fontSize: 12, + color: Colors.green.shade800, + fontWeight: FontWeight.w600), + ); + })(), + ), + Row( + children: [ + Icon(Icons.update, + size: 16, color: Colors.orange), + const SizedBox(width: 6), + Text( + 'Diperbarui:', + style: TextStyle( + fontSize: 12, + color: Colors.orange, + fontWeight: FontWeight.w600), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 22.0, top: 2), + child: (() { + final t = data.submitTugas!.updatedAt; + final dt = t is! DateTime + ? DateTime.parse(t.toString()) + : t; + return Text( + dt.fullDateTime(), + style: TextStyle( + fontSize: 12, + color: Colors.orange.shade800, + fontWeight: FontWeight.w600), + ); + })(), + ), + ], + ], + ), + const SizedBox(height: 10), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.upload_file, + size: 16, + color: Colors.blue.shade600, + ), + const SizedBox(width: 8), + Text( + "Dikumpulkan:", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.blue.shade600, + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 24.0, top: 4), + child: Text( + (() { + final t = data.submitTugas!.updatedAt; + final dt = t is! DateTime + ? DateTime.parse(t.toString()) + : t; + return dt.fullDateTime(); + })(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.blue.shade600, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/views/siswa/ubah_password.dart b/lib/views/siswa/ubah_password.dart new file mode 100644 index 0000000..ba1f6cc --- /dev/null +++ b/lib/views/siswa/ubah_password.dart @@ -0,0 +1,456 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/views/siswa/controllers/ubah_password_controller.dart'; + +class UbahPasswordPage extends StatefulWidget { + const UbahPasswordPage({super.key}); + + @override + State createState() => _UbahPasswordPageState(); +} + +class _UbahPasswordPageState extends State + with TickerProviderStateMixin { + final _formKey = GlobalKey(); + + final ubahPasswordC = Get.find(); + + bool _isObscureOld = true; + bool _isObscureNew = true; + bool _isObscureConfirm = true; + + late AnimationController _scaleAnimationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + + _scaleAnimationController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _scaleAnimationController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _scaleAnimationController.dispose(); + ubahPasswordC.oldPasswordC.value.dispose(); + ubahPasswordC.newPasswordC.value.dispose(); + ubahPasswordC.confirmPasswordC.value.dispose(); + super.dispose(); + } + + void _submitForm() { + if (_formKey.currentState!.validate()) { + ubahPasswordC.oldPasswordC.value.clear(); + ubahPasswordC.newPasswordC.value.clear(); + ubahPasswordC.confirmPasswordC.value.clear(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: AppBar( + title: const Text( + 'Ubah Password', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + centerTitle: true, + elevation: 0, + backgroundColor: Colors.transparent, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF667EEA), + Color(0xFF764BA2), + ], + ), + ), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: () => Get.back(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Form( + key: _formKey, + child: Column( + children: [ + // Header Section + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF6366F1), + Color(0xFF8B5CF6), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xFF6366F1).withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + ), + child: const Icon( + Icons.lock_reset, + size: 32, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + const Text( + "Keamanan Akun", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18, + color: Colors.white, + fontFamily: 'Poppins', + ), + ), + const SizedBox(height: 8), + const Text( + "Ubah password Anda untuk menjaga keamanan akun", + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: Colors.white, + fontFamily: 'Poppins', + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Form Section + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + // Password Lama + _buildPasswordField( + controller: ubahPasswordC.oldPasswordC.value, + label: 'Password Lama', + hint: 'Masukkan password lama Anda', + isObscure: _isObscureOld, + onToggleVisibility: () { + setState(() { + _isObscureOld = !_isObscureOld; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password lama tidak boleh kosong'; + } + return null; + }, + icon: Icons.lock_outline, + ), + + const SizedBox(height: 20), + + // Password Baru + _buildPasswordField( + controller: ubahPasswordC.newPasswordC.value, + label: 'Password Baru', + hint: 'Masukkan password baru (min. 6 karakter)', + isObscure: _isObscureNew, + onToggleVisibility: () { + setState(() { + _isObscureNew = !_isObscureNew; + }); + }, + validator: (value) { + if (value == null || value.length < 6) { + return 'Password baru minimal 6 karakter'; + } + return null; + }, + icon: Icons.lock_outline, + ), + + const SizedBox(height: 20), + + // Konfirmasi Password + _buildPasswordField( + controller: ubahPasswordC.confirmPasswordC.value, + label: 'Konfirmasi Password Baru', + hint: 'Masukkan ulang password baru', + isObscure: _isObscureConfirm, + onToggleVisibility: () { + setState(() { + _isObscureConfirm = !_isObscureConfirm; + }); + }, + validator: (value) { + if (value != ubahPasswordC.newPasswordC.value.text) { + return 'Konfirmasi password tidak cocok'; + } + return null; + }, + icon: Icons.lock_outline, + ), + ], + ), + ), + + const SizedBox(height: 30), + + // Submit Button + Obx(() => GestureDetector( + onTap: ubahPasswordC.isLoading.value + ? null + : () { + if (_formKey.currentState!.validate()) { + ubahPasswordC.ubahPassword(); + } + }, + onTapDown: (_) { + if (!ubahPasswordC.isLoading.value) { + _scaleAnimationController.forward(); + } + }, + onTapUp: (_) { + if (!ubahPasswordC.isLoading.value) { + _scaleAnimationController.reverse(); + } + }, + onTapCancel: () { + if (!ubahPasswordC.isLoading.value) { + _scaleAnimationController.reverse(); + } + }, + child: AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 18), + decoration: BoxDecoration( + gradient: ubahPasswordC.isLoading.value + ? null + : const LinearGradient( + colors: [ + Color(0xFF10B981), + Color(0xFF059669) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + color: ubahPasswordC.isLoading.value + ? Colors.grey.shade300 + : null, + borderRadius: BorderRadius.circular(20), + boxShadow: ubahPasswordC.isLoading.value + ? null + : [ + BoxShadow( + color: const Color(0xFF10B981) + .withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (ubahPasswordC.isLoading.value) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.grey, + ), + ), + ) + else + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.save, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + ubahPasswordC.isLoading.value + ? "Menyimpan..." + : "Simpan Password", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: ubahPasswordC.isLoading.value + ? Colors.grey.shade600 + : Colors.white, + fontFamily: 'Poppins', + ), + ), + ], + ), + ), + ); + }, + ), + )), + ], + ), + ), + ), + ); + } + + Widget _buildPasswordField({ + required TextEditingController controller, + required String label, + required String hint, + required bool isObscure, + required VoidCallback onToggleVisibility, + required String? Function(String?) validator, + required IconData icon, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF6366F1).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + color: const Color(0xFF6366F1), + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + label, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.black87, + fontFamily: 'Poppins', + ), + ), + ], + ), + const SizedBox(height: 12), + TextFormField( + controller: controller, + obscureText: isObscure, + style: const TextStyle( + fontSize: 14, + fontFamily: 'Poppins', + ), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle( + color: Colors.grey.shade400, + fontFamily: 'Poppins', + ), + filled: true, + fillColor: Colors.grey.shade50, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: const BorderSide( + color: Color(0xFF6366F1), + width: 2, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: const BorderSide( + color: Color(0xFFEF4444), + width: 1, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: const BorderSide( + color: Color(0xFFEF4444), + width: 2, + ), + ), + suffixIcon: IconButton( + icon: Icon( + isObscure ? Icons.visibility : Icons.visibility_off, + color: Colors.grey.shade600, + ), + onPressed: onToggleVisibility, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + ), + validator: validator, + ), + ], + ); + } +} diff --git a/lib/widgets/my_date_format.dart b/lib/widgets/my_date_format.dart new file mode 100644 index 0000000..60fefd0 --- /dev/null +++ b/lib/widgets/my_date_format.dart @@ -0,0 +1,30 @@ +import 'package:intl/intl.dart'; + +// Utilities +const String locale = 'id_ID'; + +extension DateTimeExtension on DateTime { + String dayName() => DateFormat("E", locale).format(this); + String simpleDate() => DateFormat("yyyy-MM-dd", locale).format(this); + String simpleDateRevers() => DateFormat("dd-MM-yyyy", locale).format(this); + String fullDate() => DateFormat("d MMM yyyy", locale).format(this); + String fullDateAll() => DateFormat("dd MMMM yyyy", locale).format(this); + String fullDateTime() => + DateFormat("E, d MMM yyyy HH:mm", locale).format(this); + String dateTime() => DateFormat("yyyy-MM-dd HH:mm:ss", locale).format(this); + String getTime() => DateFormat('HH:mm', locale).format(this); + String getTimeSecond() => DateFormat('HH:mm:ss', locale).format(this); + String getFullTime() => DateFormat('HH:mm a', locale).format(this); + String getHour() => DateFormat('HH', locale).format(this); + String getDay() => DateFormat('EEEE', locale).format(this); + String getTgl() => DateFormat('dd', locale).format(this); + String getMonth() => DateFormat('MMM', locale).format(this); + String getMonthNumber() => DateFormat('MM', locale).format(this); + String getMinute() => DateFormat('mm', locale).format(this); + String getYear() => DateFormat('yyyy', locale).format(this); + String getMonthAndYear() => DateFormat('MMMM yyyy', locale).format(this); + String getDayAndDate() => + DateFormat('EEEE, MMM dd, yyyy', locale).format(this); + String getSimpleDayAndDate() => + DateFormat('EEEE, dd MMM yyyy', locale).format(this); +} diff --git a/lib/widgets/my_snackbar.dart b/lib/widgets/my_snackbar.dart new file mode 100644 index 0000000..0254e64 --- /dev/null +++ b/lib/widgets/my_snackbar.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ui/widgets/my_text.dart'; + +snackbarfailed(var msg) { + return Get.snackbar( + "Gagal!", + "message", + backgroundColor: Colors.grey.shade400, + duration: const Duration(seconds: 3), + colorText: Colors.red.shade500, + messageText: Text( + msg, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + ), + ); +} + +snackbarSuccess(var msg) { + return Get.snackbar( + "Berhasil", + "message", + backgroundColor: Colors.grey.shade400, + duration: const Duration(seconds: 3), + colorText: Colors.green.shade500, + messageText: Text( + msg, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + ), + ); +} + +snackbarAlert(var title, var msg, Color color) { + return Get.showSnackbar( + GetSnackBar( + title: title, + message: msg, + backgroundColor: color, + duration: const Duration(seconds: 8), + ), + ); +} diff --git a/lib/widgets/my_text.dart b/lib/widgets/my_text.dart new file mode 100644 index 0000000..f6546d8 --- /dev/null +++ b/lib/widgets/my_text.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class MyText extends StatelessWidget { + final String text; + final double fontSize; + final Color color; + final FontWeight fontWeight; + final TextAlign? textAlign; + final int? maxLines; + const MyText({ + super.key, + required this.text, + required this.fontSize, + required this.color, + required this.fontWeight, + this.textAlign, + this.maxLines, + }); + + @override + Widget build(BuildContext context) { + if (maxLines != null) { + return Text( + text, + textAlign: textAlign ?? TextAlign.start, + style: TextStyle( + fontWeight: fontWeight, + fontSize: fontSize, + color: color, + ), + maxLines: maxLines, + overflow: TextOverflow.ellipsis, + ); + } + return Text( + text, + textAlign: textAlign ?? TextAlign.start, + style: TextStyle( + fontWeight: fontWeight, + fontSize: fontSize, + color: color, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..042eab5 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,754 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + 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" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dio: + dependency: "direct main" + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "8986dec4581b4bcd4b6df5d75a2ea0bede3db802f500635d05fa8be298f9467f" + url: "https://pub.dev" + source: hosted + version: "10.1.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + url: "https://pub.dev" + source: hosted + version: "5.2.1" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c + url: "https://pub.dev" + source: hosted + version: "0.14.3" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + 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" + get: + dependency: "direct main" + description: + name: get + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 + url: "https://pub.dev" + source: hosted + version: "4.7.2" + get_storage: + dependency: "direct main" + description: + name: get_storage + sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + http: + dependency: "direct main" + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.dev" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + 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: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + 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: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + ntp: + dependency: "direct main" + description: + name: ntp + sha256: "198db73e5059b334b50dbe8c626011c26576778ee9fc53f4c55c1d89d08ed2d2" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + 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" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f" + url: "https://pub.dev" + source: hosted + version: "12.0.0+1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + 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.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + 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: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + 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: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + url: "https://pub.dev" + source: hosted + version: "5.12.0" + 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" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..841737b --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,44 @@ +name: ui +description: "UI E-Learning" + +version: 1.0.0+1 + +environment: + sdk: ">=3.4.0 <4.0.0" + +dependencies: + cupertino_icons: ^1.0.6 + dio: ^5.8.0+1 + file_picker: ^10.1.2 + flutter: + sdk: flutter + flutter_dotenv: ^5.2.1 + flutter_secure_storage: ^9.2.4 + get: ^4.7.2 + get_storage: ^2.1.1 + google_fonts: ^6.2.1 + http: ^1.3.0 + intl: ^0.20.2 + ntp: ^2.0.0 + permission_handler: ^12.0.0+1 + shared_preferences: ^2.5.2 + url_launcher: ^6.3.1 + +dev_dependencies: + flutter_lints: ^3.0.0 + flutter_test: + sdk: flutter + flutter_launcher_icons: ^0.14.3 + +flutter: + assets: + - .env + - assets/images/ + - assets/data/ + + uses-material-design: true + +flutter_icons: + android: true + ios: true + image_path: "assets/images/skolearn_icon.png" diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..14142c1 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,31 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// ignore: depend_on_referenced_packages +import 'package:ui/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget( const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}