pembaruan config dll
This commit is contained in:
parent
89d9827387
commit
27882ce9e6
|
@ -1,2 +1,7 @@
|
|||
# Ignore .env file to avoid pushing sensitive data to GitHub
|
||||
backend/.env
|
||||
backend/.env
|
||||
backend/node_modules/
|
||||
|
||||
**/node_modules/
|
||||
|
||||
**/.env
|
|
@ -0,0 +1,25 @@
|
|||
require('dotenv').config(); // untuk baca file .env
|
||||
|
||||
module.exports = {
|
||||
development: {
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
host: process.env.DB_HOST,
|
||||
dialect: 'mysql',
|
||||
},
|
||||
test: {
|
||||
username: 'root',
|
||||
password: null,
|
||||
database: 'database_test',
|
||||
host: '127.0.0.1',
|
||||
dialect: 'mysql',
|
||||
},
|
||||
production: {
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
host: process.env.DB_HOST,
|
||||
dialect: 'mysql',
|
||||
}
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"development": {
|
||||
"username": "root",
|
||||
"password": null,
|
||||
"database": "sibayam",
|
||||
"host": "127.0.0.1",
|
||||
"dialect": "mysql"
|
||||
},
|
||||
"test": {
|
||||
"username": "root",
|
||||
"password": null,
|
||||
"database": "database_test",
|
||||
"host": "127.0.0.1",
|
||||
"dialect": "mysql"
|
||||
},
|
||||
"production": {
|
||||
"username": "root",
|
||||
"password": null,
|
||||
"database": "database_production",
|
||||
"host": "127.0.0.1",
|
||||
"dialect": "mysql"
|
||||
}
|
||||
}
|
|
@ -1,10 +1,32 @@
|
|||
// const { Sequelize } = require('sequelize');
|
||||
// require('dotenv').config();
|
||||
|
||||
// const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
|
||||
// host: process.env.DB_HOST,
|
||||
// dialect: 'mysql',
|
||||
// timezone: '+07:00',
|
||||
// });
|
||||
|
||||
// module.exports = sequelize;
|
||||
|
||||
const { Sequelize } = require('sequelize');
|
||||
require('dotenv').config();
|
||||
|
||||
const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
|
||||
const sequelize = new Sequelize(
|
||||
process.env.DB_NAME,
|
||||
process.env.DB_USER,
|
||||
process.env.DB_PASSWORD,
|
||||
{
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT, // tambahkan port
|
||||
dialect: 'mysql',
|
||||
timezone: '+07:00',
|
||||
});
|
||||
dialectOptions: {
|
||||
ssl: {
|
||||
rejectUnauthorized: false, // jika diperlukan (tergantung Clever Cloud)
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = sequelize;
|
||||
module.exports = sequelize;
|
||||
|
|
|
@ -112,10 +112,16 @@ const createGmailTransporter = () => {
|
|||
// Generate 6 digit random code
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
|
||||
|
||||
|
||||
// Konversi ke waktu Indonesia (GMT+7)
|
||||
const expiresAtWIB = new Date(expiresAt.getTime() + 7 * 60 * 60 * 1000);
|
||||
|
||||
// Format manual jadi ISO-like (tanpa Z karena bukan UTC)
|
||||
const isoWIB = expiresAtWIB.toISOString().replace( '+07:00');
|
||||
|
||||
await user.update({
|
||||
resetToken: code,
|
||||
resetTokenExpiry: expiresAt,
|
||||
resetTokenExpiry: isoWIB,
|
||||
});
|
||||
|
||||
// Nama aplikasi yang konsisten
|
||||
|
|
|
@ -77,7 +77,7 @@ function calculateBayesProbability(rules, entityType) {
|
|||
async function getTotalGejalaForEntity(entityId, entityType, inputGejala) {
|
||||
try {
|
||||
let totalGejala = 0;
|
||||
|
||||
|
||||
if (entityType === 'penyakit') {
|
||||
const allRules = await Rule_penyakit.findAll({
|
||||
where: { id_penyakit: entityId }
|
||||
|
@ -89,7 +89,7 @@ async function getTotalGejalaForEntity(entityId, entityType, inputGejala) {
|
|||
});
|
||||
totalGejala = allRules.length;
|
||||
}
|
||||
|
||||
|
||||
return totalGejala;
|
||||
} catch (error) {
|
||||
console.error('Error getting total gejala:', error);
|
||||
|
@ -103,12 +103,12 @@ async function resolveAmbiguity(candidates, inputGejala) {
|
|||
for (let candidate of candidates) {
|
||||
const entityType = candidate.type;
|
||||
const entityId = entityType === 'penyakit' ? candidate.id_penyakit : candidate.id_hama;
|
||||
|
||||
|
||||
candidate.total_gejala_entity = await getTotalGejalaForEntity(entityId, entityType, inputGejala);
|
||||
|
||||
|
||||
// Hitung persentase kesesuaian gejala
|
||||
candidate.persentase_kesesuaian = candidate.total_gejala_entity > 0
|
||||
? (candidate.jumlah_gejala_cocok / candidate.total_gejala_entity) * 100
|
||||
candidate.persentase_kesesuaian = candidate.total_gejala_entity > 0
|
||||
? (candidate.jumlah_gejala_cocok / candidate.total_gejala_entity) * 100
|
||||
: 0;
|
||||
}
|
||||
|
||||
|
@ -121,12 +121,12 @@ async function resolveAmbiguity(candidates, inputGejala) {
|
|||
if (a.jumlah_gejala_cocok !== b.jumlah_gejala_cocok) {
|
||||
return b.jumlah_gejala_cocok - a.jumlah_gejala_cocok;
|
||||
}
|
||||
|
||||
|
||||
// Prioritas 2: Persentase kesesuaian
|
||||
if (Math.abs(a.persentase_kesesuaian - b.persentase_kesesuaian) > 0.01) {
|
||||
return b.persentase_kesesuaian - a.persentase_kesesuaian;
|
||||
}
|
||||
|
||||
|
||||
// Prioritas 3: Entity dengan total gejala lebih sedikit (lebih spesifik)
|
||||
return a.total_gejala_entity - b.total_gejala_entity;
|
||||
});
|
||||
|
@ -134,6 +134,24 @@ async function resolveAmbiguity(candidates, inputGejala) {
|
|||
return candidates[0]; // Kembalikan yang terbaik
|
||||
}
|
||||
|
||||
// Helper function untuk memfilter hasil dengan 100% akurasi yang hanya cocok 1 gejala
|
||||
function filterSingleSymptomPerfectMatch(results) {
|
||||
const filtered = results.filter(result => {
|
||||
// Jika probabilitas 100% dan hanya cocok dengan 1 gejala, filter keluar
|
||||
const isPerfectMatch = Math.abs(result.probabilitas_persen - 100) < 0.0001;
|
||||
const isSingleSymptom = result.jumlah_gejala_cocok === 1;
|
||||
|
||||
if (isPerfectMatch && isSingleSymptom) {
|
||||
console.log(`Memfilter ${result.nama} (${result.type}) - 100% akurasi dengan hanya 1 gejala cocok`);
|
||||
return false; // Filter keluar
|
||||
}
|
||||
|
||||
return true; // Tetap masukkan
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
exports.diagnosa = async (req, res) => {
|
||||
const { gejala } = req.body;
|
||||
const userId = req.user?.id;
|
||||
|
@ -195,16 +213,41 @@ exports.diagnosa = async (req, res) => {
|
|||
...sortedHama.map(h => ({ type: 'hama', ...h }))
|
||||
].sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
|
||||
|
||||
// ========== FILTER HASIL 100% DENGAN 1 GEJALA ==========
|
||||
const filteredResults = filterSingleSymptomPerfectMatch(allResults);
|
||||
|
||||
// Jika semua hasil terfilter, gunakan hasil berdasarkan kecocokan gejala terbanyak
|
||||
let finalResults = filteredResults;
|
||||
let filterInfo = {
|
||||
total_sebelum_filter: allResults.length,
|
||||
total_setelah_filter: filteredResults.length,
|
||||
hasil_terfilter: allResults.length - filteredResults.length
|
||||
};
|
||||
|
||||
if (filteredResults.length === 0 && allResults.length > 0) {
|
||||
console.log('Semua hasil terfilter karena 100% dengan 1 gejala. Menggunakan kecocokan gejala terbanyak.');
|
||||
// Urutkan berdasarkan jumlah gejala cocok, lalu probabilitas
|
||||
finalResults = allResults.sort((a, b) => {
|
||||
if (a.jumlah_gejala_cocok !== b.jumlah_gejala_cocok) {
|
||||
return b.jumlah_gejala_cocok - a.jumlah_gejala_cocok;
|
||||
}
|
||||
return b.probabilitas_persen - a.probabilitas_persen;
|
||||
});
|
||||
|
||||
filterInfo.fallback_to_symptom_count = true;
|
||||
filterInfo.fallback_reason = 'Semua hasil memiliki 100% akurasi dengan hanya 1 gejala cocok';
|
||||
}
|
||||
|
||||
// ========== PENANGANAN AMBIGUITAS ==========
|
||||
let hasilTertinggi = null;
|
||||
let isAmbiguous = false;
|
||||
let ambiguityResolution = null;
|
||||
|
||||
if (allResults.length > 0) {
|
||||
const nilaiTertinggi = allResults[0].probabilitas_persen;
|
||||
|
||||
if (finalResults.length > 0) {
|
||||
const nilaiTertinggi = finalResults[0].probabilitas_persen;
|
||||
|
||||
// Cari semua hasil dengan nilai probabilitas yang sama dengan yang tertinggi
|
||||
const kandidatTertinggi = allResults.filter(result =>
|
||||
const kandidatTertinggi = finalResults.filter(result =>
|
||||
Math.abs(result.probabilitas_persen - nilaiTertinggi) < 0.0001 // Toleransi untuk floating point
|
||||
);
|
||||
|
||||
|
@ -212,10 +255,10 @@ exports.diagnosa = async (req, res) => {
|
|||
// Ada ambiguitas - perlu resolusi
|
||||
isAmbiguous = true;
|
||||
console.log(`Ditemukan ${kandidatTertinggi.length} kandidat dengan nilai probabilitas sama: ${nilaiTertinggi}%`);
|
||||
|
||||
|
||||
// Lakukan resolusi ambiguitas
|
||||
hasilTertinggi = await resolveAmbiguity(kandidatTertinggi, gejala);
|
||||
|
||||
|
||||
ambiguityResolution = {
|
||||
total_kandidat: kandidatTertinggi.length,
|
||||
metode_resolusi: 'jumlah_gejala_cocok',
|
||||
|
@ -235,7 +278,38 @@ exports.diagnosa = async (req, res) => {
|
|||
};
|
||||
} else {
|
||||
// Tidak ada ambiguitas
|
||||
hasilTertinggi = allResults[0];
|
||||
hasilTertinggi = finalResults[0];
|
||||
|
||||
// Validasi: Jika hanya cocok 1 gejala dan masih ada kandidat lain dengan kecocokan lebih baik
|
||||
if (hasilTertinggi.jumlah_gejala_cocok === 1 && finalResults.length > 1) {
|
||||
const alternatif = finalResults
|
||||
.filter(result => result.jumlah_gejala_cocok > 1 && result.probabilitas_persen >= hasilTertinggi.probabilitas_persen - 5); // toleransi 5%
|
||||
|
||||
if (alternatif.length > 0) {
|
||||
// Gunakan resolveAmbiguity terhadap alternatif + hasilTertinggi
|
||||
const kandidatAmbigu = [hasilTertinggi, ...alternatif];
|
||||
hasilTertinggi = await resolveAmbiguity(kandidatAmbigu, gejala);
|
||||
|
||||
isAmbiguous = true;
|
||||
ambiguityResolution = {
|
||||
total_kandidat: kandidatAmbigu.length,
|
||||
metode_resolusi: 'gejala_minimum_filter',
|
||||
kandidat: kandidatAmbigu.map(k => ({
|
||||
type: k.type,
|
||||
nama: k.nama,
|
||||
probabilitas_persen: k.probabilitas_persen,
|
||||
jumlah_gejala_cocok: k.jumlah_gejala_cocok,
|
||||
total_gejala_entity: k.total_gejala_entity,
|
||||
persentase_kesesuaian: k.persentase_kesesuaian
|
||||
})),
|
||||
terpilih: {
|
||||
type: hasilTertinggi.type,
|
||||
nama: hasilTertinggi.nama,
|
||||
alasan: `Dipilih karena memiliki jumlah gejala cocok lebih banyak dari kandidat yang hanya cocok 1 gejala`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,7 +364,8 @@ exports.diagnosa = async (req, res) => {
|
|||
gejala_input: gejala.map(id => parseInt(id)),
|
||||
hasil_tertinggi: hasilTertinggi,
|
||||
is_ambiguous: isAmbiguous,
|
||||
ambiguity_resolution: ambiguityResolution
|
||||
ambiguity_resolution: ambiguityResolution,
|
||||
filter_info: filterInfo // Informasi tentang filtering yang dilakukan
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 131 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
|
@ -6,7 +6,7 @@ const Sequelize = require('sequelize');
|
|||
const process = require('process');
|
||||
const basename = path.basename(__filename);
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const config = require(__dirname + '/../config/config.json')[env];
|
||||
const config = require(__dirname + '/../config/config.js')[env];
|
||||
const db = {};
|
||||
|
||||
let sequelize;
|
||||
|
|
|
@ -177,11 +177,16 @@ class _RulePageState extends State<RulePage> {
|
|||
),
|
||||
).then((_) => fetchRules());
|
||||
},
|
||||
icon: Icon(Icons.bug_report),
|
||||
label: Text("Tambah Rule Hama"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
icon: Icon(Icons.bug_report, size: 16,),
|
||||
label: Text(
|
||||
"Tambah Rule Hama",
|
||||
style: TextStyle(fontSize: 12)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), // Padding lebih kecil
|
||||
minimumSize: Size(0, 32), // Tinggi minimum lebih kecil
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
|
@ -204,11 +209,16 @@ class _RulePageState extends State<RulePage> {
|
|||
),
|
||||
).then((_) => fetchRules());
|
||||
},
|
||||
icon: Icon(Icons.healing),
|
||||
label: Text("Tambah Rule Penyakit"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
icon: Icon(Icons.healing, size: 16,),
|
||||
label: Text(
|
||||
"Tambah Rule Penyakit",
|
||||
style: TextStyle(fontSize: 12),),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), // Padding lebih kecil
|
||||
minimumSize: Size(0, 32), // Tinggi minimum lebih kecil
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1000,6 +1000,29 @@ Future<List<Map<String, dynamic>>> getAllHistori() async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<bool> verifyResetCode({required String email, required String code}) async {
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl/reset-password'),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
'email': email,
|
||||
'resetToken': code,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Jika status code 200, berarti kode valid
|
||||
return true;
|
||||
} else {
|
||||
// Jika status code bukan 200, kode tidak valid
|
||||
final error = jsonDecode(response.body);
|
||||
throw error['message'] ?? 'Kode verifikasi tidak valid';
|
||||
}
|
||||
} catch (e) {
|
||||
throw e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Create Rule penyakit
|
||||
static Future<http.Response> createRulePenyakit({
|
||||
|
|
|
@ -13,14 +13,26 @@ class DetailHamaPage extends StatefulWidget {
|
|||
_DetailHamaPageState createState() => _DetailHamaPageState();
|
||||
}
|
||||
|
||||
class _DetailHamaPageState extends State<DetailHamaPage> {
|
||||
class _DetailHamaPageState extends State<DetailHamaPage> with TickerProviderStateMixin {
|
||||
late Future<Map<String, dynamic>> _detailHamaFuture;
|
||||
late Map<String, dynamic> _currentDetailHama;
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentDetailHama = widget.detailHama;
|
||||
|
||||
// Initialize animation
|
||||
_animationController = AnimationController(
|
||||
duration: Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
_animationController.forward();
|
||||
|
||||
// Jika hamaId tersedia, fetch data terbaru dari API
|
||||
if (widget.hamaId != null) {
|
||||
|
@ -31,6 +43,12 @@ class _DetailHamaPageState extends State<DetailHamaPage> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _fetchDetailHama(int id) async {
|
||||
try {
|
||||
final detailData = await ApiService().getHamaById(id);
|
||||
|
@ -67,10 +85,37 @@ class _DetailHamaPageState extends State<DetailHamaPage> {
|
|||
future: ApiService().getHamaImageBytesByFilename(filename),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
return Container(
|
||||
height: 280,
|
||||
width: double.infinity,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.grey[300]!, Colors.grey[100]!],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF609966)),
|
||||
strokeWidth: 3,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
"Memuat gambar...",
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasError || snapshot.data == null) {
|
||||
return _buildPlaceholderImage(
|
||||
|
@ -78,13 +123,29 @@ class _DetailHamaPageState extends State<DetailHamaPage> {
|
|||
Icons.broken_image,
|
||||
);
|
||||
} else {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.memory(
|
||||
snapshot.data!,
|
||||
height: 200,
|
||||
return Hero(
|
||||
tag: 'hama_image_${widget.hamaId}',
|
||||
child: Container(
|
||||
height: 280,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.contain, // untuk memastikan proporsional & penuh
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Image.memory(
|
||||
snapshot.data!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -95,149 +156,116 @@ class _DetailHamaPageState extends State<DetailHamaPage> {
|
|||
// Widget untuk placeholder gambar
|
||||
Widget _buildPlaceholderImage(String message, IconData icon) {
|
||||
return Container(
|
||||
height: 200,
|
||||
height: 280,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.grey[300]!, Colors.grey[100]!],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 64, color: Colors.grey[600]),
|
||||
SizedBox(height: 8),
|
||||
Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 48, color: Colors.grey[600]),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
title: Text("Detail Hama", style: TextStyle(color: Colors.white)),
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Widget _buildInfoCard({
|
||||
required String title,
|
||||
required String content,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<Map<String, dynamic>>(
|
||||
future: _detailHamaFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
print('Error: ${snapshot.error}');
|
||||
// Tampilkan data yang sudah ada jika terjadi error
|
||||
return _buildDetailContent(_currentDetailHama);
|
||||
}
|
||||
print("Snapshot data runtimeType: ${snapshot.data.runtimeType}");
|
||||
print("Snapshot data content: ${snapshot.data}");
|
||||
|
||||
// Jika berhasil fetch data baru, tampilkan data tersebut
|
||||
final detailData = snapshot.data ?? _currentDetailHama;
|
||||
return _buildDetailContent(detailData);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailContent(Map<String, dynamic> detailData) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Tampilkan foto dari database dengan penanganan error yang lebih baik
|
||||
_buildImageWidget(detailData["foto"]),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Card Nama Hama
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
elevation: 6,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Nama Hama:",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
detailData["nama"] ?? "Nama hama tidak tersedia",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Card Deskripsi + Penanganan
|
||||
SizedBox(
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
elevation: 6,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Deskripsi:",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
detailData["deskripsi"] ?? "Deskripsi tidak tersedia",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
"Penanganan:",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
detailData["penanganan"] ?? "Penanganan tidak tersedia",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1.6,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -245,4 +273,119 @@ class _DetailHamaPageState extends State<DetailHamaPage> {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFF8F9FA),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
elevation: 0,
|
||||
leading: Container(
|
||||
margin: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
"Detail Hama",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: FutureBuilder<Map<String, dynamic>>(
|
||||
future: _detailHamaFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF609966)),
|
||||
strokeWidth: 4,
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
"Memuat data hama...",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
print('Error: ${snapshot.error}');
|
||||
return _buildDetailContent(_currentDetailHama);
|
||||
}
|
||||
|
||||
final detailData = snapshot.data ?? _currentDetailHama;
|
||||
return _buildDetailContent(detailData);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailContent(Map<String, dynamic> detailData) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Hero Image Section
|
||||
_buildImageWidget(detailData["foto"]),
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Nama Hama Card
|
||||
_buildInfoCard(
|
||||
title: "Nama Hama",
|
||||
content: detailData["nama"] ?? "Nama hama tidak tersedia",
|
||||
icon: Icons.bug_report,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
|
||||
// Deskripsi Card
|
||||
_buildInfoCard(
|
||||
title: "Deskripsi",
|
||||
content: detailData["deskripsi"] ?? "Deskripsi tidak tersedia",
|
||||
icon: Icons.description,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
|
||||
// Penanganan Card
|
||||
_buildInfoCard(
|
||||
title: "Penanganan",
|
||||
content: detailData["penanganan"] ?? "Penanganan tidak tersedia",
|
||||
icon: Icons.medical_services,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
|
||||
// Bottom spacing
|
||||
SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,16 +13,28 @@ class DetailPenyakitPage extends StatefulWidget {
|
|||
_DetailPenyakitPageState createState() => _DetailPenyakitPageState();
|
||||
}
|
||||
|
||||
class _DetailPenyakitPageState extends State<DetailPenyakitPage> {
|
||||
class _DetailPenyakitPageState extends State<DetailPenyakitPage> with TickerProviderStateMixin {
|
||||
late Future<Map<String, dynamic>> _detailPenyakitFuture;
|
||||
late Map<String, dynamic> _currentDetailPenyakit;
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentDetailPenyakit = widget.DetailPenyakit;
|
||||
|
||||
// Initialize animation
|
||||
_animationController = AnimationController(
|
||||
duration: Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
_animationController.forward();
|
||||
|
||||
// Jika hamaId tersedia, fetch data terbaru dari API
|
||||
// Jika penyakitId tersedia, fetch data terbaru dari API
|
||||
if (widget.penyakitId != null) {
|
||||
_detailPenyakitFuture = _fetchDetailPenyakit(widget.penyakitId!);
|
||||
} else {
|
||||
|
@ -31,6 +43,12 @@ class _DetailPenyakitPageState extends State<DetailPenyakitPage> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _fetchDetailPenyakit(int id) async {
|
||||
try {
|
||||
final detailData = await ApiService().getPenyakitById(id);
|
||||
|
@ -67,10 +85,37 @@ class _DetailPenyakitPageState extends State<DetailPenyakitPage> {
|
|||
future: ApiService().getPenyakitImageBytesByFilename(filename),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
return Container(
|
||||
height: 280,
|
||||
width: double.infinity,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.grey[300]!, Colors.grey[100]!],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFE74C3C)),
|
||||
strokeWidth: 3,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
"Memuat gambar...",
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasError || snapshot.data == null) {
|
||||
return _buildPlaceholderImage(
|
||||
|
@ -78,13 +123,29 @@ class _DetailPenyakitPageState extends State<DetailPenyakitPage> {
|
|||
Icons.broken_image,
|
||||
);
|
||||
} else {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.memory(
|
||||
snapshot.data!,
|
||||
height: 200,
|
||||
return Hero(
|
||||
tag: 'penyakit_image_${widget.penyakitId}',
|
||||
child: Container(
|
||||
height: 280,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.contain, // untuk memastikan proporsional & penuh
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Image.memory(
|
||||
snapshot.data!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -95,149 +156,116 @@ class _DetailPenyakitPageState extends State<DetailPenyakitPage> {
|
|||
// Widget untuk placeholder gambar
|
||||
Widget _buildPlaceholderImage(String message, IconData icon) {
|
||||
return Container(
|
||||
height: 200,
|
||||
height: 280,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.grey[300]!, Colors.grey[100]!],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 64, color: Colors.grey[600]),
|
||||
SizedBox(height: 8),
|
||||
Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 48, color: Colors.grey[600]),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
title: Text("Detail Penyakit", style: TextStyle(color: Colors.white)),
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Widget _buildInfoCard({
|
||||
required String title,
|
||||
required String content,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<Map<String, dynamic>>(
|
||||
future: _detailPenyakitFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
print('Error: ${snapshot.error}');
|
||||
// Tampilkan data yang sudah ada jika terjadi error
|
||||
return _buildDetailContent(_currentDetailPenyakit);
|
||||
}
|
||||
print("Snapshot data runtimeType: ${snapshot.data.runtimeType}");
|
||||
print("Snapshot data content: ${snapshot.data}");
|
||||
|
||||
// Jika berhasil fetch data baru, tampilkan data tersebut
|
||||
final detailData = snapshot.data ?? _currentDetailPenyakit;
|
||||
return _buildDetailContent(detailData);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailContent(Map<String, dynamic> detailData) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Tampilkan foto dari database dengan penanganan error yang lebih baik
|
||||
_buildImageWidget(detailData["foto"]),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Card Nama Hama
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
elevation: 6,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Nama Penyakit:",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
detailData["nama"] ?? "Nama hama tidak tersedia",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Card Deskripsi + Penanganan
|
||||
SizedBox(
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
elevation: 6,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Deskripsi:",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
detailData["deskripsi"] ?? "Deskripsi tidak tersedia",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
"Penanganan:",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
detailData["penanganan"] ?? "Penanganan tidak tersedia",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1.6,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -245,4 +273,119 @@ class _DetailPenyakitPageState extends State<DetailPenyakitPage> {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFF8F9FA),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
elevation: 0,
|
||||
leading: Container(
|
||||
margin: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
"Detail Penyakit",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: FutureBuilder<Map<String, dynamic>>(
|
||||
future: _detailPenyakitFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF9DC08D)),
|
||||
strokeWidth: 4,
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
"Memuat data penyakit...",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
print('Error: ${snapshot.error}');
|
||||
return _buildDetailContent(_currentDetailPenyakit);
|
||||
}
|
||||
|
||||
final detailData = snapshot.data ?? _currentDetailPenyakit;
|
||||
return _buildDetailContent(detailData);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailContent(Map<String, dynamic> detailData) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Hero Image Section
|
||||
_buildImageWidget(detailData["foto"]),
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Nama Penyakit Card
|
||||
_buildInfoCard(
|
||||
title: "Nama Penyakit",
|
||||
content: detailData["nama"] ?? "Nama penyakit tidak tersedia",
|
||||
icon: Icons.coronavirus,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
|
||||
// Deskripsi Card
|
||||
_buildInfoCard(
|
||||
title: "Deskripsi",
|
||||
content: detailData["deskripsi"] ?? "Deskripsi tidak tersedia",
|
||||
icon: Icons.description,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
|
||||
// Penanganan Card
|
||||
_buildInfoCard(
|
||||
title: "Penanganan",
|
||||
content: detailData["penanganan"] ?? "Penanganan tidak tersedia",
|
||||
icon: Icons.medical_services,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
|
||||
// Bottom spacing
|
||||
SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,132 +8,316 @@ class ForgotPasswordPage extends StatefulWidget {
|
|||
|
||||
class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
final TextEditingController codeController = TextEditingController();
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
bool isLoading = false;
|
||||
bool isCodeSent = false;
|
||||
|
||||
void _showErrorDialog(String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red),
|
||||
SizedBox(width: 10),
|
||||
Text('Error'),
|
||||
],
|
||||
),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'OK',
|
||||
style: TextStyle(color: Color(0xFF9DC08D)),
|
||||
),
|
||||
),
|
||||
],
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSuccessDialog(String title, String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Color(0xFF9DC08D)),
|
||||
SizedBox(width: 10),
|
||||
Text(title),
|
||||
],
|
||||
),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
if (title == 'Berhasil') {
|
||||
Navigator.of(context).pop(); // kembali ke halaman login
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'OK',
|
||||
style: TextStyle(color: Color(0xFF9DC08D)),
|
||||
),
|
||||
),
|
||||
],
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Fungsi untuk mengirim kode verifikasi
|
||||
void handleSendCode() async {
|
||||
if (emailController.text.trim().isEmpty) {
|
||||
_showErrorDialog('Email harus diisi');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => isLoading = true);
|
||||
|
||||
try {
|
||||
await apiService.sendResetCode(email: emailController.text.trim());
|
||||
|
||||
// Tampilkan dialog input kode verifikasi
|
||||
showVerificationDialog();
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toString())),
|
||||
);
|
||||
} finally {
|
||||
setState(() => isLoading = false);
|
||||
_showSuccessDialog(
|
||||
'Kode Terkirim',
|
||||
'Kode verifikasi telah dikirim ke email Anda.',
|
||||
);
|
||||
showResetPasswordDialog();
|
||||
} catch (e) {
|
||||
setState(() => isLoading = false);
|
||||
_showErrorDialog(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Dialog untuk input kode verifikasi
|
||||
void showVerificationDialog() {
|
||||
// Dialog untuk input kode verifikasi dan reset password
|
||||
void showResetPasswordDialog() {
|
||||
final codeController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final confirmPasswordController = TextEditingController();
|
||||
bool isDialogLoading = false;
|
||||
bool obscurePassword = true;
|
||||
bool obscureConfirmPassword = true;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Masukkan Kode Verifikasi'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Kode verifikasi telah dikirim ke email Anda.'),
|
||||
SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: codeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Kode Verifikasi',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text('Reset Password'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Masukkan kode verifikasi dan password baru Anda.'),
|
||||
SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: codeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Kode Verifikasi',
|
||||
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: obscurePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password Baru',
|
||||
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
obscurePassword ? Icons.visibility : Icons.visibility_off,
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
),
|
||||
onPressed: () {
|
||||
setDialogState(() {
|
||||
obscurePassword = !obscurePassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: confirmPasswordController,
|
||||
obscureText: obscureConfirmPassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Konfirmasi Password',
|
||||
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
obscureConfirmPassword ? Icons.visibility : Icons.visibility_off,
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
),
|
||||
onPressed: () {
|
||||
setDialogState(() {
|
||||
obscureConfirmPassword = !obscureConfirmPassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Verifikasi'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
showNewPasswordDialog();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Dialog untuk input password baru
|
||||
void showNewPasswordDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Reset Password'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password Baru',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Reset'),
|
||||
onPressed: () => handleResetPassword(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Fungsi untuk reset password
|
||||
void handleResetPassword() async {
|
||||
setState(() => isLoading = true);
|
||||
|
||||
try {
|
||||
await apiService.resetPasswordWithCode(
|
||||
code: codeController.text.trim(),
|
||||
password: passwordController.text.trim(),
|
||||
);
|
||||
|
||||
Navigator.pop(context); // Tutup dialog password
|
||||
|
||||
// Tampilkan pesan sukses
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text('Berhasil'),
|
||||
content: Text('Password berhasil direset.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('OK'),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // tutup dialog
|
||||
Navigator.of(context).pop(); // kembali ke halaman login
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: isDialogLoading
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF9DC08D)),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Reset Password',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF9DC08D),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
onPressed: isDialogLoading ? null : () async {
|
||||
// Validasi input
|
||||
if (codeController.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Silakan masukkan kode verifikasi.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Silakan masukkan password baru.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordController.text != confirmPasswordController.text) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Password tidak cocok.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordController.text.length < 6) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Password minimal 6 karakter.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setDialogState(() {
|
||||
isDialogLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Panggil API untuk reset password
|
||||
await apiService.resetPasswordWithCode(
|
||||
code: codeController.text.trim(),
|
||||
password: passwordController.text,
|
||||
);
|
||||
|
||||
setDialogState(() {
|
||||
isDialogLoading = false;
|
||||
});
|
||||
|
||||
// Tutup dialog dan tampilkan pesan sukses
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Password berhasil direset!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Redirect ke login page
|
||||
// Navigator.pushReplacementNamed(context, '/login');
|
||||
|
||||
} catch (e) {
|
||||
setDialogState(() {
|
||||
isDialogLoading = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Terjadi kesalahan: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toString())),
|
||||
);
|
||||
} finally {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
emailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -143,6 +327,7 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
|||
appBar: AppBar(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
title: Text('Lupa Password'),
|
||||
elevation: 0,
|
||||
),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
|
@ -168,9 +353,19 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
|||
controller: emailController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
|
|
|
@ -43,10 +43,14 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
final List<dynamic> penyakitList = data['penyakit'] ?? [];
|
||||
final List<dynamic> hamaList = data['hama'] ?? [];
|
||||
final Map<String, dynamic>? hasilTertinggi = data['hasil_tertinggi'];
|
||||
|
||||
|
||||
// Ambiguity information from backend
|
||||
final bool isAmbiguous = data['is_ambiguous'] ?? false;
|
||||
final Map<String, dynamic>? ambiguityResolution = data['ambiguity_resolution'];
|
||||
final Map<String, dynamic>? ambiguityResolution =
|
||||
data['ambiguity_resolution'];
|
||||
|
||||
// Filter information from backend
|
||||
final Map<String, dynamic>? filterInfo = data['filter_info'];
|
||||
|
||||
// Get the first penyakit and hama (if any)
|
||||
Map<String, dynamic>? firstPenyakit =
|
||||
|
@ -88,81 +92,197 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
),
|
||||
body: Container(
|
||||
color: Color(0xFFEDF1D6),
|
||||
child: isLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Ambiguity notification (if applicable)
|
||||
if (isAmbiguous && ambiguityResolution != null)
|
||||
_buildAmbiguityNotification(ambiguityResolution),
|
||||
child:
|
||||
isLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Filter notification (if applicable)
|
||||
if (filterInfo != null)
|
||||
_buildFilterNotification(filterInfo),
|
||||
|
||||
// Main result display - use hasil_tertinggi from backend
|
||||
_buildDetailedResultFromBackend(context, hasilTertinggi),
|
||||
// Ambiguity notification (if applicable)
|
||||
if (isAmbiguous && ambiguityResolution != null)
|
||||
_buildAmbiguityNotification(ambiguityResolution),
|
||||
|
||||
SizedBox(height: 24),
|
||||
// Main result display - use hasil_tertinggi from backend
|
||||
_buildDetailedResultFromBackend(context, hasilTertinggi),
|
||||
|
||||
// Selected symptoms section
|
||||
_buildSection(
|
||||
context,
|
||||
'Gejala yang Dipilih',
|
||||
widget.gejalaTerpilih.isEmpty
|
||||
? _buildEmptyResult('Tidak ada gejala yang dipilih')
|
||||
: Card(
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Selected symptoms section
|
||||
_buildSection(
|
||||
context,
|
||||
'Gejala yang Dipilih',
|
||||
widget.gejalaTerpilih.isEmpty
|
||||
? _buildEmptyResult('Tidak ada gejala yang dipilih')
|
||||
: Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widget.gejalaTerpilih
|
||||
.map(
|
||||
(gejala) => Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 18,
|
||||
children:
|
||||
widget.gejalaTerpilih
|
||||
.map(
|
||||
(gejala) => Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(child: Text(gejala)),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 18,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(child: Text(gejala)),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Other possible diseases section
|
||||
_buildSection(
|
||||
context,
|
||||
'Kemungkinan Penyakit Lainnya',
|
||||
_buildOtherPossibilities(
|
||||
penyakitList,
|
||||
hasilTertinggi,
|
||||
'penyakit',
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Other possible pests section
|
||||
_buildSection(
|
||||
context,
|
||||
'Kemungkinan Hama Lainnya',
|
||||
_buildOtherPossibilities(
|
||||
hamaList,
|
||||
hasilTertinggi,
|
||||
'hama',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterNotification(Map<String, dynamic> filterInfo) {
|
||||
final int totalSebelum = filterInfo['total_sebelum_filter'] ?? 0;
|
||||
final int totalSetelah = filterInfo['total_setelah_filter'] ?? 0;
|
||||
final int hasilTerfilter = filterInfo['hasil_terfilter'] ?? 0;
|
||||
final bool fallbackToSymptomCount =
|
||||
filterInfo['fallback_to_symptom_count'] ?? false;
|
||||
final String? fallbackReason = filterInfo['fallback_reason'];
|
||||
|
||||
// Only show notification if there's filtering activity
|
||||
if (hasilTerfilter == 0 && !fallbackToSymptomCount) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
Color cardColor;
|
||||
Color iconColor;
|
||||
IconData iconData;
|
||||
String title;
|
||||
String description;
|
||||
|
||||
if (fallbackToSymptomCount) {
|
||||
// Fallback scenario
|
||||
cardColor = Colors.orange.shade50;
|
||||
iconColor = Colors.orange.shade700;
|
||||
iconData = Icons.warning_amber_outlined;
|
||||
title = 'Penyesuaian Hasil Diagnosa';
|
||||
description =
|
||||
'Sistem menggunakan kecocokan gejala terbanyak karena hasil dengan akurasi 100% hanya cocok dengan 1 gejala.';
|
||||
} else {
|
||||
// Normal filtering
|
||||
cardColor = Colors.green.shade50;
|
||||
iconColor = Colors.green.shade700;
|
||||
iconData = Icons.filter_alt_outlined;
|
||||
title = 'Filter Hasil Diagnosa';
|
||||
description =
|
||||
'Sistem memfilter $hasilTerfilter hasil dengan akurasi 100% yang hanya cocok dengan 1 gejala untuk memberikan diagnosis yang lebih akurat.';
|
||||
}
|
||||
|
||||
return Card(
|
||||
color: cardColor,
|
||||
elevation: 2,
|
||||
margin: EdgeInsets.only(bottom: 16),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(iconData, color: iconColor, size: 24),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: iconColor,
|
||||
),
|
||||
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Other possible diseases section
|
||||
_buildSection(
|
||||
context,
|
||||
'Kemungkinan Penyakit Lainnya',
|
||||
_buildOtherPossibilities(penyakitList, hasilTertinggi, 'penyakit'),
|
||||
),
|
||||
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Other possible pests section
|
||||
_buildSection(
|
||||
context,
|
||||
'Kemungkinan Hama Lainnya',
|
||||
_buildOtherPossibilities(hamaList, hasilTertinggi, 'hama'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(description, style: TextStyle(fontSize: 14)),
|
||||
if (fallbackReason != null) ...[
|
||||
SizedBox(height: 8),
|
||||
Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Alasan: $fallbackReason',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.orange.shade800,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (!fallbackToSymptomCount) ...[
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Total hasil: $totalSebelum → $totalSetelah',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -183,11 +303,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: Colors.blue.shade700,
|
||||
size: 24,
|
||||
),
|
||||
Icon(Icons.info_outline, color: Colors.blue.shade700, size: 24),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
|
@ -249,11 +365,13 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
// Determine type based on the presence of id fields
|
||||
String type = '';
|
||||
bool isPenyakit = false;
|
||||
|
||||
if (hasilTertinggi.containsKey('id_penyakit') && hasilTertinggi['id_penyakit'] != null) {
|
||||
|
||||
if (hasilTertinggi.containsKey('id_penyakit') &&
|
||||
hasilTertinggi['id_penyakit'] != null) {
|
||||
type = 'penyakit';
|
||||
isPenyakit = true;
|
||||
} else if (hasilTertinggi.containsKey('id_hama') && hasilTertinggi['id_hama'] != null) {
|
||||
} else if (hasilTertinggi.containsKey('id_hama') &&
|
||||
hasilTertinggi['id_hama'] != null) {
|
||||
type = 'hama';
|
||||
isPenyakit = false;
|
||||
} else {
|
||||
|
@ -266,22 +384,28 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
final completeData = _getCompleteItemData(hasilTertinggi, type);
|
||||
|
||||
// Extract the data we need with safe access
|
||||
final nama = completeData['nama'] ?? hasilTertinggi['nama'] ?? 'Tidak diketahui';
|
||||
final nama =
|
||||
completeData['nama'] ?? hasilTertinggi['nama'] ?? 'Tidak diketahui';
|
||||
final deskripsi = completeData['deskripsi'] ?? 'Tidak tersedia';
|
||||
final penanganan = completeData['penanganan'] ?? 'Tidak tersedia';
|
||||
final foto = completeData['foto'];
|
||||
final probabilitas = _getProbabilitas(hasilTertinggi);
|
||||
|
||||
|
||||
// Get additional ambiguity info if available
|
||||
final jumlahGejalacocok = hasilTertinggi['jumlah_gejala_cocok'];
|
||||
final totalGejalaEntity = hasilTertinggi['total_gejala_entity'];
|
||||
final persentaseKesesuaian = hasilTertinggi['persentase_kesesuaian'];
|
||||
|
||||
// Check if this is a perfect match result that would normally be filtered
|
||||
final isPerfectSingleMatch =
|
||||
(probabilitas * 100).round() == 100 && jumlahGejalacocok == 1;
|
||||
|
||||
// Debug log
|
||||
print('DEBUG - Building detailed result for: $nama');
|
||||
print('DEBUG - Type: $type, isPenyakit: $isPenyakit');
|
||||
print('DEBUG - Probabilitas: $probabilitas');
|
||||
|
||||
print('DEBUG - Is perfect single match: $isPerfectSingleMatch');
|
||||
|
||||
return Card(
|
||||
elevation: 6,
|
||||
shape: RoundedRectangleBorder(
|
||||
|
@ -300,7 +424,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
children: [
|
||||
Icon(
|
||||
isPenyakit ? Icons.coronavirus_outlined : Icons.bug_report,
|
||||
color: isPenyakit ? Colors.red.shade700 : Colors.orange.shade700,
|
||||
color:
|
||||
isPenyakit ? Colors.red.shade700 : Colors.orange.shade700,
|
||||
size: 28,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
|
@ -310,14 +435,49 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isPenyakit ? Colors.red.shade700 : Colors.orange.shade700,
|
||||
color:
|
||||
isPenyakit
|
||||
? Colors.red.shade700
|
||||
: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildProbabilityIndicator(probabilitas),
|
||||
],
|
||||
),
|
||||
|
||||
|
||||
// Show warning if this is a perfect single match result
|
||||
if (isPerfectSingleMatch)
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.yellow.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.yellow.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Hasil ini dipilih berdasarkan kecocokan gejala terbanyak',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.orange.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Additional info if ambiguity resolution occurred
|
||||
if (jumlahGejalacocok != null && totalGejalaEntity != null)
|
||||
Container(
|
||||
|
@ -329,7 +489,11 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.analytics_outlined, size: 16, color: Colors.grey.shade600),
|
||||
Icon(
|
||||
Icons.analytics_outlined,
|
||||
size: 16,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'Kesesuaian: $jumlahGejalacocok/$totalGejalaEntity gejala',
|
||||
|
@ -350,7 +514,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
Divider(thickness: 1, height: 24),
|
||||
|
||||
// Image section
|
||||
|
@ -431,53 +595,76 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildOtherPossibilities(
|
||||
Widget _buildOtherPossibilities(
|
||||
List<dynamic> itemList,
|
||||
Map<String, dynamic>? hasilTertinggi,
|
||||
String type,
|
||||
) {
|
||||
if (itemList.isEmpty) {
|
||||
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
|
||||
) {
|
||||
// Check if there's a 100% match in hasilTertinggi
|
||||
if (hasilTertinggi != null) {
|
||||
double probabilitas = _getProbabilitas(hasilTertinggi);
|
||||
if ((probabilitas * 100).round() == 100) {
|
||||
return _buildEmptyResult(
|
||||
'Ditemukan kecocokan 100% pada diagnosa utama',
|
||||
);
|
||||
}
|
||||
|
||||
// Filter out the top result that's already shown
|
||||
List<dynamic> otherItems = [];
|
||||
|
||||
if (hasilTertinggi != null) {
|
||||
// Get the ID of the top result
|
||||
String? topResultId;
|
||||
if (type == 'penyakit' && hasilTertinggi.containsKey('id_penyakit')) {
|
||||
topResultId = hasilTertinggi['id_penyakit']?.toString();
|
||||
} else if (type == 'hama' && hasilTertinggi.containsKey('id_hama')) {
|
||||
topResultId = hasilTertinggi['id_hama']?.toString();
|
||||
}
|
||||
|
||||
// Filter out the top result
|
||||
otherItems = itemList.where((item) {
|
||||
String? itemId;
|
||||
if (type == 'penyakit') {
|
||||
itemId = item['id_penyakit']?.toString();
|
||||
} else {
|
||||
itemId = item['id_hama']?.toString();
|
||||
}
|
||||
return topResultId == null || itemId != topResultId;
|
||||
}).toList();
|
||||
} else {
|
||||
// If no top result, skip the first item
|
||||
otherItems = itemList.skip(1).toList();
|
||||
}
|
||||
|
||||
if (otherItems.isEmpty) {
|
||||
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: otherItems
|
||||
.map((item) => _buildItemCard(item, type))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
if (itemList.isEmpty) {
|
||||
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
|
||||
}
|
||||
|
||||
// Filter out items with 100% probability and the top result
|
||||
List otherItems = [];
|
||||
|
||||
if (hasilTertinggi != null) {
|
||||
// Get the ID of the top result
|
||||
String? topResultId;
|
||||
if (type == 'penyakit' && hasilTertinggi.containsKey('id_penyakit')) {
|
||||
topResultId = hasilTertinggi['id_penyakit']?.toString();
|
||||
} else if (type == 'hama' && hasilTertinggi.containsKey('id_hama')) {
|
||||
topResultId = hasilTertinggi['id_hama']?.toString();
|
||||
}
|
||||
|
||||
// Filter out the top result AND items with 100% probability
|
||||
otherItems = itemList.where((item) {
|
||||
String? itemId;
|
||||
if (type == 'penyakit') {
|
||||
itemId = item['id_penyakit']?.toString();
|
||||
} else {
|
||||
itemId = item['id_hama']?.toString();
|
||||
}
|
||||
|
||||
// Skip if this is the top result
|
||||
if (topResultId != null && itemId == topResultId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip if this item has 100% probability
|
||||
double itemProbabilitas = _getProbabilitas(item);
|
||||
if ((itemProbabilitas * 100).round() == 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
} else {
|
||||
// If no hasilTertinggi, filter out 100% probability items from all except first
|
||||
otherItems = itemList.skip(1).where((item) {
|
||||
double itemProbabilitas = _getProbabilitas(item);
|
||||
return (itemProbabilitas * 100).round() != 100;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
if (otherItems.isEmpty) {
|
||||
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: otherItems.map((item) => _buildItemCard(item, type)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _fetchAdditionalData() async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
|
@ -518,7 +705,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
if (detail.isNotEmpty) {
|
||||
double probability = 0.0;
|
||||
if (penyakit.containsKey('probabilitas_persen')) {
|
||||
probability = (penyakit['probabilitas_persen'] as num).toDouble() / 100;
|
||||
probability =
|
||||
(penyakit['probabilitas_persen'] as num).toDouble() / 100;
|
||||
} else if (penyakit.containsKey('nilai_bayes')) {
|
||||
probability = (penyakit['nilai_bayes'] as num).toDouble();
|
||||
}
|
||||
|
@ -529,7 +717,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
'id_penyakit': penyakitIdStr,
|
||||
};
|
||||
|
||||
final nama = penyakitDetails[penyakitIdStr]?['nama'] ?? 'Nama tidak ditemukan';
|
||||
final nama =
|
||||
penyakitDetails[penyakitIdStr]?['nama'] ?? 'Nama tidak ditemukan';
|
||||
print('DEBUG - Found details for penyakit ID $penyakitIdStr: $nama');
|
||||
}
|
||||
}
|
||||
|
@ -561,7 +750,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
'id_hama': hamaIdStr,
|
||||
};
|
||||
|
||||
final nama = hamaDetails[hamaIdStr]?['nama'] ?? 'Nama tidak ditemukan';
|
||||
final nama =
|
||||
hamaDetails[hamaIdStr]?['nama'] ?? 'Nama tidak ditemukan';
|
||||
print('DEBUG - Found details for hama ID $hamaIdStr: $nama');
|
||||
}
|
||||
}
|
||||
|
@ -597,7 +787,9 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
details = penyakitDetails[idStr];
|
||||
|
||||
if (details == null || details.isEmpty) {
|
||||
print('DEBUG - No cached details for penyakit ID: $idStr, searching API data...');
|
||||
print(
|
||||
'DEBUG - No cached details for penyakit ID: $idStr, searching API data...',
|
||||
);
|
||||
details = semuaPenyakit.firstWhere(
|
||||
(p) => p['id'].toString() == idStr,
|
||||
orElse: () => <String, dynamic>{},
|
||||
|
@ -611,7 +803,9 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
details = hamaDetails[idStr];
|
||||
|
||||
if (details == null || details.isEmpty) {
|
||||
print('DEBUG - No cached details for hama ID: $idStr, searching API data...');
|
||||
print(
|
||||
'DEBUG - No cached details for hama ID: $idStr, searching API data...',
|
||||
);
|
||||
details = semuaHama.firstWhere(
|
||||
(h) => h['id'].toString() == idStr,
|
||||
orElse: () => <String, dynamic>{},
|
||||
|
@ -644,7 +838,9 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
type == 'penyakit' ? 'id_penyakit' : 'id_hama': idStr,
|
||||
};
|
||||
|
||||
print('DEBUG - Final data for $type ID $idStr (${result['nama']}): probabilitas=${result['probabilitas']}');
|
||||
print(
|
||||
'DEBUG - Final data for $type ID $idStr (${result['nama']}): probabilitas=${result['probabilitas']}',
|
||||
);
|
||||
} else {
|
||||
print('DEBUG - No details found for $type ID $idStr');
|
||||
}
|
||||
|
@ -656,32 +852,63 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
final completeData = _getCompleteItemData(item, type);
|
||||
final nama = completeData['nama'] ?? 'Tidak diketahui';
|
||||
final probabilitas = _getProbabilitas(completeData);
|
||||
|
||||
|
||||
// Get additional info for display
|
||||
final jumlahGejalacocok = item['jumlah_gejala_cocok'];
|
||||
final totalGejalaEntity = item['total_gejala_entity'];
|
||||
final persentaseKesesuaian = item['persentase_kesesuaian'];
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.only(bottom: 8),
|
||||
elevation: 2,
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
type == 'penyakit' ? Icons.coronavirus_outlined : Icons.bug_report,
|
||||
color: type == 'penyakit' ? Colors.red.shade700 : Colors.orange.shade700,
|
||||
color: type == 'penyakit' ? Color(0xFF9DC08D) : Color(0xFF7A9A6D),
|
||||
size: 24,
|
||||
),
|
||||
title: Text(nama),
|
||||
subtitle: jumlahGejalacocok != null && totalGejalaEntity != null
|
||||
? Text(
|
||||
'Kesesuaian: $jumlahGejalacocok/$totalGejalaEntity gejala',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
)
|
||||
: null,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
title: Text(
|
||||
nama,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF40513B),
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildProbabilityIndicator(probabilitas),
|
||||
SizedBox(width: 8),
|
||||
if (jumlahGejalacocok != null && totalGejalaEntity != null) ...[
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Kesesuaian: $jumlahGejalacocok/$totalGejalaEntity gejala',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
if (persentaseKesesuaian != null)
|
||||
Text(
|
||||
'(${persentaseKesesuaian.toStringAsFixed(1)}%)',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: Container(
|
||||
width: 60,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: type == 'penyakit' ? Color(0xFF9DC08D) : Color(0xFF7A9A6D),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${(probabilitas * 100).toStringAsFixed(0)}%',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -732,9 +959,10 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
|||
}
|
||||
|
||||
Widget _buildProbabilityIndicator(double value) {
|
||||
final Color indicatorColor = value > 0.7
|
||||
? Colors.red
|
||||
: value > 0.4
|
||||
final Color indicatorColor =
|
||||
value > 0.7
|
||||
? Colors.red
|
||||
: value > 0.4
|
||||
? Colors.orange
|
||||
: Colors.green;
|
||||
|
||||
|
@ -832,4 +1060,4 @@ Widget _buildProbabilityIndicator(double value) {
|
|||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,34 @@ class _LoginPageState extends State<LoginPage> {
|
|||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
bool _isPasswordVisible = false;
|
||||
|
||||
// Method to show error dialog
|
||||
void _showErrorDialog(String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red),
|
||||
SizedBox(width: 10),
|
||||
Text('Error'),
|
||||
],
|
||||
),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('OK', style: TextStyle(color: Color(0xFF9DC08D))),
|
||||
),
|
||||
],
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
setState(() {
|
||||
|
@ -27,12 +54,10 @@ class _LoginPageState extends State<LoginPage> {
|
|||
String password = _passwordController.text.trim();
|
||||
|
||||
if (email.isEmpty || password.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Email dan Password harus diisi")),
|
||||
);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
_showErrorDialog('Email dan Password harus diisi');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -41,10 +66,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
var response = await http.post(
|
||||
url,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"email": email,
|
||||
"password": password,
|
||||
}),
|
||||
body: jsonEncode({"email": email, "password": password}),
|
||||
);
|
||||
|
||||
var responseData = jsonDecode(response.body);
|
||||
|
@ -56,7 +78,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
await prefs.setString('token', responseData['token']);
|
||||
await prefs.setString('role', responseData['role']);
|
||||
await prefs.setString('email', email);
|
||||
await prefs.setString('userId', responseData['userId'].toString());
|
||||
await prefs.setString('userId', responseData['userId'].toString());
|
||||
|
||||
// Redirect berdasarkan role
|
||||
if (responseData['role'] == 'admin') {
|
||||
|
@ -71,19 +93,19 @@ class _LoginPageState extends State<LoginPage> {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(responseData['message'] ?? "Login gagal")),
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
_showErrorDialog(
|
||||
responseData['message'] ?? 'Email atau Password salah',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Terjadi kesalahan: $e")),
|
||||
);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
_showErrorDialog('Terjadi kesalahan koneksi. Silakan coba lagi.');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -109,10 +131,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/bayam.png',
|
||||
height: 150,
|
||||
),
|
||||
Image.asset('assets/images/bayam.png', height: 150),
|
||||
SizedBox(height: 30),
|
||||
Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
|
@ -128,20 +147,54 @@ class _LoginPageState extends State<LoginPage> {
|
|||
controller: _emailController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'email',
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
obscureText:
|
||||
!_isPasswordVisible, // Toggle visibility based on state
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_isPasswordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isPasswordVisible = !_isPasswordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
|
@ -156,18 +209,19 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
),
|
||||
onPressed: _isLoading ? null : _login,
|
||||
child: _isLoading
|
||||
? CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
)
|
||||
: Text(
|
||||
'Login',
|
||||
style: TextStyle(
|
||||
child:
|
||||
_isLoading
|
||||
? CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
)
|
||||
: Text(
|
||||
'Login',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -175,11 +229,11 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
@ -187,17 +241,39 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'Belum punya akun? Daftar',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
decoration: TextDecoration.underline,
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_add_outlined,
|
||||
color: Color(0xFF9DC08D),
|
||||
size: 16,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Belum punya akun? Daftar',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF9DC08D),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
@ -205,12 +281,34 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'Lupa Password?',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
decoration: TextDecoration.underline,
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lock_reset_outlined,
|
||||
color: Color(0xFF9DC08D),
|
||||
size: 16,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Lupa Password?',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF9DC08D),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -142,7 +142,13 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text('Update Profil'),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.edit, color: Color(0xFF9DC08D)),
|
||||
SizedBox(width: 8),
|
||||
Text('Update Profil'),
|
||||
],
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
|
@ -151,16 +157,40 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
children: [
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(labelText: 'Nama'),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama',
|
||||
prefixIcon: Icon(
|
||||
Icons.person,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Color(0xFF9DC08D)),
|
||||
),
|
||||
),
|
||||
validator:
|
||||
(value) =>
|
||||
value?.isEmpty ?? true
|
||||
? 'Nama tidak boleh kosong'
|
||||
: null,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: InputDecoration(labelText: 'Email'),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
prefixIcon: Icon(Icons.email, color: Color(0xFF9DC08D)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Color(0xFF9DC08D)),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value?.isEmpty ?? true)
|
||||
return 'Email tidak boleh kosong';
|
||||
|
@ -168,12 +198,21 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password Baru',
|
||||
helperText:
|
||||
'Kosongkan jika tidak ingin mengubah password',
|
||||
prefixIcon: Icon(Icons.lock, color: Color(0xFF9DC08D)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Color(0xFF9DC08D)),
|
||||
),
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
|
@ -184,18 +223,43 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _alamatController,
|
||||
decoration: InputDecoration(labelText: 'Alamat'),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Alamat',
|
||||
prefixIcon: Icon(
|
||||
Icons.location_on,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Color(0xFF9DC08D)),
|
||||
),
|
||||
),
|
||||
validator:
|
||||
(value) =>
|
||||
value?.isEmpty ?? true
|
||||
? 'Alamat tidak boleh kosong'
|
||||
: null,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _nomorTeleponController,
|
||||
decoration: InputDecoration(labelText: 'Nomor Telepon'),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nomor Telepon',
|
||||
prefixIcon: Icon(Icons.phone, color: Color(0xFF9DC08D)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Color(0xFF9DC08D)),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
validator:
|
||||
(value) =>
|
||||
|
@ -208,11 +272,12 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
TextButton.icon(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('Batal'),
|
||||
icon: Icon(Icons.cancel, color: Colors.grey),
|
||||
label: Text('Batal', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
ElevatedButton(
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
try {
|
||||
|
@ -234,15 +299,29 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Profil berhasil diperbarui'),
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.white),
|
||||
SizedBox(width: 8),
|
||||
Text('Profil berhasil diperbarui'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Gagal memperbarui profil: ${e.toString()}',
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.error, color: Colors.white),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Gagal memperbarui profil: ${e.toString()}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
|
@ -250,10 +329,11 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
}
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.save, color: Colors.white),
|
||||
label: Text('Update', style: TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
),
|
||||
child: Text('Update'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -266,21 +346,38 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
backgroundColor: Color(0xFF9DC08D),
|
||||
body: Stack(
|
||||
children: [
|
||||
// Judul halaman tetap di luar border (di bagian atas dan center)
|
||||
// Background decoration
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF9DC08D), Color(0xFF8BB37A)],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Judul halaman dengan icon
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 40.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Profil Pengguna",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.account_circle, color: Colors.white, size: 32),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
"Profil Pengguna",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 80),
|
||||
],
|
||||
|
@ -292,32 +389,38 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
Positioned(
|
||||
top: 40.0,
|
||||
left: 16.0,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Colors.white, size: 28),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Colors.white, size: 28),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Isi halaman
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 36.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Card box untuk data pengguna
|
||||
Container(
|
||||
height: 200,
|
||||
width: 450,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 4,
|
||||
elevation: 8,
|
||||
shadowColor: Colors.black.withOpacity(0.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child:
|
||||
isLoading
|
||||
? _buildLoadingState()
|
||||
|
@ -329,51 +432,72 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
),
|
||||
SizedBox(height: 30),
|
||||
|
||||
// Button untuk update data profil
|
||||
ElevatedButton(
|
||||
onPressed: _showUpdateProfileDialog,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
// Buttons row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Button untuk update data profil
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _showUpdateProfileDialog,
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: Color(0xFF9DC08D),
|
||||
size: 20,
|
||||
),
|
||||
label: Text(
|
||||
"Update Profil",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF9DC08D),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
SizedBox(width: 12),
|
||||
// Button untuk logout
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _logout(context),
|
||||
icon: Icon(
|
||||
Icons.logout,
|
||||
color: Colors.red[700],
|
||||
size: 20,
|
||||
),
|
||||
label: Text(
|
||||
"Logout",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.red[700],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Update Data Profil",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Color(0xFF9DC08D),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
|
||||
// Button untuk logout
|
||||
ElevatedButton(
|
||||
onPressed: () => _logout(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Logout",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Color(0xFF9DC08D),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -392,7 +516,14 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
children: [
|
||||
CircularProgressIndicator(color: Color(0xFF9DC08D)),
|
||||
SizedBox(height: 16),
|
||||
Text("Memuat data profil..."),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.hourglass_empty, color: Color(0xFF9DC08D)),
|
||||
SizedBox(width: 8),
|
||||
Text("Memuat data profil..."),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -412,7 +543,17 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
TextButton(onPressed: _loadUserData, child: Text("Coba Lagi")),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _loadUserData,
|
||||
icon: Icon(Icons.refresh, color: Colors.white),
|
||||
label: Text("Coba Lagi", style: TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -427,20 +568,91 @@ class _ProfilPageState extends State<ProfilPage> {
|
|||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildProfileItem("Nama: ${userData?['name'] ?? '-'}"),
|
||||
Divider(color: Colors.black),
|
||||
_buildProfileItem("Email: ${userData?['email'] ?? '-'}"),
|
||||
Divider(color: Colors.black),
|
||||
_buildProfileItem("Alamat: ${userData?['alamat'] ?? '-'}"),
|
||||
// Header card
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Color(0xFF9DC08D), size: 24),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
"Informasi Profil",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
_buildProfileItem(Icons.person, "Nama", userData?['name'] ?? '-'),
|
||||
SizedBox(height: 12),
|
||||
|
||||
_buildProfileItem(Icons.email, "Email", userData?['email'] ?? '-'),
|
||||
SizedBox(height: 12),
|
||||
|
||||
_buildProfileItem(
|
||||
Icons.location_on,
|
||||
"Alamat",
|
||||
userData?['alamat'] ?? '-',
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
|
||||
_buildProfileItem(
|
||||
Icons.admin_panel_settings,
|
||||
"Role",
|
||||
userData?['role'] ?? '-',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Fungsi untuk membuat item dalam Card box
|
||||
Widget _buildProfileItem(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Text(text, style: TextStyle(fontSize: 18)),
|
||||
// Fungsi untuk membuat item dalam Card box dengan icon
|
||||
Widget _buildProfileItem(IconData icon, String label, String value) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF9DC08D).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(icon, color: Color(0xFF9DC08D), size: 20),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,113 @@ import 'package:flutter/material.dart';
|
|||
import 'package:frontend/api_services/api_services.dart';
|
||||
|
||||
// Halaman Pendaftaran
|
||||
class RegisterPage extends StatelessWidget {
|
||||
class RegisterPage extends StatefulWidget {
|
||||
@override
|
||||
_RegisterPageState createState() => _RegisterPageState();
|
||||
}
|
||||
|
||||
class _RegisterPageState extends State<RegisterPage> {
|
||||
final TextEditingController nameController = TextEditingController();
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
final TextEditingController alamatController = TextEditingController();
|
||||
final TextEditingController nomorHpController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
void _showErrorDialog(String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red),
|
||||
SizedBox(width: 10),
|
||||
Text('Registrasi Gagal'),
|
||||
],
|
||||
),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'OK',
|
||||
style: TextStyle(color: Color(0xFF9DC08D)),
|
||||
),
|
||||
),
|
||||
],
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSuccessDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Color(0xFF9DC08D)),
|
||||
SizedBox(width: 10),
|
||||
Text('Berhasil'),
|
||||
],
|
||||
),
|
||||
content: Text('Registrasi berhasil! Silahkan login dengan akun Anda.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // Close dialog
|
||||
Navigator.of(context).pop(); // Back to login page
|
||||
},
|
||||
child: Text(
|
||||
'OK',
|
||||
style: TextStyle(color: Color(0xFF9DC08D)),
|
||||
),
|
||||
),
|
||||
],
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleRegister() async {
|
||||
// Validasi input
|
||||
if (nameController.text.isEmpty ||
|
||||
emailController.text.isEmpty ||
|
||||
passwordController.text.isEmpty ||
|
||||
alamatController.text.isEmpty ||
|
||||
nomorHpController.text.isEmpty) {
|
||||
_showErrorDialog('Semua field harus diisi');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await apiService.registerUser(
|
||||
name: nameController.text,
|
||||
email: emailController.text,
|
||||
password: passwordController.text,
|
||||
alamat: alamatController.text,
|
||||
nomorTelepon: nomorHpController.text,
|
||||
);
|
||||
_showSuccessDialog();
|
||||
} catch (e) {
|
||||
_showErrorDialog(e.toString());
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -35,9 +133,14 @@ class RegisterPage extends StatelessWidget {
|
|||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama',
|
||||
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||
),
|
||||
),
|
||||
controller: nameController,
|
||||
),
|
||||
|
@ -46,9 +149,14 @@ class RegisterPage extends StatelessWidget {
|
|||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||
),
|
||||
),
|
||||
controller: passwordController,
|
||||
),
|
||||
|
@ -56,9 +164,14 @@ class RegisterPage extends StatelessWidget {
|
|||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||
),
|
||||
),
|
||||
controller: emailController,
|
||||
),
|
||||
|
@ -66,22 +179,32 @@ class RegisterPage extends StatelessWidget {
|
|||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Alamat',
|
||||
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||
),
|
||||
),
|
||||
controller: alamatController,
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nomor HP',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
controller: nomorHpController,
|
||||
),
|
||||
// TextField(
|
||||
// decoration: InputDecoration(
|
||||
// labelText: 'Nomor HP',
|
||||
// labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||
// border: OutlineInputBorder(
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// focusedBorder: OutlineInputBorder(
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// controller: nomorHpController,
|
||||
// ),
|
||||
SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
@ -93,33 +216,17 @@ class RegisterPage extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await apiService.registerUser(
|
||||
name: nameController.text,
|
||||
email: emailController.text,
|
||||
password: passwordController.text,
|
||||
alamat: alamatController.text,
|
||||
nomorTelepon: nomorHpController.text,
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Registrasi berhasil!')),
|
||||
);
|
||||
Navigator.pop(context); // kembali ke login page
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Registrasi gagal: $e')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'Daftar',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
onPressed: _isLoading ? null : _handleRegister,
|
||||
child: _isLoading
|
||||
? CircularProgressIndicator(color: Colors.white)
|
||||
: Text(
|
||||
'Daftar',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue