pembaruan config dll

This commit is contained in:
unknown 2025-06-05 00:38:06 +07:00
parent 89d9827387
commit 27882ce9e6
18 changed files with 1993 additions and 724 deletions

5
.gitignore vendored
View File

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

25
backend/config/config.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(
"Tambah Rule Hama",
style: TextStyle(fontSize: 12)),
style: ElevatedButton.styleFrom( 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(
"Tambah Rule Penyakit",
style: TextStyle(fontSize: 12),),
style: ElevatedButton.styleFrom( 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
), ),
), ),
], ],

View File

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

View File

@ -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: Container(
height: 280,
width: double.infinity,
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( child: Image.memory(
snapshot.data!, snapshot.data!,
height: 200, fit: BoxFit.cover,
width: double.infinity, ),
fit: BoxFit.contain, // untuk memastikan proporsional & penuh ),
), ),
); );
} }
@ -95,151 +156,233 @@ 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.w600,
fontSize: 16,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildInfoCard({
required String title,
required String content,
required IconData icon,
required Color color,
}) {
return Container(
margin: EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.2),
blurRadius: 15,
offset: Offset(0, 8),
spreadRadius: 2,
),
],
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
SizedBox(width: 16),
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey[800],
), ),
), ),
], ],
), ),
SizedBox(height: 16),
Container(
width: double.infinity,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Text(
content,
style: TextStyle(
fontSize: 16,
height: 1.6,
color: Colors.grey[700],
),
textAlign: TextAlign.justify,
),
),
],
),
),
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFFF8F9FA),
appBar: AppBar( appBar: AppBar(
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFF9DC08D),
title: Text("Detail Hama", style: TextStyle(color: Colors.white)), elevation: 0,
leading: IconButton( leading: Container(
icon: Icon(Icons.arrow_back, color: Colors.white), 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(), onPressed: () => Navigator.of(context).pop(),
), ),
), ),
body: FutureBuilder<Map<String, dynamic>>( 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, future: _detailHamaFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return Center( return Container(
child: CircularProgressIndicator(color: Colors.white), 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) { if (snapshot.hasError) {
print('Error: ${snapshot.error}'); print('Error: ${snapshot.error}');
// Tampilkan data yang sudah ada jika terjadi error
return _buildDetailContent(_currentDetailHama); 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; final detailData = snapshot.data ?? _currentDetailHama;
return _buildDetailContent(detailData); return _buildDetailContent(detailData);
}, },
), ),
),
); );
} }
Widget _buildDetailContent(Map<String, dynamic> detailData) { Widget _buildDetailContent(Map<String, dynamic> detailData) {
return SingleChildScrollView( return FadeTransition(
opacity: _fadeAnimation,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Tampilkan foto dari database dengan penanganan error yang lebih baik // Hero Image Section
_buildImageWidget(detailData["foto"]), _buildImageWidget(detailData["foto"]),
SizedBox(height: 16), SizedBox(height: 24),
// Card Nama Hama // Nama Hama Card
SizedBox( _buildInfoCard(
width: double.infinity, title: "Nama Hama",
child: Card( content: detailData["nama"] ?? "Nama hama tidak tersedia",
elevation: 6, icon: Icons.bug_report,
shape: RoundedRectangleBorder( color: Color(0xFF9DC08D),
borderRadius: BorderRadius.circular(12),
), ),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Nama Hama:",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
detailData["nama"] ?? "Nama hama tidak tersedia",
style: TextStyle(fontSize: 16),
),
],
),
),
),
),
SizedBox(height: 16),
// Card Deskripsi + Penanganan // Deskripsi Card
SizedBox( _buildInfoCard(
width: double.infinity, title: "Deskripsi",
child: Card( content: detailData["deskripsi"] ?? "Deskripsi tidak tersedia",
elevation: 6, icon: Icons.description,
shape: RoundedRectangleBorder( color: Color(0xFF9DC08D),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Deskripsi:",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
detailData["deskripsi"] ?? "Deskripsi tidak tersedia",
style: TextStyle(fontSize: 16),
),
SizedBox(height: 16),
Text(
"Penanganan:",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
detailData["penanganan"] ?? "Penanganan tidak tersedia",
style: TextStyle(fontSize: 16),
),
],
),
),
), ),
// Penanganan Card
_buildInfoCard(
title: "Penanganan",
content: detailData["penanganan"] ?? "Penanganan tidak tersedia",
icon: Icons.medical_services,
color: Color(0xFF9DC08D),
), ),
// Bottom spacing
SizedBox(height: 20),
], ],
), ),
), ),

View File

