first commit android

This commit is contained in:
Itzfebry 2025-07-17 14:46:48 +07:00
commit 74408e62a6
127 changed files with 19239 additions and 0 deletions

75
.gitignore vendored Normal file
View File

@ -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/

30
.metadata Normal file
View File

@ -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'

148
IMPLEMENTATION_SUMMARY.md Normal file
View File

@ -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!** 🚀

View File

@ -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<void> 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

16
README.md Normal file
View File

@ -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.

28
analysis_options.yaml Normal file
View File

@ -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

View File

@ -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"
}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/images/skoda.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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 <repository-url>
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.

View File

@ -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"

View File

@ -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

View File

@ -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";
}

View File

@ -0,0 +1,2 @@
export 'package:flutter_dotenv/flutter_dotenv.dart';
export 'theme_constant.dart';

View File

@ -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),
),
],
);
}

View File

@ -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;
}
}

39
lib/main.dart Normal file
View File

@ -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<void> 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),
),
);
}
}

View File

@ -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<SharedPreferences>();
final token = prefs.getString('token');
if (token == null || token.isEmpty) {
return const RouteSettings(name: AppRoutes.login);
}
return null; // Lanjutkan ke halaman yang diminta
}
}

View File

@ -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<Datum> data;
DetailSubmitTugasSiswaModel({
required this.status,
required this.message,
required this.data,
});
factory DetailSubmitTugasSiswaModel.fromJson(Map<String, dynamic> json) =>
DetailSubmitTugasSiswaModel(
status: json["status"],
message: json["message"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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,
};
}

View File

@ -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<Datum> data;
KelasModel({
required this.status,
required this.message,
required this.data,
});
factory KelasModel.fromJson(Map<String, dynamic> json) => KelasModel(
status: json["status"],
message: json["message"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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<String, dynamic> json) => Datum(
nama: json["nama"],
createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]),
);
Map<String, dynamic> toJson() => {
"nama": nama,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
};
}

View File

@ -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<Datum> data;
MataPelajaranModel({
required this.status,
required this.message,
required this.data,
});
factory MataPelajaranModel.fromJson(Map<String, dynamic> json) =>
MataPelajaranModel(
status: json["status"],
message: json["message"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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> 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<String, dynamic> 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<Materi>.from(json["materi"].map((x) => Materi.fromJson(x))),
jumlahBuku: json["jumlah_buku"],
jumlahVideo: json["jumlah_video"],
);
Map<String, dynamic> 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<dynamic>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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(),
};
}

View File

@ -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<Datum> data;
MataPelajaranSimpleModel({
required this.status,
required this.message,
required this.data,
});
factory MataPelajaranSimpleModel.fromJson(Map<String, dynamic> json) =>
MataPelajaranSimpleModel(
status: json["status"],
message: json["message"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() => {
"id": id,
"user_id": userId,
"nip": nip,
"nama": nama,
"jk": jk,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
};
}

View File

@ -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<Datum> data;
MateriBukuModel({
required this.status,
required this.message,
required this.data,
});
factory MateriBukuModel.fromJson(Map<String, dynamic> json) =>
MateriBukuModel(
status: json["status"],
message: json["message"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() => {
"id": id,
"nama": nama,
"guru_nip": guruNip,
"kelas": kelas,
"tahun_ajaran": tahunAjaran,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
};
}

View File

@ -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<Datum> data;
MateriVideoModel({
required this.status,
required this.message,
required this.data,
});
factory MateriVideoModel.fromJson(Map<String, dynamic> json) =>
MateriVideoModel(
status: json["status"],
message: json["message"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() => {
"id": id,
"nama": nama,
"guru_nip": guruNip,
"kelas": kelas,
"tahun_ajaran": tahunAjaran,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
};
}

View File

@ -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<String, dynamic> json) =>
QuizAnswerModel(
status: json["status"],
message: json["message"],
data: Data.fromJson(json["data"]),
);
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() => {
"quiz_id": quizId,
"correct": correct,
"fase": fase,
"new_level": newLevel,
"skor_sementara": skorSementara,
"selesai": selesai,
"waktu_tersisa": waktuTersisa,
};
}

View File

@ -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<String, dynamic> json) =>
QuizAttemptModel(
status: json["status"],
message: json["message"],
data: Data.fromJson(json["data"]),
);
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
}
}

