commit 74408e62a68e44e90eeaf5adc16289c84e57b7e5 Author: Itzfebry Date: Thu Jul 17 14:46:48 2025 +0700 first commit android 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 0000000..488409d Binary files /dev/null and b/assets/icons/settings-sliders.png differ diff --git a/assets/images/dashboardsiswa.png b/assets/images/dashboardsiswa.png new file mode 100644 index 0000000..1357752 Binary files /dev/null and b/assets/images/dashboardsiswa.png differ 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 0000000..85bc0d2 Binary files /dev/null and b/assets/images/iPhone 14 & 15 Pro Max - 2 (3).png differ diff --git a/assets/images/matapelajaran.png b/assets/images/matapelajaran.png new file mode 100644 index 0000000..b4d5a2a Binary files /dev/null and b/assets/images/matapelajaran.png differ diff --git a/assets/images/skoda.png b/assets/images/skoda.png new file mode 100644 index 0000000..f8aceb2 Binary files /dev/null and b/assets/images/skoda.png differ diff --git a/assets/images/skolearn_icon.png b/assets/images/skolearn_icon.png new file mode 100644 index 0000000..a1ba671 Binary files /dev/null and b/assets/images/skolearn_icon.png differ diff --git a/assets/images/welcomescreen.png b/assets/images/welcomescreen.png new file mode 100644 index 0000000..916f617 Binary files /dev/null and b/assets/images/welcomescreen.png differ 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); + }); +}