@ -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: Container(
height: 280,
width: double.infinity,
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( child: Image.memory(
snapshot.data!, snapshot.data!,
height: 200, fit: BoxFit.cover,
width: double.infinity, ),
fit: BoxFit.contain, // untuk memastikan proporsional & penuh ),
), ),
); );
} }
@ -95,151 +156,233 @@ 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.w600,
fontSize: 16,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildInfoCard({
required String title,
required String content,
required IconData icon,
required Color color,
}) {
return Container(
margin: EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.2),
blurRadius: 15,
offset: Offset(0, 8),
spreadRadius: 2,
),
],
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
SizedBox(width: 16),
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey[800],
), ),
), ),
], ],
), ),
SizedBox(height: 16),
Container(
width: double.infinity,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Text(
content,
style: TextStyle(
fontSize: 16,
height: 1.6,
color: Colors.grey[700],
),
textAlign: TextAlign.justify,
),
),
],
),
),
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFFF8F9FA),
appBar: AppBar( appBar: AppBar(
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFF9DC08D),
title: Text("Detail Penyakit", style: TextStyle(color: Colors.white)), elevation: 0,
leading: IconButton( leading: Container(
icon: Icon(Icons.arrow_back, color: Colors.white), 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(), onPressed: () => Navigator.of(context).pop(),
), ),
), ),
body: FutureBuilder<Map<String, dynamic>>( 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, future: _detailPenyakitFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return Center( return Container(
child: CircularProgressIndicator(color: Colors.white), 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) { if (snapshot.hasError) {
print('Error: ${snapshot.error}'); print('Error: ${snapshot.error}');
// Tampilkan data yang sudah ada jika terjadi error
return _buildDetailContent(_currentDetailPenyakit); 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; final detailData = snapshot.data ?? _currentDetailPenyakit;
return _buildDetailContent(detailData); return _buildDetailContent(detailData);
}, },
), ),
),
); );
} }
Widget _buildDetailContent(Map<String, dynamic> detailData) { Widget _buildDetailContent(Map<String, dynamic> detailData) {
return SingleChildScrollView( return FadeTransition(
opacity: _fadeAnimation,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Tampilkan foto dari database dengan penanganan error yang lebih baik // Hero Image Section
_buildImageWidget(detailData["foto"]), _buildImageWidget(detailData["foto"]),
SizedBox(height: 16), SizedBox(height: 24),
// Card Nama Hama // Nama Penyakit Card
SizedBox( _buildInfoCard(
width: double.infinity, title: "Nama Penyakit",
child: Card( content: detailData["nama"] ?? "Nama penyakit tidak tersedia",
elevation: 6, icon: Icons.coronavirus,
shape: RoundedRectangleBorder( color: Color(0xFF9DC08D),
borderRadius: BorderRadius.circular(12),
), ),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Nama Penyakit:",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
detailData["nama"] ?? "Nama hama tidak tersedia",
style: TextStyle(fontSize: 16),
),
],
),
),
),
),
SizedBox(height: 16),
// Card Deskripsi + Penanganan // Deskripsi Card
SizedBox( _buildInfoCard(
width: double.infinity, title: "Deskripsi",
child: Card( content: detailData["deskripsi"] ?? "Deskripsi tidak tersedia",
elevation: 6, icon: Icons.description,
shape: RoundedRectangleBorder( color: Color(0xFF9DC08D),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Deskripsi:",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
detailData["deskripsi"] ?? "Deskripsi tidak tersedia",
style: TextStyle(fontSize: 16),
),
SizedBox(height: 16),
Text(
"Penanganan:",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
detailData["penanganan"] ?? "Penanganan tidak tersedia",
style: TextStyle(fontSize: 16),
),
],
),
),
), ),
// Penanganan Card
_buildInfoCard(
title: "Penanganan",
content: detailData["penanganan"] ?? "Penanganan tidak tersedia",
icon: Icons.medical_services,
color: Color(0xFF9DC08D),
), ),
// Bottom spacing
SizedBox(height: 20),
], ],
), ),
), ),

View File