View File

@ -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<Datum> data;
QuizGuruModel({
required this.status,
required this.message,
required this.data,
});
factory QuizGuruModel.fromJson(Map<String, dynamic> json) => QuizGuruModel(
status: json["status"],
message: json["message"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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<String, dynamic> 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<String, dynamic> 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(),
};
}

186
lib/models/quiz_mode.dart Normal file
View File

@ -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<Datum> data;
QuizModel({
required this.status,
required this.message,
required this.data,
});
factory QuizModel.fromJson(Map<String, dynamic> json) => QuizModel(
status: json["status"],
message: json["message"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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(),
};
}

View File

@ -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<String, dynamic> json) =>
QuizQuestionModel(
status: json["status"] ?? false,
message: json["message"] ?? "",
data: json["data"] == null ? null : Data.fromJson(json["data"]),
);
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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(),
};
}

View File

@ -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<Datum> data;
TahunAjaranModel({
required this.status,
required this.message,
required this.data,
});
factory TahunAjaranModel.fromJson(Map<String, dynamic> json) =>
TahunAjaranModel(
status: json["status"],
message: json["message"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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<String, dynamic> json) => Datum(
tahun: json["tahun"],
status: json["status"],
createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]),
);
Map<String, dynamic> toJson() => {
"tahun": tahun,
"status": status,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
};
}

272
lib/models/tugas_model.dart Normal file
View File

@ -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<Datum> data;
TugasModel({
required this.status,
required this.message,
required this.data,
});
factory TugasModel.fromJson(Map<String, dynamic> json) {
try {
return TugasModel(
status: json["status"] ?? false,
message: json["message"] ?? "No message",
data: json["data"] != null
? List<Datum>.from(json["data"].map((x) => Datum.fromJson(x)))
: <Datum>[],
);
} catch (e) {
throw Exception("Error parsing TugasModel: $e");
}
}
Map<String, dynamic> toJson() => {
"status": status,
"message": message,
"data": List<dynamic>.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<String, dynamic> 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<String, dynamic>) {
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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(),
};
}

186
lib/routes/app_pages.dart Normal file
View File

@ -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(),
),
];
}

View File

@ -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";
}

22
lib/routes/export.dart Normal file
View File

@ -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';

View File

@ -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>(() => AuthController());
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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<int> 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;
}
}
}

View File

@ -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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
late String role;
late String idFieldLabel;
final AuthController authController = Get.find<AuthController>();
late AnimationController _controller;
late Animation<double> _animation;
final Map<String, String> 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<double>(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>(
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),
);
}
}

View File

@ -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(),
),
);
}
}

View File

@ -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<SplashScreen> {
final checkTokenC = Get.put(CheckTokenController());
@override
void initState() {
super.initState();
_checkLoginStatus();
}
Future<void> _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
],
),
),
);
}
}

View File

@ -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),
),
),
],
),
),
),
);
}
}

View File

@ -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>(() => SiswaController());
}
}

View File

@ -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<Map<String, dynamic>>();
@override
void onInit() {
super.onInit();
getMe();
}
Future<void> 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;
}
}
}

View File

@ -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<GuruDashboardPage> createState() => _GuruDashboardPageState();
}
class _GuruDashboardPageState extends State<GuruDashboardPage>
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
final DashboardGuruController dashboardC = Get.put(DashboardGuruController());
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 10),
vsync: this,
)..repeat();
_animation = Tween<double>(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<SiswaController>();
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),
),
],
),
],
),
),
],
),
),
],
),
);
}
}

View File

@ -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>(() => KelasController());
Get.lazyPut<TahunAjaranController>(() => TahunAjaranController());
Get.lazyPut<MataPelajaranGuruController>(
() => MataPelajaranGuruController(),
);
}
}

