first commit android
|
|
@ -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/
|
||||
|
|
@ -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'
|
||||
|
|
@ -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!** 🚀
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 558 B |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
export 'theme_constant.dart';
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
// Primary Green Theme Colors
|
||||
static const Color primaryGreen = Color(0xFF3B8C6E);
|
||||
static const Color primaryGreenLight = Color(0xFF5FAF91);
|
||||
static const Color primaryGreenDark = Color(0xFF266B50);
|
||||
|
||||
// Accent Colors
|
||||
static const Color accentOrange = Color(0xFFF5A623);
|
||||
static const Color accentPurple = Color(0xFF9B51E0);
|
||||
static const Color accentBlue = Color(0xFF4A90E2);
|
||||
|
||||
// Neutral Colors
|
||||
static const Color neutralWhite = Color(0xFFFFFFFF);
|
||||
static const Color neutralGrey = Color(0xFFF5F5F5);
|
||||
static const Color neutralDarkGrey = Color(0xFF9E9E9E);
|
||||
static const Color neutralBlack = Color(0xFF333333);
|
||||
|
||||
// Gradient for cards and backgrounds
|
||||
static const Gradient greenGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [primaryGreenLight, primaryGreen],
|
||||
);
|
||||
|
||||
static const Gradient accentGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [accentOrange, Color(0xFFFA8E22)],
|
||||
);
|
||||
|
||||
// Text Styles
|
||||
static const TextStyle headingStyle = TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
color: neutralBlack,
|
||||
);
|
||||
|
||||
static const TextStyle subheadingStyle = TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: neutralBlack,
|
||||
);
|
||||
|
||||
static const TextStyle bodyStyle = TextStyle(
|
||||
fontSize: 14,
|
||||
color: neutralBlack,
|
||||
);
|
||||
|
||||
static const TextStyle smallStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: neutralDarkGrey,
|
||||
);
|
||||
|
||||
// Button Styles
|
||||
static final ButtonStyle primaryButtonStyle = ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryGreen,
|
||||
foregroundColor: neutralWhite,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
);
|
||||
|
||||
static final ButtonStyle secondaryButtonStyle = OutlinedButton.styleFrom(
|
||||
foregroundColor: primaryGreen,
|
||||
side: const BorderSide(color: primaryGreen),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
);
|
||||
|
||||
// Card Decoration
|
||||
static final BoxDecoration cardDecoration = BoxDecoration(
|
||||
color: neutralWhite,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
static final BoxDecoration gradientCardDecoration = BoxDecoration(
|
||||
gradient: greenGradient,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||