@ -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'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('Kode verifikasi telah dikirim ke email Anda.'), Text('Masukkan kode verifikasi dan password baru Anda.'),
SizedBox(height: 16), SizedBox(height: 16),
TextField( TextField(
controller: codeController, controller: codeController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Kode Verifikasi', labelText: 'Kode Verifikasi',
border: OutlineInputBorder(), 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, keyboardType: TextInputType.number,
), ),
], SizedBox(height: 16),
),
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( TextField(
controller: passwordController, controller: passwordController,
obscureText: obscurePassword,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Password Baru', labelText: 'Password Baru',
border: OutlineInputBorder(), 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;
});
},
),
), ),
obscureText: true,
), ),
], ],
), ),
),
actions: [ actions: [
TextButton( TextButton(
child: Text('Reset'), child: Text(
onPressed: () => handleResetPassword(), 'Batal',
style: TextStyle(color: Colors.grey),
), ),
], onPressed: () {
Navigator.pop(context);
},
),
TextButton(
child: isDialogLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF9DC08D)),
),
)
: Text(
'Reset Password',
style: TextStyle(
color: Color(0xFF9DC08D),
fontWeight: FontWeight.bold,
),
),
onPressed: isDialogLoading ? null : () async {
// Validasi input
if (codeController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Silakan masukkan kode verifikasi.'),
backgroundColor: Colors.red,
),
);
return;
}
if (passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Silakan masukkan password baru.'),
backgroundColor: Colors.red,
),
);
return;
}
if (passwordController.text != confirmPasswordController.text) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Password tidak cocok.'),
backgroundColor: Colors.red,
),
);
return;
}
if (passwordController.text.length < 6) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Password minimal 6 karakter.'),
backgroundColor: Colors.red,
),
);
return;
}
setDialogState(() {
isDialogLoading = true;
});
try {
// Panggil API untuk reset password
await apiService.resetPasswordWithCode(
code: codeController.text.trim(),
password: passwordController.text,
);
setDialogState(() {
isDialogLoading = false;
});
// Tutup dialog dan tampilkan pesan sukses
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Password berhasil direset!'),
backgroundColor: Colors.green,
),
);
// Redirect ke login page
// Navigator.pushReplacementNamed(context, '/login');
} catch (e) {
setDialogState(() {
isDialogLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Terjadi kesalahan: ${e.toString()}'),
backgroundColor: Colors.red,
), ),
); );
} }
// Fungsi untuk reset password
void handleResetPassword() async {
setState(() => isLoading = true);
try {
await apiService.resetPasswordWithCode(
code: codeController.text.trim(),
password: passwordController.text.trim(),
);
Navigator.pop(context); // Tutup dialog password
// Tampilkan pesan sukses
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Berhasil'),
content: Text('Password berhasil direset.'),
actions: [
TextButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context).pop(); // tutup dialog
Navigator.of(context).pop(); // kembali ke halaman login
}, },
), ),
], ],
), ),
),
); );
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
} finally {
setState(() => isLoading = false);
} }
@override
void dispose() {
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,
), ),

View File

