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
|
# 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');
|
const { Sequelize } = require('sequelize');
|
||||||
require('dotenv').config();
|
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,
|
host: process.env.DB_HOST,
|
||||||
|
port: process.env.DB_PORT, // tambahkan port
|
||||||
dialect: 'mysql',
|
dialect: 'mysql',
|
||||||
timezone: '+07:00',
|
timezone: '+07:00',
|
||||||
});
|
dialectOptions: {
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false, // jika diperlukan (tergantung Clever Cloud)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = sequelize;
|
module.exports = sequelize;
|
|
@ -113,9 +113,15 @@ const createGmailTransporter = () => {
|
||||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
|
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({
|
await user.update({
|
||||||
resetToken: code,
|
resetToken: code,
|
||||||
resetTokenExpiry: expiresAt,
|
resetTokenExpiry: isoWIB,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Nama aplikasi yang konsisten
|
// Nama aplikasi yang konsisten
|
||||||
|
|
|
@ -134,6 +134,24 @@ async function resolveAmbiguity(candidates, inputGejala) {
|
||||||
return candidates[0]; // Kembalikan yang terbaik
|
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) => {
|
exports.diagnosa = async (req, res) => {
|
||||||
const { gejala } = req.body;
|
const { gejala } = req.body;
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
|
@ -195,16 +213,41 @@ exports.diagnosa = async (req, res) => {
|
||||||
...sortedHama.map(h => ({ type: 'hama', ...h }))
|
...sortedHama.map(h => ({ type: 'hama', ...h }))
|
||||||
].sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
|
].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 ==========
|
// ========== PENANGANAN AMBIGUITAS ==========
|
||||||
let hasilTertinggi = null;
|
let hasilTertinggi = null;
|
||||||
let isAmbiguous = false;
|
let isAmbiguous = false;
|
||||||
let ambiguityResolution = null;
|
let ambiguityResolution = null;
|
||||||
|
|
||||||
if (allResults.length > 0) {
|
if (finalResults.length > 0) {
|
||||||
const nilaiTertinggi = allResults[0].probabilitas_persen;
|
const nilaiTertinggi = finalResults[0].probabilitas_persen;
|
||||||
|
|
||||||
// Cari semua hasil dengan nilai probabilitas yang sama dengan yang tertinggi
|
// 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
|
Math.abs(result.probabilitas_persen - nilaiTertinggi) < 0.0001 // Toleransi untuk floating point
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -235,7 +278,38 @@ exports.diagnosa = async (req, res) => {
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Tidak ada ambiguitas
|
// 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)),
|
gejala_input: gejala.map(id => parseInt(id)),
|
||||||
hasil_tertinggi: hasilTertinggi,
|
hasil_tertinggi: hasilTertinggi,
|
||||||
is_ambiguous: isAmbiguous,
|
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 process = require('process');
|
||||||
const basename = path.basename(__filename);
|
const basename = path.basename(__filename);
|
||||||
const env = process.env.NODE_ENV || 'development';
|
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 = {};
|
const db = {};
|
||||||
|
|
||||||
let sequelize;
|
let sequelize;
|
||||||
|
|
|
@ -177,11 +177,16 @@ class _RulePageState extends State<RulePage> {
|
||||||
),
|
),
|
||||||
).then((_) => fetchRules());
|
).then((_) => fetchRules());
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.bug_report),
|
icon: Icon(Icons.bug_report, size: 16,),
|
||||||
label: Text("Tambah Rule Hama"),
|
label: Text(
|
||||||
style: ElevatedButton.styleFrom(
|
"Tambah Rule Hama",
|
||||||
|
style: TextStyle(fontSize: 12)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
foregroundColor: Colors.white,
|
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),
|
SizedBox(width: 10),
|
||||||
|
@ -204,11 +209,16 @@ class _RulePageState extends State<RulePage> {
|
||||||
),
|
),
|
||||||
).then((_) => fetchRules());
|
).then((_) => fetchRules());
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.healing),
|
icon: Icon(Icons.healing, size: 16,),
|
||||||
label: Text("Tambah Rule Penyakit"),
|
label: Text(
|
||||||
style: ElevatedButton.styleFrom(
|
"Tambah Rule Penyakit",
|
||||||
|
style: TextStyle(fontSize: 12),),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
foregroundColor: Colors.white,
|
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
|
// Create Rule penyakit
|
||||||
static Future<http.Response> createRulePenyakit({
|
static Future<http.Response> createRulePenyakit({
|
||||||
|
|
|
@ -13,15 +13,27 @@ class DetailHamaPage extends StatefulWidget {
|
||||||
_DetailHamaPageState createState() => _DetailHamaPageState();
|
_DetailHamaPageState createState() => _DetailHamaPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DetailHamaPageState extends State<DetailHamaPage> {
|
class _DetailHamaPageState extends State<DetailHamaPage> with TickerProviderStateMixin {
|
||||||
late Future<Map<String, dynamic>> _detailHamaFuture;
|
late Future<Map<String, dynamic>> _detailHamaFuture;
|
||||||
late Map<String, dynamic> _currentDetailHama;
|
late Map<String, dynamic> _currentDetailHama;
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _fadeAnimation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_currentDetailHama = widget.detailHama;
|
_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
|
// Jika hamaId tersedia, fetch data terbaru dari API
|
||||||
if (widget.hamaId != null) {
|
if (widget.hamaId != null) {
|
||||||
_detailHamaFuture = _fetchDetailHama(widget.hamaId!);
|
_detailHamaFuture = _fetchDetailHama(widget.hamaId!);
|
||||||
|
@ -31,6 +43,12 @@ class _DetailHamaPageState extends State<DetailHamaPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _fetchDetailHama(int id) async {
|
Future<Map<String, dynamic>> _fetchDetailHama(int id) async {
|
||||||
try {
|
try {
|
||||||
final detailData = await ApiService().getHamaById(id);
|
final detailData = await ApiService().getHamaById(id);
|
||||||
|
@ -67,10 +85,37 @@ class _DetailHamaPageState extends State<DetailHamaPage> {
|
||||||
future: ApiService().getHamaImageBytesByFilename(filename),
|
future: ApiService().getHamaImageBytesByFilename(filename),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return SizedBox(
|
return Container(
|
||||||
height: 200,
|
height: 280,
|
||||||
width: double.infinity,
|
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) {
|
} else if (snapshot.hasError || snapshot.data == null) {
|
||||||
return _buildPlaceholderImage(
|
return _buildPlaceholderImage(
|
||||||
|
@ -78,13 +123,29 @@ class _DetailHamaPageState extends State<DetailHamaPage> {
|
||||||
Icons.broken_image,
|
Icons.broken_image,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return ClipRRect(
|
return Hero(
|
||||||
borderRadius: BorderRadius.circular(12),
|
tag: 'hama_image_${widget.hamaId}',
|
||||||
child: Image.memory(
|
child: Container(
|
||||||
snapshot.data!,
|
height: 280,
|
||||||
height: 200,
|
|
||||||
width: double.infinity,
|
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 untuk placeholder gambar
|
||||||
Widget _buildPlaceholderImage(String message, IconData icon) {
|
Widget _buildPlaceholderImage(String message, IconData icon) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 200,
|
height: 280,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[200],
|
borderRadius: BorderRadius.circular(20),
|
||||||
borderRadius: BorderRadius.circular(12),
|
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(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 64, color: Colors.grey[600]),
|
Container(
|
||||||
SizedBox(height: 8),
|
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(
|
Text(
|
||||||
message,
|
message,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[700],
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Widget _buildInfoCard({
|
||||||
Widget build(BuildContext context) {
|
required String title,
|
||||||
return Scaffold(
|
required String content,
|
||||||
backgroundColor: Color(0xFF9DC08D),
|
required IconData icon,
|
||||||
appBar: AppBar(
|
required Color color,
|
||||||
backgroundColor: Color(0xFF9DC08D),
|
}) {
|
||||||
title: Text("Detail Hama", style: TextStyle(color: Colors.white)),
|
return Container(
|
||||||
leading: IconButton(
|
margin: EdgeInsets.only(bottom: 20),
|
||||||
icon: Icon(Icons.arrow_back, color: Colors.white),
|
decoration: BoxDecoration(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
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(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Tampilkan foto dari database dengan penanganan error yang lebih baik
|
Row(
|
||||||
_buildImageWidget(detailData["foto"]),
|
children: [
|
||||||
SizedBox(height: 16),
|
Container(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
// Card Nama Hama
|
decoration: BoxDecoration(
|
||||||
SizedBox(
|
color: color.withOpacity(0.1),
|
||||||
width: double.infinity,
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Card(
|
),
|
||||||
elevation: 6,
|
child: Icon(
|
||||||
shape: RoundedRectangleBorder(
|
icon,
|
||||||
borderRadius: BorderRadius.circular(12),
|
color: color,
|
||||||
),
|
size: 24,
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: 16),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
// Card Deskripsi + Penanganan
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Card(
|
padding: EdgeInsets.all(16),
|
||||||
elevation: 6,
|
decoration: BoxDecoration(
|
||||||
shape: RoundedRectangleBorder(
|
color: Colors.grey[50],
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
border: Border.all(color: Colors.grey[200]!),
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.all(16.0),
|
child: Text(
|
||||||
child: Column(
|
content,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
style: TextStyle(
|
||||||
children: [
|
fontSize: 16,
|
||||||
Text(
|
height: 1.6,
|
||||||
"Deskripsi:",
|
color: Colors.grey[700],
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
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();
|
_DetailPenyakitPageState createState() => _DetailPenyakitPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DetailPenyakitPageState extends State<DetailPenyakitPage> {
|
class _DetailPenyakitPageState extends State<DetailPenyakitPage> with TickerProviderStateMixin {
|
||||||
late Future<Map<String, dynamic>> _detailPenyakitFuture;
|
late Future<Map<String, dynamic>> _detailPenyakitFuture;
|
||||||
late Map<String, dynamic> _currentDetailPenyakit;
|
late Map<String, dynamic> _currentDetailPenyakit;
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _fadeAnimation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_currentDetailPenyakit = widget.DetailPenyakit;
|
_currentDetailPenyakit = widget.DetailPenyakit;
|
||||||
|
|
||||||
// Jika hamaId tersedia, fetch data terbaru dari API
|
// 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 penyakitId tersedia, fetch data terbaru dari API
|
||||||
if (widget.penyakitId != null) {
|
if (widget.penyakitId != null) {
|
||||||
_detailPenyakitFuture = _fetchDetailPenyakit(widget.penyakitId!);
|
_detailPenyakitFuture = _fetchDetailPenyakit(widget.penyakitId!);
|
||||||
} else {
|
} 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 {
|
Future<Map<String, dynamic>> _fetchDetailPenyakit(int id) async {
|
||||||
try {
|
try {
|
||||||
final detailData = await ApiService().getPenyakitById(id);
|
final detailData = await ApiService().getPenyakitById(id);
|
||||||
|
@ -67,10 +85,37 @@ class _DetailPenyakitPageState extends State<DetailPenyakitPage> {
|
||||||
future: ApiService().getPenyakitImageBytesByFilename(filename),
|
future: ApiService().getPenyakitImageBytesByFilename(filename),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return SizedBox(
|
return Container(
|
||||||
height: 200,
|
height: 280,
|
||||||
width: double.infinity,
|
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) {
|
} else if (snapshot.hasError || snapshot.data == null) {
|
||||||
return _buildPlaceholderImage(
|
return _buildPlaceholderImage(
|
||||||
|
@ -78,13 +123,29 @@ class _DetailPenyakitPageState extends State<DetailPenyakitPage> {
|
||||||
Icons.broken_image,
|
Icons.broken_image,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return ClipRRect(
|
return Hero(
|
||||||
borderRadius: BorderRadius.circular(12),
|
tag: 'penyakit_image_${widget.penyakitId}',
|
||||||
child: Image.memory(
|
child: Container(
|
||||||
snapshot.data!,
|
height: 280,
|
||||||
height: 200,
|
|
||||||
width: double.infinity,
|
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 untuk placeholder gambar
|
||||||
Widget _buildPlaceholderImage(String message, IconData icon) {
|
Widget _buildPlaceholderImage(String message, IconData icon) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 200,
|
height: 280,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[200],
|
borderRadius: BorderRadius.circular(20),
|
||||||
borderRadius: BorderRadius.circular(12),
|
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(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 64, color: Colors.grey[600]),
|
Container(
|
||||||
SizedBox(height: 8),
|
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(
|
Text(
|
||||||
message,
|
message,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[700],
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Widget _buildInfoCard({
|
||||||
Widget build(BuildContext context) {
|
required String title,
|
||||||
return Scaffold(
|
required String content,
|
||||||
backgroundColor: Color(0xFF9DC08D),
|
required IconData icon,
|
||||||
appBar: AppBar(
|
required Color color,
|
||||||
backgroundColor: Color(0xFF9DC08D),
|
}) {
|
||||||
title: Text("Detail Penyakit", style: TextStyle(color: Colors.white)),
|
return Container(
|
||||||
leading: IconButton(
|
margin: EdgeInsets.only(bottom: 20),
|
||||||
icon: Icon(Icons.arrow_back, color: Colors.white),
|
decoration: BoxDecoration(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
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(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Tampilkan foto dari database dengan penanganan error yang lebih baik
|
Row(
|
||||||
_buildImageWidget(detailData["foto"]),
|
children: [
|
||||||
SizedBox(height: 16),
|
Container(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
// Card Nama Hama
|
decoration: BoxDecoration(
|
||||||
SizedBox(
|
color: color.withOpacity(0.1),
|
||||||
width: double.infinity,
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Card(
|
),
|
||||||
elevation: 6,
|
child: Icon(
|
||||||
shape: RoundedRectangleBorder(
|
icon,
|
||||||
borderRadius: BorderRadius.circular(12),
|
color: color,
|
||||||
),
|
size: 24,
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: 16),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
// Card Deskripsi + Penanganan
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Card(
|
padding: EdgeInsets.all(16),
|
||||||
elevation: 6,
|
decoration: BoxDecoration(
|
||||||
shape: RoundedRectangleBorder(
|
color: Colors.grey[50],
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
border: Border.all(color: Colors.grey[200]!),
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.all(16.0),
|
child: Text(
|
||||||
child: Column(
|
content,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
style: TextStyle(
|
||||||
children: [
|
fontSize: 16,
|
||||||
Text(
|
height: 1.6,
|
||||||
"Deskripsi:",
|
color: Colors.grey[700],
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
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> {
|
class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
||||||
final TextEditingController emailController = TextEditingController();
|
final TextEditingController emailController = TextEditingController();
|
||||||
final TextEditingController codeController = TextEditingController();
|
|
||||||
final TextEditingController passwordController = TextEditingController();
|
|
||||||
final ApiService apiService = ApiService();
|
final ApiService apiService = ApiService();
|
||||||
|
|
||||||
bool isLoading = false;
|
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
|
// Fungsi untuk mengirim kode verifikasi
|
||||||
void handleSendCode() async {
|
void handleSendCode() async {
|
||||||
|
if (emailController.text.trim().isEmpty) {
|
||||||
|
_showErrorDialog('Email harus diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => isLoading = true);
|
setState(() => isLoading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiService.sendResetCode(email: emailController.text.trim());
|
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);
|
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
|
// Dialog untuk input kode verifikasi dan reset password
|
||||||
void showVerificationDialog() {
|
void showResetPasswordDialog() {
|
||||||
|
final codeController = TextEditingController();
|
||||||
|
final passwordController = TextEditingController();
|
||||||
|
final confirmPasswordController = TextEditingController();
|
||||||
|
bool isDialogLoading = false;
|
||||||
|
bool obscurePassword = true;
|
||||||
|
bool obscureConfirmPassword = true;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => StatefulBuilder(
|
||||||
title: Text('Masukkan Kode Verifikasi'),
|
builder: (context, setDialogState) => AlertDialog(
|
||||||
content: Column(
|
title: Text('Reset Password'),
|
||||||
mainAxisSize: MainAxisSize.min,
|
content: SingleChildScrollView(
|
||||||
children: [
|
child: Column(
|
||||||
Text('Kode verifikasi telah dikirim ke email Anda.'),
|
mainAxisSize: MainAxisSize.min,
|
||||||
SizedBox(height: 16),
|
children: [
|
||||||
TextField(
|
Text('Masukkan kode verifikasi dan password baru Anda.'),
|
||||||
controller: codeController,
|
SizedBox(height: 16),
|
||||||
decoration: InputDecoration(
|
TextField(
|
||||||
labelText: 'Kode Verifikasi',
|
controller: codeController,
|
||||||
border: OutlineInputBorder(),
|
decoration: InputDecoration(
|
||||||
),
|
labelText: 'Kode Verifikasi',
|
||||||
keyboardType: TextInputType.number,
|
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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text('OK'),
|
child: Text(
|
||||||
|
'Batal',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(); // tutup dialog
|
Navigator.pop(context);
|
||||||
Navigator.of(context).pop(); // kembali ke halaman login
|
},
|
||||||
|
),
|
||||||
|
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())),
|
|
||||||
);
|
@override
|
||||||
} finally {
|
void dispose() {
|
||||||
setState(() => isLoading = false);
|
emailController.dispose();
|
||||||
}
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -143,6 +327,7 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Color(0xFF9DC08D),
|
backgroundColor: Color(0xFF9DC08D),
|
||||||
title: Text('Lupa Password'),
|
title: Text('Lupa Password'),
|
||||||
|
elevation: 0,
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
@ -168,9 +353,19 @@ class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
||||||
controller: emailController,
|
controller: emailController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Email',
|
labelText: 'Email',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.black.withOpacity(0.6),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
),
|
),
|
||||||
|
|
|
@ -46,7 +46,11 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
|
|
||||||
// Ambiguity information from backend
|
// Ambiguity information from backend
|
||||||
final bool isAmbiguous = data['is_ambiguous'] ?? false;
|
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)
|
// Get the first penyakit and hama (if any)
|
||||||
Map<String, dynamic>? firstPenyakit =
|
Map<String, dynamic>? firstPenyakit =
|
||||||
|
@ -88,81 +92,197 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
),
|
),
|
||||||
body: Container(
|
body: Container(
|
||||||
color: Color(0xFFEDF1D6),
|
color: Color(0xFFEDF1D6),
|
||||||
child: isLoading
|
child:
|
||||||
? Center(child: CircularProgressIndicator())
|
isLoading
|
||||||
: SingleChildScrollView(
|
? Center(child: CircularProgressIndicator())
|
||||||
padding: EdgeInsets.all(16),
|
: SingleChildScrollView(
|
||||||
child: Column(
|
padding: EdgeInsets.all(16),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// Ambiguity notification (if applicable)
|
children: [
|
||||||
if (isAmbiguous && ambiguityResolution != null)
|
// Filter notification (if applicable)
|
||||||
_buildAmbiguityNotification(ambiguityResolution),
|
if (filterInfo != null)
|
||||||
|
_buildFilterNotification(filterInfo),
|
||||||
|
|
||||||
// Main result display - use hasil_tertinggi from backend
|
// Ambiguity notification (if applicable)
|
||||||
_buildDetailedResultFromBackend(context, hasilTertinggi),
|
if (isAmbiguous && ambiguityResolution != null)
|
||||||
|
_buildAmbiguityNotification(ambiguityResolution),
|
||||||
|
|
||||||
SizedBox(height: 24),
|
// Main result display - use hasil_tertinggi from backend
|
||||||
|
_buildDetailedResultFromBackend(context, hasilTertinggi),
|
||||||
|
|
||||||
// Selected symptoms section
|
SizedBox(height: 24),
|
||||||
_buildSection(
|
|
||||||
context,
|
// Selected symptoms section
|
||||||
'Gejala yang Dipilih',
|
_buildSection(
|
||||||
widget.gejalaTerpilih.isEmpty
|
context,
|
||||||
? _buildEmptyResult('Tidak ada gejala yang dipilih')
|
'Gejala yang Dipilih',
|
||||||
: Card(
|
widget.gejalaTerpilih.isEmpty
|
||||||
|
? _buildEmptyResult('Tidak ada gejala yang dipilih')
|
||||||
|
: Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: widget.gejalaTerpilih
|
children:
|
||||||
.map(
|
widget.gejalaTerpilih
|
||||||
(gejala) => Padding(
|
.map(
|
||||||
padding: EdgeInsets.symmetric(
|
(gejala) => Padding(
|
||||||
vertical: 4,
|
padding: EdgeInsets.symmetric(
|
||||||
),
|
vertical: 4,
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
color: Colors.green,
|
|
||||||
size: 18,
|
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
child: Row(
|
||||||
Expanded(child: Text(gejala)),
|
crossAxisAlignment:
|
||||||
],
|
CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
),
|
Icon(
|
||||||
)
|
Icons.check_circle,
|
||||||
.toList(),
|
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(
|
SizedBox(height: 12),
|
||||||
context,
|
Text(description, style: TextStyle(fontSize: 14)),
|
||||||
'Kemungkinan Penyakit Lainnya',
|
if (fallbackReason != null) ...[
|
||||||
_buildOtherPossibilities(penyakitList, hasilTertinggi, 'penyakit'),
|
SizedBox(height: 8),
|
||||||
),
|
Container(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
SizedBox(height: 24),
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade100,
|
||||||
// Other possible pests section
|
borderRadius: BorderRadius.circular(8),
|
||||||
_buildSection(
|
),
|
||||||
context,
|
child: Text(
|
||||||
'Kemungkinan Hama Lainnya',
|
'Alasan: $fallbackReason',
|
||||||
_buildOtherPossibilities(hamaList, hasilTertinggi, 'hama'),
|
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: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.info_outline, color: Colors.blue.shade700, size: 24),
|
||||||
Icons.info_outline,
|
|
||||||
color: Colors.blue.shade700,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -250,10 +366,12 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
String type = '';
|
String type = '';
|
||||||
bool isPenyakit = false;
|
bool isPenyakit = false;
|
||||||
|
|
||||||
if (hasilTertinggi.containsKey('id_penyakit') && hasilTertinggi['id_penyakit'] != null) {
|
if (hasilTertinggi.containsKey('id_penyakit') &&
|
||||||
|
hasilTertinggi['id_penyakit'] != null) {
|
||||||
type = 'penyakit';
|
type = 'penyakit';
|
||||||
isPenyakit = true;
|
isPenyakit = true;
|
||||||
} else if (hasilTertinggi.containsKey('id_hama') && hasilTertinggi['id_hama'] != null) {
|
} else if (hasilTertinggi.containsKey('id_hama') &&
|
||||||
|
hasilTertinggi['id_hama'] != null) {
|
||||||
type = 'hama';
|
type = 'hama';
|
||||||
isPenyakit = false;
|
isPenyakit = false;
|
||||||
} else {
|
} else {
|
||||||
|
@ -266,7 +384,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
final completeData = _getCompleteItemData(hasilTertinggi, type);
|
final completeData = _getCompleteItemData(hasilTertinggi, type);
|
||||||
|
|
||||||
// Extract the data we need with safe access
|
// 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 deskripsi = completeData['deskripsi'] ?? 'Tidak tersedia';
|
||||||
final penanganan = completeData['penanganan'] ?? 'Tidak tersedia';
|
final penanganan = completeData['penanganan'] ?? 'Tidak tersedia';
|
||||||
final foto = completeData['foto'];
|
final foto = completeData['foto'];
|
||||||
|
@ -277,10 +396,15 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
final totalGejalaEntity = hasilTertinggi['total_gejala_entity'];
|
final totalGejalaEntity = hasilTertinggi['total_gejala_entity'];
|
||||||
final persentaseKesesuaian = hasilTertinggi['persentase_kesesuaian'];
|
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
|
// Debug log
|
||||||
print('DEBUG - Building detailed result for: $nama');
|
print('DEBUG - Building detailed result for: $nama');
|
||||||
print('DEBUG - Type: $type, isPenyakit: $isPenyakit');
|
print('DEBUG - Type: $type, isPenyakit: $isPenyakit');
|
||||||
print('DEBUG - Probabilitas: $probabilitas');
|
print('DEBUG - Probabilitas: $probabilitas');
|
||||||
|
print('DEBUG - Is perfect single match: $isPerfectSingleMatch');
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 6,
|
elevation: 6,
|
||||||
|
@ -300,7 +424,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
isPenyakit ? Icons.coronavirus_outlined : Icons.bug_report,
|
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,
|
size: 28,
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
|
@ -310,7 +435,10 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isPenyakit ? Colors.red.shade700 : Colors.orange.shade700,
|
color:
|
||||||
|
isPenyakit
|
||||||
|
? Colors.red.shade700
|
||||||
|
: Colors.orange.shade700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -318,6 +446,38 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 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
|
// Additional info if ambiguity resolution occurred
|
||||||
if (jumlahGejalacocok != null && totalGejalaEntity != null)
|
if (jumlahGejalacocok != null && totalGejalaEntity != null)
|
||||||
Container(
|
Container(
|
||||||
|
@ -329,7 +489,11 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.analytics_outlined, size: 16, color: Colors.grey.shade600),
|
Icon(
|
||||||
|
Icons.analytics_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
SizedBox(width: 6),
|
SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'Kesesuaian: $jumlahGejalacocok/$totalGejalaEntity gejala',
|
'Kesesuaian: $jumlahGejalacocok/$totalGejalaEntity gejala',
|
||||||
|
@ -431,53 +595,76 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOtherPossibilities(
|
Widget _buildOtherPossibilities(
|
||||||
List<dynamic> itemList,
|
List<dynamic> itemList,
|
||||||
Map<String, dynamic>? hasilTertinggi,
|
Map<String, dynamic>? hasilTertinggi,
|
||||||
String type,
|
String type,
|
||||||
) {
|
) {
|
||||||
if (itemList.isEmpty) {
|
// Check if there's a 100% match in hasilTertinggi
|
||||||
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
|
if (hasilTertinggi != null) {
|
||||||
|
double probabilitas = _getProbabilitas(hasilTertinggi);
|
||||||
|
if ((probabilitas * 100).round() == 100) {
|
||||||
|
return _buildEmptyResult(
|
||||||
|
'Ditemukan kecocokan 100% pada diagnosa utama',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 that's already shown
|
// Filter out the top result AND items with 100% probability
|
||||||
List<dynamic> otherItems = [];
|
otherItems = itemList.where((item) {
|
||||||
|
String? itemId;
|
||||||
if (hasilTertinggi != null) {
|
if (type == 'penyakit') {
|
||||||
// Get the ID of the top result
|
itemId = item['id_penyakit']?.toString();
|
||||||
String? topResultId;
|
} else {
|
||||||
if (type == 'penyakit' && hasilTertinggi.containsKey('id_penyakit')) {
|
itemId = item['id_hama']?.toString();
|
||||||
topResultId = hasilTertinggi['id_penyakit']?.toString();
|
|
||||||
} else if (type == 'hama' && hasilTertinggi.containsKey('id_hama')) {
|
|
||||||
topResultId = hasilTertinggi['id_hama']?.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out the top result
|
// Skip if this is the top result
|
||||||
otherItems = itemList.where((item) {
|
if (topResultId != null && itemId == topResultId) {
|
||||||
String? itemId;
|
return false;
|
||||||
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) {
|
// Skip if this item has 100% probability
|
||||||
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
|
double itemProbabilitas = _getProbabilitas(item);
|
||||||
}
|
if ((itemProbabilitas * 100).round() == 100) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return true;
|
||||||
children: otherItems
|
}).toList();
|
||||||
.map((item) => _buildItemCard(item, type))
|
} else {
|
||||||
.toList(),
|
// 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 {
|
Future<void> _fetchAdditionalData() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
@ -518,7 +705,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
if (detail.isNotEmpty) {
|
if (detail.isNotEmpty) {
|
||||||
double probability = 0.0;
|
double probability = 0.0;
|
||||||
if (penyakit.containsKey('probabilitas_persen')) {
|
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')) {
|
} else if (penyakit.containsKey('nilai_bayes')) {
|
||||||
probability = (penyakit['nilai_bayes'] as num).toDouble();
|
probability = (penyakit['nilai_bayes'] as num).toDouble();
|
||||||
}
|
}
|
||||||
|
@ -529,7 +717,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
'id_penyakit': penyakitIdStr,
|
'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');
|
print('DEBUG - Found details for penyakit ID $penyakitIdStr: $nama');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -561,7 +750,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
'id_hama': hamaIdStr,
|
'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');
|
print('DEBUG - Found details for hama ID $hamaIdStr: $nama');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -597,7 +787,9 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
details = penyakitDetails[idStr];
|
details = penyakitDetails[idStr];
|
||||||
|
|
||||||
if (details == null || details.isEmpty) {
|
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(
|
details = semuaPenyakit.firstWhere(
|
||||||
(p) => p['id'].toString() == idStr,
|
(p) => p['id'].toString() == idStr,
|
||||||
orElse: () => <String, dynamic>{},
|
orElse: () => <String, dynamic>{},
|
||||||
|
@ -611,7 +803,9 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
details = hamaDetails[idStr];
|
details = hamaDetails[idStr];
|
||||||
|
|
||||||
if (details == null || details.isEmpty) {
|
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(
|
details = semuaHama.firstWhere(
|
||||||
(h) => h['id'].toString() == idStr,
|
(h) => h['id'].toString() == idStr,
|
||||||
orElse: () => <String, dynamic>{},
|
orElse: () => <String, dynamic>{},
|
||||||
|
@ -644,7 +838,9 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
type == 'penyakit' ? 'id_penyakit' : 'id_hama': idStr,
|
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 {
|
} else {
|
||||||
print('DEBUG - No details found for $type ID $idStr');
|
print('DEBUG - No details found for $type ID $idStr');
|
||||||
}
|
}
|
||||||
|
@ -660,28 +856,59 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
|
||||||
// Get additional info for display
|
// Get additional info for display
|
||||||
final jumlahGejalacocok = item['jumlah_gejala_cocok'];
|
final jumlahGejalacocok = item['jumlah_gejala_cocok'];
|
||||||
final totalGejalaEntity = item['total_gejala_entity'];
|
final totalGejalaEntity = item['total_gejala_entity'];
|
||||||
|
final persentaseKesesuaian = item['persentase_kesesuaian'];
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.only(bottom: 8),
|
margin: EdgeInsets.only(bottom: 8),
|
||||||
|
elevation: 2,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
type == 'penyakit' ? Icons.coronavirus_outlined : Icons.bug_report,
|
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),
|
title: Text(
|
||||||
subtitle: jumlahGejalacocok != null && totalGejalaEntity != null
|
nama,
|
||||||
? Text(
|
style: TextStyle(
|
||||||
'Kesesuaian: $jumlahGejalacocok/$totalGejalaEntity gejala',
|
fontWeight: FontWeight.w500,
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
color: Color(0xFF40513B),
|
||||||
)
|
),
|
||||||
: null,
|
),
|
||||||
trailing: Row(
|
subtitle: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildProbabilityIndicator(probabilitas),
|
if (jumlahGejalacocok != null && totalGejalaEntity != null) ...[
|
||||||
SizedBox(width: 8),
|
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) {
|
Widget _buildProbabilityIndicator(double value) {
|
||||||
final Color indicatorColor = value > 0.7
|
final Color indicatorColor =
|
||||||
? Colors.red
|
value > 0.7
|
||||||
: value > 0.4
|
? Colors.red
|
||||||
|
: value > 0.4
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: Colors.green;
|
: Colors.green;
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,34 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
final TextEditingController _emailController = TextEditingController();
|
final TextEditingController _emailController = TextEditingController();
|
||||||
final TextEditingController _passwordController = TextEditingController();
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
bool _isLoading = false;
|
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 {
|
Future<void> _login() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -27,12 +54,10 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
String password = _passwordController.text.trim();
|
String password = _passwordController.text.trim();
|
||||||
|
|
||||||
if (email.isEmpty || password.isEmpty) {
|
if (email.isEmpty || password.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text("Email dan Password harus diisi")),
|
|
||||||
);
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
_showErrorDialog('Email dan Password harus diisi');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,10 +66,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
var response = await http.post(
|
var response = await http.post(
|
||||||
url,
|
url,
|
||||||
headers: {"Content-Type": "application/json"},
|
headers: {"Content-Type": "application/json"},
|
||||||
body: jsonEncode({
|
body: jsonEncode({"email": email, "password": password}),
|
||||||
"email": email,
|
|
||||||
"password": password,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var responseData = jsonDecode(response.body);
|
var responseData = jsonDecode(response.body);
|
||||||
|
@ -71,19 +93,19 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(content: Text(responseData['message'] ?? "Login gagal")),
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_showErrorDialog(
|
||||||
|
responseData['message'] ?? 'Email atau Password salah',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(content: Text("Terjadi kesalahan: $e")),
|
_isLoading = false;
|
||||||
);
|
});
|
||||||
|
_showErrorDialog('Terjadi kesalahan koneksi. Silakan coba lagi.');
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -109,10 +131,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
Image.asset('assets/images/bayam.png', height: 150),
|
||||||
'assets/images/bayam.png',
|
|
||||||
height: 150,
|
|
||||||
),
|
|
||||||
SizedBox(height: 30),
|
SizedBox(height: 30),
|
||||||
Card(
|
Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
@ -128,20 +147,54 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'email',
|
labelText: 'email',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.black.withOpacity(0.6),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: true,
|
obscureText:
|
||||||
|
!_isPasswordVisible, // Toggle visibility based on state
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Password',
|
labelText: 'Password',
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
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),
|
SizedBox(height: 20),
|
||||||
|
@ -156,18 +209,19 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: _isLoading ? null : _login,
|
onPressed: _isLoading ? null : _login,
|
||||||
child: _isLoading
|
child:
|
||||||
? CircularProgressIndicator(
|
_isLoading
|
||||||
color: Colors.white,
|
? CircularProgressIndicator(
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
'Login',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
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),
|
SizedBox(height: 20),
|
||||||
Row(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
TextButton(
|
||||||
onTap: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
@ -187,17 +241,39 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
style: TextButton.styleFrom(
|
||||||
'Belum punya akun? Daftar',
|
backgroundColor: Colors.white,
|
||||||
style: TextStyle(
|
padding: EdgeInsets.symmetric(
|
||||||
color: Colors.white,
|
horizontal: 16,
|
||||||
decoration: TextDecoration.underline,
|
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),
|
SizedBox(height: 10),
|
||||||
GestureDetector(
|
TextButton(
|
||||||
onTap: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
@ -205,12 +281,34 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
style: TextButton.styleFrom(
|
||||||
'Lupa Password?',
|
backgroundColor: Colors.white,
|
||||||
style: TextStyle(
|
padding: EdgeInsets.symmetric(
|
||||||
color: Colors.white,
|
horizontal: 16,
|
||||||
decoration: TextDecoration.underline,
|
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,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
(context) => AlertDialog(
|
(context) => AlertDialog(
|
||||||
title: Text('Update Profil'),
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.edit, color: Color(0xFF9DC08D)),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Update Profil'),
|
||||||
|
],
|
||||||
|
),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
|
@ -151,16 +157,40 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nameController,
|
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:
|
validator:
|
||||||
(value) =>
|
(value) =>
|
||||||
value?.isEmpty ?? true
|
value?.isEmpty ?? true
|
||||||
? 'Nama tidak boleh kosong'
|
? 'Nama tidak boleh kosong'
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
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) {
|
validator: (value) {
|
||||||
if (value?.isEmpty ?? true)
|
if (value?.isEmpty ?? true)
|
||||||
return 'Email tidak boleh kosong';
|
return 'Email tidak boleh kosong';
|
||||||
|
@ -168,12 +198,21 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Password Baru',
|
labelText: 'Password Baru',
|
||||||
helperText:
|
helperText:
|
||||||
'Kosongkan jika tidak ingin mengubah password',
|
'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,
|
obscureText: true,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
|
@ -184,18 +223,43 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _alamatController,
|
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:
|
validator:
|
||||||
(value) =>
|
(value) =>
|
||||||
value?.isEmpty ?? true
|
value?.isEmpty ?? true
|
||||||
? 'Alamat tidak boleh kosong'
|
? 'Alamat tidak boleh kosong'
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nomorTeleponController,
|
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,
|
keyboardType: TextInputType.phone,
|
||||||
validator:
|
validator:
|
||||||
(value) =>
|
(value) =>
|
||||||
|
@ -208,11 +272,12 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton.icon(
|
||||||
onPressed: () => Navigator.pop(context),
|
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 {
|
onPressed: () async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
try {
|
try {
|
||||||
|
@ -234,15 +299,29 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
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,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Row(
|
||||||
'Gagal memperbarui profil: ${e.toString()}',
|
children: [
|
||||||
|
Icon(Icons.error, color: Colors.white),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Gagal memperbarui profil: ${e.toString()}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.red,
|
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(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Color(0xFF9DC08D),
|
backgroundColor: Color(0xFF9DC08D),
|
||||||
),
|
),
|
||||||
child: Text('Update'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -266,21 +346,38 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
backgroundColor: Color(0xFF9DC08D),
|
backgroundColor: Color(0xFF9DC08D),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
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(
|
Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 40.0),
|
padding: const EdgeInsets.only(top: 40.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
"Profil Pengguna",
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
style: TextStyle(
|
children: [
|
||||||
fontSize: 24,
|
Icon(Icons.account_circle, color: Colors.white, size: 32),
|
||||||
fontWeight: FontWeight.bold,
|
SizedBox(width: 12),
|
||||||
color: Colors.white,
|
Text(
|
||||||
),
|
"Profil Pengguna",
|
||||||
textAlign: TextAlign.center,
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 80),
|
SizedBox(height: 80),
|
||||||
],
|
],
|
||||||
|
@ -292,32 +389,38 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 40.0,
|
top: 40.0,
|
||||||
left: 16.0,
|
left: 16.0,
|
||||||
child: IconButton(
|
child: Container(
|
||||||
icon: Icon(Icons.arrow_back, color: Colors.white, size: 28),
|
decoration: BoxDecoration(
|
||||||
onPressed: () {
|
color: Colors.white.withOpacity(0.2),
|
||||||
Navigator.pop(context);
|
borderRadius: BorderRadius.circular(25),
|
||||||
},
|
),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back, color: Colors.white, size: 28),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Isi halaman
|
// Isi halaman
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 36.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Card box untuk data pengguna
|
// Card box untuk data pengguna
|
||||||
Container(
|
Container(
|
||||||
height: 200,
|
|
||||||
width: 450,
|
width: 450,
|
||||||
child: Card(
|
child: Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
elevation: 4,
|
elevation: 8,
|
||||||
|
shadowColor: Colors.black.withOpacity(0.2),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child:
|
child:
|
||||||
isLoading
|
isLoading
|
||||||
? _buildLoadingState()
|
? _buildLoadingState()
|
||||||
|
@ -329,51 +432,72 @@ class _ProfilPageState extends State<ProfilPage> {
|
||||||
),
|
),
|
||||||
SizedBox(height: 30),
|
SizedBox(height: 30),
|
||||||
|
|
||||||
// Button untuk update data profil
|
// Buttons row
|
||||||
ElevatedButton(
|
Row(
|
||||||
onPressed: _showUpdateProfileDialog,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
style: ElevatedButton.styleFrom(
|
children: [
|
||||||
shape: RoundedRectangleBorder(
|
// Button untuk update data profil
|
||||||
borderRadius: BorderRadius.circular(8),
|
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,
|
SizedBox(width: 12),
|
||||||
padding: EdgeInsets.symmetric(
|
// Button untuk logout
|
||||||
horizontal: 32,
|
Expanded(
|
||||||
vertical: 12,
|
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: [
|
children: [
|
||||||
CircularProgressIndicator(color: Color(0xFF9DC08D)),
|
CircularProgressIndicator(color: Color(0xFF9DC08D)),
|
||||||
SizedBox(height: 16),
|
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),
|
style: TextStyle(color: Colors.red),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildProfileItem("Nama: ${userData?['name'] ?? '-'}"),
|
// Header card
|
||||||
Divider(color: Colors.black),
|
Row(
|
||||||
_buildProfileItem("Email: ${userData?['email'] ?? '-'}"),
|
children: [
|
||||||
Divider(color: Colors.black),
|
Icon(Icons.info_outline, color: Color(0xFF9DC08D), size: 24),
|
||||||
_buildProfileItem("Alamat: ${userData?['alamat'] ?? '-'}"),
|
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
|
// Fungsi untuk membuat item dalam Card box dengan icon
|
||||||
Widget _buildProfileItem(String text) {
|
Widget _buildProfileItem(IconData icon, String label, String value) {
|
||||||
return Padding(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
padding: EdgeInsets.all(12),
|
||||||
child: Text(text, style: TextStyle(fontSize: 18)),
|
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';
|
import 'package:frontend/api_services/api_services.dart';
|
||||||
|
|
||||||
// Halaman Pendaftaran
|
// 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 nameController = TextEditingController();
|
||||||
final TextEditingController emailController = TextEditingController();
|
final TextEditingController emailController = TextEditingController();
|
||||||
final TextEditingController passwordController = TextEditingController();
|
final TextEditingController passwordController = TextEditingController();
|
||||||
final TextEditingController alamatController = TextEditingController();
|
final TextEditingController alamatController = TextEditingController();
|
||||||
final TextEditingController nomorHpController = TextEditingController();
|
final TextEditingController nomorHpController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
final ApiService apiService = ApiService();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -35,9 +133,14 @@ class RegisterPage extends StatelessWidget {
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Nama',
|
labelText: 'Nama',
|
||||||
|
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
),
|
),
|
||||||
|
@ -46,9 +149,14 @@ class RegisterPage extends StatelessWidget {
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Password',
|
labelText: 'Password',
|
||||||
|
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
),
|
),
|
||||||
|
@ -56,9 +164,14 @@ class RegisterPage extends StatelessWidget {
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Email',
|
labelText: 'Email',
|
||||||
|
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
controller: emailController,
|
controller: emailController,
|
||||||
),
|
),
|
||||||
|
@ -66,22 +179,32 @@ class RegisterPage extends StatelessWidget {
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Alamat',
|
labelText: 'Alamat',
|
||||||
|
labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
controller: alamatController,
|
controller: alamatController,
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
TextField(
|
// TextField(
|
||||||
decoration: InputDecoration(
|
// decoration: InputDecoration(
|
||||||
labelText: 'Nomor HP',
|
// labelText: 'Nomor HP',
|
||||||
border: OutlineInputBorder(
|
// labelStyle: TextStyle(color: Colors.black.withOpacity(0.7)),
|
||||||
borderRadius: BorderRadius.circular(10),
|
// border: OutlineInputBorder(
|
||||||
),
|
// borderRadius: BorderRadius.circular(10),
|
||||||
),
|
// ),
|
||||||
controller: nomorHpController,
|
// focusedBorder: OutlineInputBorder(
|
||||||
),
|
// borderRadius: BorderRadius.circular(10),
|
||||||
|
// borderSide: BorderSide(color: Colors.black.withOpacity(0.6), width: 2),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// controller: nomorHpController,
|
||||||
|
// ),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
@ -93,33 +216,17 @@ class RegisterPage extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: _isLoading ? null : _handleRegister,
|
||||||
try {
|
child: _isLoading
|
||||||
await apiService.registerUser(
|
? CircularProgressIndicator(color: Colors.white)
|
||||||
name: nameController.text,
|
: Text(
|
||||||
email: emailController.text,
|
'Daftar',
|
||||||
password: passwordController.text,
|
style: TextStyle(
|
||||||
alamat: alamatController.text,
|
color: Colors.white,
|
||||||
nomorTelepon: nomorHpController.text,
|
fontSize: 16,
|
||||||
);
|
fontWeight: FontWeight.bold,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in New Issue