View File

@ -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<String>();
KelasModel? kelasM;
@override
void onInit() {
super.onInit();
getKelas();
}
Future<void> 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);
}
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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<String>();
TahunAjaranModel? tahunAjaranM;
@override
void onInit() {
super.onInit();
getTahunAjaran();
// // fetch matpel tiap kali kelas berubah
// ever(selectedTahun, (kelas) {
// if (kelas != null) {
// final matpelGuruC = Get.find<MataPelajaranGuruController>();
// matpelGuruC.getMatPel(kelas: kelas);
// }
// });
}
Future<void> 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);
}
}
}

View File

@ -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<String>(
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<String>(
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<String>(
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<String>(
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,
);
}
},
),
);
}),
),
],
),
],
),
);
}
}

View File

@ -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<MataPelajaranGuru> createState() => _MataPelajaranGuruState();
}
class _MataPelajaranGuruState extends State<MataPelajaranGuru> {
MataPelajaranGuruController matpelGuruC =
Get.find<MataPelajaranGuruController>();
KelasController kelasC = Get.find<KelasController>();
TahunAjaranController tahunAjaranC = Get.find<TahunAjaranController>();
@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,
),
],
),
);
}
}

View File

@ -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<ProfileGuruPage> createState() => _ProfileGuruPageState();
}
class _ProfileGuruPageState extends State<ProfileGuruPage> {
SiswaController siswaC = Get.find<SiswaController>();
@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,
),
],
),
),
],
),
);
}
}

View File

@ -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>(() => KelasController());
Get.lazyPut<TahunAjaranController>(() => TahunAjaranController());
Get.lazyPut<MataPelajaranGuruController>(
() => MataPelajaranGuruController(),
);
}
}

View File

@ -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>(() => QuizDetailGuruController());
}
}

View File

@ -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>(() => QuizGuruController());
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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<MataPelajaranQuizGuru> createState() => _MataPelajaranQuizGuruState();
}
class _MataPelajaranQuizGuruState extends State<MataPelajaranQuizGuru> {
MataPelajaranGuruController matpelGuruC =
Get.find<MataPelajaranGuruController>();
KelasController kelasC = Get.find<KelasController>();
TahunAjaranController tahunAjaranC = Get.find<TahunAjaranController>();
@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,
),
],
),
);
}
}

View File

@ -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<QuizGuruController>();
@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,
),
],
),
),
),
);
},
);
}
}),
),
],
),
),
),
),
),
);
}
}

View File

@ -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<QuizDetailGuruController>();
@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,
),
),
),
],
),
),
),
);
},
);
},
),
],
),
);
}
}

View File

@ -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>(
() => DetailSubmitTugasSiswaController());
}
}

View File

@ -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>(() => KelasController());
Get.lazyPut<TahunAjaranController>(() => TahunAjaranController());
Get.lazyPut<TugasDetailGuruController>(() => TugasDetailGuruController());
}
}

View File

@ -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>(
() => MataPelajaranSimpleController());
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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<String>();
var isEmptyData = true.obs;
var isFetchData = false.obs;
Future<void> 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);
}
}
}

View File

@ -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<KelasController>();
TahunAjaranController tahunAjaranC = Get.find<TahunAjaranController>();
TugasDetailGuruController tugasDetailGuruC =
Get.find<TugasDetailGuruController>();
@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,
),
],
),
),
],
),
),
),
),
);
},
);
},
)
],
),
),
);
}
}

View File

@ -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<DetailSubmitTugas> createState() => _DetailSubmitTugasState();
}
class _DetailSubmitTugasState extends State<DetailSubmitTugas> {
// TugasController tugasC = Get.find<TugasController>();
DetailSubmitTugasSiswaController detailSubTugasSiswaC =
Get.find<DetailSubmitTugasSiswaController>();
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<void> 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,
},
);
},
),
);
},
);
}
});
}
}

View File