@ -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,13 +92,18 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
), ),
body: Container( body: Container(
color: Color(0xFFEDF1D6), color: Color(0xFFEDF1D6),
child: isLoading child:
isLoading
? Center(child: CircularProgressIndicator()) ? Center(child: CircularProgressIndicator())
: SingleChildScrollView( : SingleChildScrollView(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Filter notification (if applicable)
if (filterInfo != null)
_buildFilterNotification(filterInfo),
// Ambiguity notification (if applicable) // Ambiguity notification (if applicable)
if (isAmbiguous && ambiguityResolution != null) if (isAmbiguous && ambiguityResolution != null)
_buildAmbiguityNotification(ambiguityResolution), _buildAmbiguityNotification(ambiguityResolution),
@ -116,7 +125,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: widget.gejalaTerpilih children:
widget.gejalaTerpilih
.map( .map(
(gejala) => Padding( (gejala) => Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
@ -149,7 +159,11 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
_buildSection( _buildSection(
context, context,
'Kemungkinan Penyakit Lainnya', 'Kemungkinan Penyakit Lainnya',
_buildOtherPossibilities(penyakitList, hasilTertinggi, 'penyakit'), _buildOtherPossibilities(
penyakitList,
hasilTertinggi,
'penyakit',
),
), ),
SizedBox(height: 24), SizedBox(height: 24),
@ -158,7 +172,11 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
_buildSection( _buildSection(
context, context,
'Kemungkinan Hama Lainnya', 'Kemungkinan Hama Lainnya',
_buildOtherPossibilities(hamaList, hasilTertinggi, 'hama'), _buildOtherPossibilities(
hamaList,
hasilTertinggi,
'hama',
),
), ),
], ],
), ),
@ -167,6 +185,108 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
); );
} }
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: 12),
Text(description, style: TextStyle(fontSize: 14)),
if (fallbackReason != null) ...[
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Alasan: $fallbackReason',
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.orange.shade800,
),
),
),
] else if (!fallbackToSymptomCount) ...[
SizedBox(height: 8),
Row(
children: [
Text(
'Total hasil: $totalSebelum$totalSetelah',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.green.shade700,
),
),
],
),
],
],
),
),
);
}
Widget _buildAmbiguityNotification(Map<String, dynamic> ambiguityResolution) { Widget _buildAmbiguityNotification(Map<String, dynamic> ambiguityResolution) {
final totalKandidat = ambiguityResolution['total_kandidat'] ?? 0; final totalKandidat = ambiguityResolution['total_kandidat'] ?? 0;
final terpilih = ambiguityResolution['terpilih'] ?? {}; final terpilih = ambiguityResolution['terpilih'] ?? {};
@ -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',
@ -435,13 +599,23 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
List<dynamic> itemList, List<dynamic> itemList,
Map<String, dynamic>? hasilTertinggi, Map<String, dynamic>? hasilTertinggi,
String type, String type,
) { ) {
// Check if there's a 100% match in hasilTertinggi
if (hasilTertinggi != null) {
double probabilitas = _getProbabilitas(hasilTertinggi);
if ((probabilitas * 100).round() == 100) {
return _buildEmptyResult(
'Ditemukan kecocokan 100% pada diagnosa utama',
);
}
}
if (itemList.isEmpty) { if (itemList.isEmpty) {
return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya'); return _buildEmptyResult('Tidak ada kemungkinan ${type} lainnya');
} }
// Filter out the top result that's already shown // Filter out items with 100% probability and the top result
List<dynamic> otherItems = []; List otherItems = [];
if (hasilTertinggi != null) { if (hasilTertinggi != null) {
// Get the ID of the top result // Get the ID of the top result
@ -452,7 +626,7 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
topResultId = hasilTertinggi['id_hama']?.toString(); topResultId = hasilTertinggi['id_hama']?.toString();
} }
// Filter out the top result // Filter out the top result AND items with 100% probability
otherItems = itemList.where((item) { otherItems = itemList.where((item) {
String? itemId; String? itemId;
if (type == 'penyakit') { if (type == 'penyakit') {
@ -460,11 +634,26 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
} else { } else {
itemId = item['id_hama']?.toString(); itemId = item['id_hama']?.toString();
} }
return topResultId == null || itemId != topResultId;
// Skip if this is the top result
if (topResultId != null && itemId == topResultId) {
return false;
}
// Skip if this item has 100% probability
double itemProbabilitas = _getProbabilitas(item);
if ((itemProbabilitas * 100).round() == 100) {
return false;
}
return true;
}).toList(); }).toList();
} else { } else {
// If no top result, skip the first item // If no hasilTertinggi, filter out 100% probability items from all except first
otherItems = itemList.skip(1).toList(); otherItems = itemList.skip(1).where((item) {
double itemProbabilitas = _getProbabilitas(item);
return (itemProbabilitas * 100).round() != 100;
}).toList();
} }
if (otherItems.isEmpty) { if (otherItems.isEmpty) {
@ -472,11 +661,9 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
} }
return Column( return Column(
children: otherItems children: otherItems.map((item) => _buildItemCard(item, type)).toList(),
.map((item) => _buildItemCard(item, type))
.toList(),
); );
} }
Future<void> _fetchAdditionalData() async { Future<void> _fetchAdditionalData() async {
setState(() { setState(() {
@ -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,27 +856,58 @@ 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,7 +959,8 @@ class _HasilDiagnosaPageState extends State<HasilDiagnosaPage> {
} }
Widget _buildProbabilityIndicator(double value) { Widget _buildProbabilityIndicator(double value) {
final Color indicatorColor = value > 0.7 final Color indicatorColor =
value > 0.7
? Colors.red ? Colors.red
: value > 0.4 : value > 0.4
? Colors.orange ? Colors.orange

View File

@ -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(
SnackBar(content: Text(responseData['message'] ?? "Login gagal")),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Terjadi kesalahan: $e")),
);
}
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
_showErrorDialog(
responseData['message'] ?? 'Email atau Password salah',
);
}
} catch (e) {
setState(() {
_isLoading = false;
});
_showErrorDialog('Terjadi kesalahan koneksi. Silakan coba lagi.');
}
} }
@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,7 +209,8 @@ class _LoginPageState extends State<LoginPage> {
), ),
), ),
onPressed: _isLoading ? null : _login, onPressed: _isLoading ? null : _login,
child: _isLoading child:
_isLoading
? CircularProgressIndicator( ? CircularProgressIndicator(
color: Colors.white, color: Colors.white,
) )
@ -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(
backgroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.person_add_outlined,
color: Color(0xFF9DC08D),
size: 16,
),
SizedBox(width: 8),
Text(
'Belum punya akun? Daftar', 'Belum punya akun? Daftar',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Color(0xFF9DC08D),
decoration: TextDecoration.underline, fontWeight: FontWeight.w500,
fontSize: 13,
), ),
), ),
],
), ),
SizedBox(width: 10), ),
GestureDetector( SizedBox(height: 10),
onTap: () { TextButton(
onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -205,13 +281,35 @@ class _LoginPageState extends State<LoginPage> {
), ),
); );
}, },
child: Text( style: TextButton.styleFrom(
backgroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lock_reset_outlined,
color: Color(0xFF9DC08D),
size: 16,
),
SizedBox(width: 8),
Text(
'Lupa Password?', 'Lupa Password?',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Color(0xFF9DC08D),
decoration: TextDecoration.underline, fontWeight: FontWeight.w500,
fontSize: 13,
), ),
), ),
],
),
), ),
], ],
), ),

View File

@ -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,26 +299,41 @@ 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(
children: [
Icon(Icons.error, color: Colors.white),
SizedBox(width: 8),
Expanded(
child: Text(
'Gagal memperbarui profil: ${e.toString()}', 'Gagal memperbarui profil: ${e.toString()}',
), ),
),
],
),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
} }
} }
}, },
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,13 +346,29 @@ 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: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.account_circle, color: Colors.white, size: 32),
SizedBox(width: 12),
Text( Text(
"Profil Pengguna", "Profil Pengguna",
style: TextStyle( style: TextStyle(
@ -280,7 +376,8 @@ class _ProfilPageState extends State<ProfilPage> {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white, color: Colors.white,
), ),
textAlign: TextAlign.center, ),
],
), ),
SizedBox(height: 80), SizedBox(height: 80),
], ],
@ -292,6 +389,11 @@ class _ProfilPageState extends State<ProfilPage> {
Positioned( Positioned(
top: 40.0, top: 40.0,
left: 16.0, left: 16.0,
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(25),
),
child: IconButton( child: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white, size: 28), icon: Icon(Icons.arrow_back, color: Colors.white, size: 28),
onPressed: () { onPressed: () {
@ -299,25 +401,26 @@ class _ProfilPageState extends State<ProfilPage> {
}, },
), ),
), ),
),
// 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),
// Buttons row
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Button untuk update data profil // Button untuk update data profil
ElevatedButton( Expanded(
child: ElevatedButton.icon(
onPressed: _showUpdateProfileDialog, onPressed: _showUpdateProfileDialog,
style: ElevatedButton.styleFrom( icon: Icon(
shape: RoundedRectangleBorder( Icons.edit,
borderRadius: BorderRadius.circular(8), color: Color(0xFF9DC08D),
size: 20,
), ),
backgroundColor: Colors.white, label: Text(
padding: EdgeInsets.symmetric( "Update Profil",
horizontal: 32,
vertical: 12,
),
),
child: Text(
"Update Data Profil",
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 14,
color: Color(0xFF9DC08D), color: Color(0xFF9DC08D),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
),
SizedBox(height: 30),
// Button untuk logout
ElevatedButton(
onPressed: () => _logout(context),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(10),
), ),
backgroundColor: Colors.white, backgroundColor: Colors.white,
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 32, horizontal: 16,
vertical: 12, vertical: 12,
), ),
elevation: 2,
), ),
child: Text( ),
),
SizedBox(width: 12),
// Button untuk logout
Expanded(
child: ElevatedButton.icon(
onPressed: () => _logout(context),
icon: Icon(
Icons.logout,
color: Colors.red[700],
size: 20,
),
label: Text(
"Logout", "Logout",
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 14,
color: Color(0xFF9DC08D), color: Colors.red[700],
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
backgroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
elevation: 2,
),
),
),
],
), ),
], ],
), ),
@ -392,9 +516,16 @@ class _ProfilPageState extends State<ProfilPage> {
children: [ children: [
CircularProgressIndicator(color: Color(0xFF9DC08D)), CircularProgressIndicator(color: Color(0xFF9DC08D)),
SizedBox(height: 16), SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.hourglass_empty, color: Color(0xFF9DC08D)),
SizedBox(width: 8),
Text("Memuat data profil..."), 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,
),
),
],
),
),
],
),
); );
} }
} }

View File

@ -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,26 +216,10 @@ 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,
password: passwordController.text,
alamat: alamatController.text,
nomorTelepon: nomorHpController.text,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Registrasi berhasil!')),
);
Navigator.pop(context); // kembali ke login page
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Registrasi gagal: $e')),
);
}
},
child: Text(
'Daftar', 'Daftar',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,