@ -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<String>(
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<String>(
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<String>(
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<String>(
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,
),
),
),
),
],
),
],
),
);
}
}

View File

@ -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<MataPelajaranSimpleController>();
@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,
),
],
),
),
),
);
}
}

View File

@ -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<void> _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,
)),
),
),
],
),
),
],
),
),
),
),
);
}
}

View File

@ -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>(() => NotifikasiController());
}
}

View File

@ -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>(() => SiswaController());
Get.lazyPut<NotifikasiCountController>(() => NotifikasiCountController());
}
}

View File

@ -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>(() => UbahPasswordController());
}
}

View File

@ -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<void> 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<void> 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);
}
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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 = <String, dynamic>{}.obs;
var dataAnalysis = <String, dynamic>{}.obs;
var kekuranganIsEmpty = true.obs;
var kelebihanIsEmpty = true.obs;
@override
void onInit() {
super.onInit();
getMe();
}
Future<void> 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<void> 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<void> 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);
}
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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>(() => MataPelajaranController());
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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<MataPelajaranController>();
@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,
),
),
],
),
],
),
),
],
),
),
);
},
);
}),
),
],
),
);
}
}

View File

@ -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>(() => MateriController());
}
}

View File

@ -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<void> 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<void> 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<void> 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");
}
}
}

View File

@ -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<MateriView> createState() => _MateriViewState();
}
class _MateriViewState extends State<MateriView>
with SingleTickerProviderStateMixin {
late TabController _tabController;
MateriController materiC = Get.find<MateriController>();
String? selectedSemester = "1";
String? selectedSemesterVideo = "1";
final List<String> 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<void> _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<String>(
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<String>(
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<String>(
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<String>(
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),
),
),
),
],
),
);
},
),
),
],
),
),
],
),
),
);
}
}

View File

@ -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<NotifikasiController>();
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>(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<double>(
tween: Tween<double>(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;
}
}
}

View File

@ -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<SiswaController>();
@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>(
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<AnimatedProgressItem> createState() => _AnimatedProgressItemState();
}
class _AnimatedProgressItemState extends State<AnimatedProgressItem>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_animation = Tween<double>(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,
),
);
},
),
],
),
);
}
}

View File

@ -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>(
() => MataPelajaranSimpleController());
}
}

View File

@ -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>(QuizController());
}
}

View File

@ -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>(() => QuizFinishController());
}
}

View File

@ -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>(() => QuizAttemptController());
Get.lazyPut<QuizQuestionController>(() => QuizQuestionController());
}
}

View File

@ -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<QuizQuestionController>();
var remainingTime = Duration.zero.obs;
var isTimerRunning = false.obs;
var isQuizFinished = false.obs;
// Menambahkan tracking jawaban yang telah dijawab
var answeredQuestions =
<String, String>{}.obs; // questionId -> selectedAnswer
var currentQuestionId = "".obs;
// Menambahkan tracking semua question IDs untuk auto-finish
List<String> 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<void> 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<String> 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<void> 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<void> 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<void> 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<String, dynamic> 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);
}
}

View File

@ -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<void> 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);
}
}
}

View File

@ -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<void> 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<void> 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<void> 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");
}
}
}

View File

@ -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<void> 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<QuizAttemptController>();
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<void> handleQuestionsExhausted(String attemptId) async {
try {
log("Handling questions exhausted for attempt ID: $attemptId");
// Hentikan timer quiz
QuizAttemptController quizAttemptC = Get.find<QuizAttemptController>();
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<void> autoFinishQuiz(String attemptId) async {
try {
QuizAttemptController quizAttemptC = Get.find<QuizAttemptController>();
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'),
),
],
),
);
}
}
}

View File

@ -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<MataPelajaranSimpleController>();
@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>(
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<QuizSubjectCard> createState() => _QuizSubjectCardState();
}
class _QuizSubjectCardState extends State<QuizSubjectCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
List<Color> _getGradientColors(int index) {
List<List<Color>> 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,
),
),
],
),
),
),
);
},
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More