update layout admin for get data

This commit is contained in:
unknown 2025-06-14 23:11:05 +07:00
parent 213eb6273c
commit db83fb1c2f
15 changed files with 2569 additions and 1355 deletions

View File

@ -19,8 +19,6 @@ dotenv.config();
const app = express(); const app = express();
// Middlewares // Middlewares
app.use(express.json()); app.use(express.json());
app.use(cors()); app.use(cors());

View File

@ -11,7 +11,7 @@ const swaggerOptions = {
}, },
servers: [ servers: [
{ {
url: 'https://backend-sistem-pakar-diagnosa-penya.vercel.app', url: 'https://localhost:5000', // Development server URL
description: 'Production Server' description: 'Production Server'
}, },
], ],

View File

@ -11,9 +11,14 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
List<Map<String, dynamic>> historiData = []; List<Map<String, dynamic>> historiData = [];
List<Map<String, dynamic>> groupedHistoriData = []; List<Map<String, dynamic>> groupedHistoriData = [];
List<Map<String, dynamic>> filteredHistoriData = []; // Data yang sudah difilter
bool isLoading = true; bool isLoading = true;
String? error; String? error;
// Search variables
TextEditingController searchController = TextEditingController();
String searchQuery = '';
// Pagination variables // Pagination variables
int _rowsPerPage = 10; int _rowsPerPage = 10;
int _currentPage = 0; int _currentPage = 0;
@ -24,6 +29,35 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
void initState() { void initState() {
super.initState(); super.initState();
_loadHistoriData(); _loadHistoriData();
searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
searchController.removeListener(_onSearchChanged);
searchController.dispose();
super.dispose();
}
void _onSearchChanged() {
setState(() {
searchQuery = searchController.text.toLowerCase();
_filterData();
_updatePagination(0); // Reset ke halaman pertama saat search
});
}
void _filterData() {
if (searchQuery.isEmpty) {
filteredHistoriData = List.from(groupedHistoriData);
} else {
filteredHistoriData = groupedHistoriData.where((histori) {
final userName = (histori['userName'] ?? '').toString().toLowerCase();
final diagnosa = (histori['diagnosa'] ?? '').toString().toLowerCase();
return userName.contains(searchQuery) || diagnosa.contains(searchQuery);
}).toList();
}
} }
Future<void> _loadHistoriData() async { Future<void> _loadHistoriData() async {
@ -34,7 +68,9 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
}); });
// Dapatkan semua histori terlebih dahulu // Dapatkan semua histori terlebih dahulu
final allHistori = await apiService.getAllHistori(); // Kumpulkan semua userIds yang unik final allHistori = await apiService.getAllHistori();
// Kumpulkan semua userIds yang unik
Set<String> uniqueUserIds = allHistori Set<String> uniqueUserIds = allHistori
.where((histori) => histori['userId'] != null) .where((histori) => histori['userId'] != null)
.map((histori) => histori['userId'].toString()) .map((histori) => histori['userId'].toString())
@ -60,6 +96,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
setState(() { setState(() {
historiData = detailedHistori; // Simpan data asli jika perlu historiData = detailedHistori; // Simpan data asli jika perlu
groupedHistoriData = groupedData; // Data yang sudah dikelompokkan groupedHistoriData = groupedData; // Data yang sudah dikelompokkan
filteredHistoriData = List.from(groupedData); // Initialize filtered data
_updatePagination(0); // Set halaman pertama _updatePagination(0); // Set halaman pertama
isLoading = false; isLoading = false;
}); });
@ -104,7 +141,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
diagnosa = 'Tidak ada diagnosa'; diagnosa = 'Tidak ada diagnosa';
} }
// Ambil nama user dari kolom 'nama' atau 'name' (sesuaikan dengan struktur data Anda) // Ambil nama user dari kolom 'nama' atau 'name'
String userName = item['name']?.toString() ?? 'User ID: ${item['userId']}'; String userName = item['name']?.toString() ?? 'User ID: ${item['userId']}';
// Buat composite key: userId + waktu + diagnosa // Buat composite key: userId + waktu + diagnosa
@ -116,7 +153,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
groupedMap[key] = { groupedMap[key] = {
'userId': item['userId'], 'userId': item['userId'],
'userName': userName, // Menampilkan nama user, bukan ID 'userName': userName,
'diagnosa': diagnosa, 'diagnosa': diagnosa,
'tanggal_diagnosa': item['tanggal_diagnosa'], 'tanggal_diagnosa': item['tanggal_diagnosa'],
'tanggal_display': displayDate, 'tanggal_display': displayDate,
@ -124,7 +161,8 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
'hasil': item['hasil'], 'hasil': item['hasil'],
'penyakit_nama': item['penyakit_nama'], 'penyakit_nama': item['penyakit_nama'],
'hama_nama': item['hama_nama'], 'hama_nama': item['hama_nama'],
'sortTime': dateTime.millisecondsSinceEpoch, // untuk pengurutan 'sortTime': dateTime.millisecondsSinceEpoch,
'detailData': [], // Menyimpan semua item detail untuk halaman detail
}; };
} }
@ -133,6 +171,9 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
!groupedMap[key]!['gejala'].contains(item['gejala_nama'])) { !groupedMap[key]!['gejala'].contains(item['gejala_nama'])) {
groupedMap[key]!['gejala'].add(item['gejala_nama']); groupedMap[key]!['gejala'].add(item['gejala_nama']);
} }
// Simpan data detail untuk halaman detail
groupedMap[key]!['detailData'].add(item);
} }
// Konversi map ke list dan urutkan berdasarkan waktu terbaru // Konversi map ke list dan urutkan berdasarkan waktu terbaru
@ -147,22 +188,32 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
// Update pagination // Update pagination
void _updatePagination(int page) { void _updatePagination(int page) {
_currentPage = page; _currentPage = page;
_totalPages = (groupedHistoriData.length / _rowsPerPage).ceil(); _totalPages = (filteredHistoriData.length / _rowsPerPage).ceil();
int startIndex = page * _rowsPerPage; int startIndex = page * _rowsPerPage;
int endIndex = (page + 1) * _rowsPerPage; int endIndex = (page + 1) * _rowsPerPage;
if (endIndex > groupedHistoriData.length) { if (endIndex > filteredHistoriData.length) {
endIndex = groupedHistoriData.length; endIndex = filteredHistoriData.length;
} }
if (startIndex >= groupedHistoriData.length) { if (startIndex >= filteredHistoriData.length) {
_currentPageData = []; _currentPageData = [];
} else { } else {
_currentPageData = groupedHistoriData.sublist(startIndex, endIndex); _currentPageData = filteredHistoriData.sublist(startIndex, endIndex);
} }
} }
// Navigasi ke halaman detail
void _navigateToDetail(Map<String, dynamic> histori) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailHistoriPage(histori: histori),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -170,8 +221,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
title: Text('Riwayat Diagnosa'), title: Text('Riwayat Diagnosa'),
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFF9DC08D),
), ),
body: body: isLoading
isLoading
? Center(child: CircularProgressIndicator()) ? Center(child: CircularProgressIndicator())
: error != null : error != null
? Center(child: Text('Error: $error')) ? Center(child: Text('Error: $error'))
@ -179,98 +229,147 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
? Center(child: Text('Tidak ada data riwayat diagnosa')) ? Center(child: Text('Tidak ada data riwayat diagnosa'))
: Column( : Column(
children: [ children: [
// Search Bar
Container(
margin: EdgeInsets.all(16),
child: TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'Cari berdasarkan nama user atau diagnosa...',
prefixIcon: Icon(
Icons.search,
color: Color(0xFF9DC08D),
),
suffixIcon: searchQuery.isNotEmpty
? IconButton(
icon: Icon(Icons.clear),
onPressed: () {
searchController.clear();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFF9DC08D)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFF9DC08D), width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
filled: true,
fillColor: Colors.grey[50],
),
),
),
Expanded( Expanded(
child: SingleChildScrollView( child: filteredHistoriData.isEmpty && searchQuery.isNotEmpty
scrollDirection: Axis.horizontal, ? Center(
child: SingleChildScrollView( child: Column(
child: DataTable( mainAxisAlignment: MainAxisAlignment.center,
columnSpacing: 20, children: [
headingRowColor: MaterialStateProperty.all( Icon(
Color(0xFF9DC08D).withOpacity(0.3), Icons.search_off,
size: 64,
color: Colors.grey[400],
), ),
columns: [ SizedBox(height: 16),
DataColumn( Text(
label: Text( 'Tidak ada hasil untuk "${searchController.text}"',
'Nama User', style: TextStyle(
style: TextStyle(fontWeight: FontWeight.bold), fontSize: 16,
color: Colors.grey[600],
), ),
textAlign: TextAlign.center,
), ),
DataColumn( SizedBox(height: 8),
label: Text( Text(
'Gejala', 'Coba gunakan kata kunci yang berbeda',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(
), fontSize: 14,
), color: Colors.grey[500],
DataColumn(
label: Text(
'Diagnosa',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
DataColumn(
label: Text(
'Hasil',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
DataColumn(
label: Text(
'Tanggal',
style: TextStyle(fontWeight: FontWeight.bold),
), ),
), ),
], ],
rows: ),
_currentPageData.map((histori) { )
// Gabungkan semua gejala menjadi satu string dengan koma : ListView.builder(
String gejalaText = "Tidak ada gejala"; itemCount: _currentPageData.length,
if (histori['gejala'] != null && itemBuilder: (context, index) {
(histori['gejala'] as List).isNotEmpty) { final histori = _currentPageData[index];
gejalaText = (histori['gejala'] as List).join(
', ',
);
}
return DataRow( return Container(
cells: [ margin: EdgeInsets.only(bottom: 12, left: 16, right: 16),
DataCell(Text(histori['userName'] ?? 'User tidak ditemukan')), child: Row(
DataCell( children: [
Container( // Card dengan informasi histori
constraints: BoxConstraints( Expanded(
maxWidth: 200, child: Card(
), elevation: 2,
child: Tooltip( child: InkWell(
message: gejalaText, onTap: () => _navigateToDetail(histori),
child: Text( child: Container(
gejalaText, padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
overflow: TextOverflow.ellipsis, child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
), children: [
), // Nama User
),
DataCell(
Text( Text(
histori['diagnosa'] ?? histori['userName'] ?? 'User tidak ditemukan',
'Tidak ada diagnosa',
style: TextStyle( style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 8),
// Diagnosa
Text(
histori['diagnosa'] ?? 'Tidak ada diagnosa',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
SizedBox(height: 4),
// Tanggal
Text(
histori['tanggal_display'] ?? '',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
), ),
DataCell(
Text(_formatHasil(histori['hasil'])),
),
DataCell(
Text(histori['tanggal_display'] ?? ''),
), ),
], ],
),
),
),
),
),
SizedBox(width: 8),
// Button detail di luar card
Container(
decoration: BoxDecoration(
color: Color(0xFF9DC08D),
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
icon: Icon(Icons.info_outline, color: Colors.white),
onPressed: () => _navigateToDetail(histori),
tooltip: 'Lihat Detail',
),
),
],
),
); );
}).toList(), },
), ),
), ),
),
), // Pagination controls // Pagination controls
Container( Container(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4), padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row( child: Row(
@ -284,8 +383,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
minWidth: 32, minWidth: 32,
minHeight: 32, minHeight: 32,
), ),
onPressed: onPressed: _currentPage > 0
_currentPage > 0
? () { ? () {
setState(() { setState(() {
_updatePagination(0); _updatePagination(0);
@ -300,8 +398,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
minWidth: 32, minWidth: 32,
minHeight: 32, minHeight: 32,
), ),
onPressed: onPressed: _currentPage > 0
_currentPage > 0
? () { ? () {
setState(() { setState(() {
_updatePagination(_currentPage - 1); _updatePagination(_currentPage - 1);
@ -325,8 +422,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
minWidth: 32, minWidth: 32,
minHeight: 32, minHeight: 32,
), ),
onPressed: onPressed: _currentPage < _totalPages - 1
_currentPage < _totalPages - 1
? () { ? () {
setState(() { setState(() {
_updatePagination(_currentPage + 1); _updatePagination(_currentPage + 1);
@ -341,8 +437,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
minWidth: 32, minWidth: 32,
minHeight: 32, minHeight: 32,
), ),
onPressed: onPressed: _currentPage < _totalPages - 1
_currentPage < _totalPages - 1
? () { ? () {
setState(() { setState(() {
_updatePagination(_totalPages - 1); _updatePagination(_totalPages - 1);
@ -352,7 +447,9 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
), ),
], ],
), ),
), // Rows per page selector ),
// Rows per page selector
Container( Container(
padding: EdgeInsets.only(bottom: 8), padding: EdgeInsets.only(bottom: 8),
child: Row( child: Row(
@ -367,8 +464,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
value: _rowsPerPage, value: _rowsPerPage,
isDense: true, isDense: true,
menuMaxHeight: 200, menuMaxHeight: 200,
items: items: [10, 20, 50, 100].map((value) {
[10, 20, 50, 100].map((value) {
return DropdownMenuItem<int>( return DropdownMenuItem<int>(
value: value, value: value,
child: Text('$value'), child: Text('$value'),
@ -377,9 +473,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_rowsPerPage = value!; _rowsPerPage = value!;
_updatePagination( _updatePagination(0);
0,
); // Kembali ke halaman pertama
}); });
}, },
), ),
@ -406,3 +500,213 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
return '${(hasilValue * 100).toStringAsFixed(2)}%'; return '${(hasilValue * 100).toStringAsFixed(2)}%';
} }
} }
// Halaman Detail Histori
class DetailHistoriPage extends StatelessWidget {
final Map<String, dynamic> histori;
const DetailHistoriPage({Key? key, required this.histori}) : super(key: key);
@override
Widget build(BuildContext context) {
// Gabungkan semua gejala menjadi satu string
String gejalaText = "Tidak ada gejala";
if (histori['gejala'] != null && (histori['gejala'] as List).isNotEmpty) {
gejalaText = (histori['gejala'] as List).join(', ');
}
return Scaffold(
appBar: AppBar(
title: Text('Detail Riwayat Diagnosa'),
backgroundColor: Color(0xFF9DC08D),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Color(0xFF9DC08D).withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Informasi Diagnosa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF9DC08D),
),
textAlign: TextAlign.center,
),
),
SizedBox(height: 20),
// Nama User
_buildDetailRow(
'Nama User',
histori['userName'] ?? 'User tidak ditemukan',
Icons.person,
),
SizedBox(height: 16),
// Tanggal Diagnosa
_buildDetailRow(
'Tanggal Diagnosa',
histori['tanggal_display'] ?? '',
Icons.calendar_today,
),
SizedBox(height: 16),
// Diagnosa
_buildDetailRow(
'Diagnosa',
histori['diagnosa'] ?? 'Tidak ada diagnosa',
Icons.medical_services,
),
SizedBox(height: 16),
// Hasil
_buildDetailRow(
'Hasil',
_formatHasil(histori['hasil']),
Icons.analytics,
),
SizedBox(height: 16),
// Gejala
_buildDetailSection(
'Gejala yang Dipilih',
gejalaText,
Icons.list_alt,
),
],
),
),
),
),
);
}
Widget _buildDetailRow(
String label,
String value,
IconData icon, {
Color? valueColor,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon,
size: 20,
color: Color(0xFF9DC08D),
),
SizedBox(width: 12),
Expanded(
flex: 2,
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: Colors.grey[700],
),
),
),
Text(
': ',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
Expanded(
flex: 3,
child: Text(
value,
style: TextStyle(
fontSize: 14,
color: valueColor ?? Colors.black87,
fontWeight: valueColor != null ? FontWeight.w500 : FontWeight.normal,
),
),
),
],
);
}
Widget _buildDetailSection(
String label,
String value,
IconData icon,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
size: 20,
color: Color(0xFF9DC08D),
),
SizedBox(width: 12),
Text(
label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: Colors.grey[700],
),
),
],
),
SizedBox(height: 8),
Container(
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Text(
value,
style: TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
),
],
);
}
Color _getDiagnosaColor(Map<String, dynamic> histori) {
if (histori['penyakit_nama'] != null) {
return Colors.red[700]!;
} else if (histori['hama_nama'] != null) {
return Colors.amber[800]!;
}
return Colors.black;
}
String _formatHasil(dynamic hasil) {
if (hasil == null) return '0%';
double hasilValue = double.tryParse(hasil.toString()) ?? 0.0;
return '${(hasilValue * 100).toStringAsFixed(2)}%';
}
}

View File

@ -48,6 +48,25 @@ class _AdminPageState extends State<AdminPage> {
await prefs.setInt(LAST_KNOWN_COUNT_KEY, _lastKnownDiagnosisCount); await prefs.setInt(LAST_KNOWN_COUNT_KEY, _lastKnownDiagnosisCount);
} }
// Method untuk menghitung jumlah diagnosa berdasarkan tanggal unik
int _countUniqueByDate(List<dynamic> historiList) {
Set<String> uniqueDates = {};
for (var histori in historiList) {
// Ambil tanggal_diagnosa dari data
String? tanggalDiagnosa = histori['tanggal_diagnosa']?.toString();
if (tanggalDiagnosa != null && tanggalDiagnosa.isNotEmpty) {
// Extract hanya tanggal (YYYY-MM-DD) tanpa waktu
String dateOnly = tanggalDiagnosa.split(' ')[0];
uniqueDates.add(dateOnly);
}
}
print("Unique dates found: ${uniqueDates.toList()}");
return uniqueDates.length;
}
// Method untuk memuat data dashboard dari API // Method untuk memuat data dashboard dari API
Future<void> _loadDashboardData() async { Future<void> _loadDashboardData() async {
try { try {
@ -78,21 +97,33 @@ class _AdminPageState extends State<AdminPage> {
pestCount = hamaList.length; pestCount = hamaList.length;
print("Jumlah hama: $pestCount"); print("Jumlah hama: $pestCount");
// Modified diagnosis count logic print("Fetching histori data...");
// Mengambil data histori dan hitung berdasarkan tanggal unik
final allHistori = await ApiService().getAllHistori(); final allHistori = await ApiService().getAllHistori();
int currentCount = allHistori.length; print("Total histori records: ${allHistori.length}");
if (currentCount > _lastKnownDiagnosisCount) { // Hitung jumlah diagnosa berdasarkan tanggal unik
int newDiagnoses = currentCount - _lastKnownDiagnosisCount; int currentUniqueCount = _countUniqueByDate(allHistori);
print("Unique diagnosis dates: $currentUniqueCount");
// Update diagnosis count berdasarkan tanggal unik
if (currentUniqueCount > _lastKnownDiagnosisCount) {
int newDiagnoses = currentUniqueCount - _lastKnownDiagnosisCount;
diagnosisCount += newDiagnoses; diagnosisCount += newDiagnoses;
_lastKnownDiagnosisCount = currentCount; _lastKnownDiagnosisCount = currentUniqueCount;
// Save the updated counts // Save the updated counts
await _saveCounts(); await _saveCounts();
print("New diagnoses added: $newDiagnoses"); print("New unique diagnosis dates added: $newDiagnoses");
print("Total diagnosis count: $diagnosisCount"); print("Total diagnosis count: $diagnosisCount");
} else {
// Jika tidak ada penambahan, set ke current unique count
diagnosisCount = currentUniqueCount;
_lastKnownDiagnosisCount = currentUniqueCount;
await _saveCounts();
} }
} catch (e) { } catch (e) {
print("Error loading dashboard data: $e"); print("Error loading dashboard data: $e");
} finally { } finally {

View File

@ -288,24 +288,24 @@ class _EditHamaPageState extends State<EditHamaPage> {
maxLines: 3, maxLines: 3,
), ),
SizedBox(height: 20), SizedBox(height: 20),
TextField( // TextField(
controller: _nilaiPakarController, // controller: _nilaiPakarController,
decoration: InputDecoration( // decoration: InputDecoration(
labelText: 'Nilai Pakar', // labelText: 'Nilai Pakar',
hintText: 'Contoh: 0.5', // hintText: 'Contoh: 0.5',
), // ),
keyboardType: TextInputType.numberWithOptions(decimal: true), // keyboardType: TextInputType.numberWithOptions(decimal: true),
onChanged: (value) { // onChanged: (value) {
// Validate as user types (optional) // // Validate as user types (optional)
try { // try {
if (value.isNotEmpty) { // if (value.isNotEmpty) {
double.parse(value.replaceAll(',', '.')); // double.parse(value.replaceAll(',', '.'));
} // }
} catch (e) { // } catch (e) {
// Could show validation error here // // Could show validation error here
} // }
}, // },
), // ),
SizedBox(height: 20), SizedBox(height: 20),
Text( Text(
'Foto Hama', 'Foto Hama',

View File

@ -287,24 +287,24 @@ class _EditPenyakitPageState extends State<EditPenyakitPage> {
maxLines: 3, maxLines: 3,
), ),
SizedBox(height: 20), SizedBox(height: 20),
TextField( // TextField(
controller: _nilaiPakarController, // controller: _nilaiPakarController,
decoration: InputDecoration( // decoration: InputDecoration(
labelText: 'Nilai Pakar', // labelText: 'Nilai Pakar',
hintText: 'Contoh: 0.5', // hintText: 'Contoh: 0.5',
), // ),
keyboardType: TextInputType.numberWithOptions(decimal: true), // keyboardType: TextInputType.numberWithOptions(decimal: true),
onChanged: (value) { // onChanged: (value) {
// Validate as user types (optional) // // Validate as user types (optional)
try { // try {
if (value.isNotEmpty) { // if (value.isNotEmpty) {
double.parse(value.replaceAll(',', '.')); // double.parse(value.replaceAll(',', '.'));
} // }
} catch (e) { // } catch (e) {
// Could show validation error here // // Could show validation error here
} // }
}, // },
), // ),
SizedBox(height: 20), SizedBox(height: 20),
Text( Text(
'Foto Penyakit', 'Foto Penyakit',

View File

@ -9,13 +9,22 @@ class GejalaPage extends StatefulWidget {
class _GejalaPageState extends State<GejalaPage> { class _GejalaPageState extends State<GejalaPage> {
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
List<Map<String, dynamic>> gejalaList = []; List<Map<String, dynamic>> gejalaList = [];
List<Map<String, dynamic>> filteredGejalaList = [];
TextEditingController searchController = TextEditingController();
bool isSearchVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
fetchGejala(); fetchGejala();
searchController.addListener(_filterGejala);
} }
@override
void dispose() {
searchController.dispose();
super.dispose();
}
// 🔹 Ambil data gejala dari API // 🔹 Ambil data gejala dari API
Future<void> fetchGejala() async { Future<void> fetchGejala() async {
@ -23,12 +32,35 @@ class _GejalaPageState extends State<GejalaPage> {
final data = await apiService.getGejala(); final data = await apiService.getGejala();
setState(() { setState(() {
gejalaList = data; gejalaList = data;
filteredGejalaList = data;
}); });
} catch (e) { } catch (e) {
print('Error fetching gejala: $e'); print('Error fetching gejala: $e');
} }
} }
void _filterGejala() {
String query = searchController.text.toLowerCase();
setState(() {
filteredGejalaList = gejalaList.where((gejala) {
String nama = (gejala['nama'] ?? '').toLowerCase();
String kode = (gejala['kode'] ?? '').toLowerCase();
return nama.contains(query) || kode.contains(query);
}).toList();
currentPage = 0; // Reset pagination saat search
});
}
void _toggleSearch() {
setState(() {
isSearchVisible = !isSearchVisible;
if (!isSearchVisible) {
searchController.clear();
filteredGejalaList = gejalaList;
}
});
}
// 🔹 Tambah gejala baru ke API // 🔹 Tambah gejala baru ke API
void _tambahGejala() { void _tambahGejala() {
TextEditingController namaController = TextEditingController(); TextEditingController namaController = TextEditingController();
@ -76,9 +108,7 @@ class _GejalaPageState extends State<GejalaPage> {
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text( title: Text('Edit Gejala'),
'Edit Hama',
),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -154,8 +184,6 @@ class _GejalaPageState extends State<GejalaPage> {
); );
} }
//pagination //pagination
int currentPage = 0; int currentPage = 0;
int rowsPerPage = 10; int rowsPerPage = 10;
@ -163,85 +191,130 @@ class _GejalaPageState extends State<GejalaPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int start = currentPage * rowsPerPage; int start = currentPage * rowsPerPage;
int end = (start + rowsPerPage < gejalaList.length) int end = (start + rowsPerPage < filteredGejalaList.length)
? start + rowsPerPage ? start + rowsPerPage
: gejalaList.length; : filteredGejalaList.length;
List currentPageData = gejalaList.sublist(start, end); List currentPageData = filteredGejalaList.sublist(start, end);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Halaman Gejala'), title: Text('Halaman Gejala'),
backgroundColor: Color(0xFF9DC08D),
), ),
body: Column( body: Column(
children: [ children: [
SizedBox(height: 20), SizedBox(height: 16),
Row( // Search Button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Padding( ElevatedButton.icon(
padding: const EdgeInsets.only(right: 20.0), onPressed: _toggleSearch,
child: ElevatedButton( icon: Icon(Icons.search),
onPressed: _tambahGejala, label: Text('Cari'),
child: Text( style: ElevatedButton.styleFrom(
'Tambah Gejala', backgroundColor: Color(0xFF9DC08D),
style: TextStyle(color: Colors.green[200]), foregroundColor: Colors.white,
),
), ),
), ),
], ],
), ),
SizedBox(height: 20), ),
// Search Field (conditional)
if (isSearchVisible)
Container(
margin: EdgeInsets.all(16),
child: TextField(
controller: searchController,
autofocus: true,
decoration: InputDecoration(
hintText: 'Cari nama atau kode gejala...',
prefixIcon: Icon(Icons.search),
suffixIcon: IconButton(
icon: Icon(Icons.close),
onPressed: _toggleSearch,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
filled: true,
fillColor: Colors.grey[100],
),
),
),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView( child: Column(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 20,
headingRowColor: MaterialStateColor.resolveWith(
(states) => const Color(0xFF9DC08D),
),
columns: [
DataColumn(label: SizedBox(width: 35, child: Text('No'))),
DataColumn(label: SizedBox(width: 80, child: Text('Kode'))),
DataColumn(label: SizedBox(width: 150, child: Text('Nama'))),
DataColumn(label: SizedBox(width: 80, child: Text('Aksi'))),
],
rows: [
...currentPageData.map(
(gejala) => DataRow(
cells: [
DataCell(Text((gejalaList.indexOf(gejala) + 1).toString())),
DataCell(Text(gejala['kode'] ?? '-')),
DataCell(Text(gejala['nama'] ?? '-')),
DataCell(
Row(
children: [ children: [
IconButton( // Data List
icon: Icon(Icons.edit, color: Color(0xFF9DC08D)), Expanded(
onPressed: () => showEditDialog(context, gejala), child: ListView.builder(
itemCount: currentPageData.length,
itemBuilder: (context, index) {
final gejala = currentPageData[index];
return Container(
margin: EdgeInsets.only(bottom: 12),
child: Row(
children: [
// Card dengan nama gejala
Expanded(
child: Card(
elevation: 2,
child: InkWell(
onTap: () => showEditDialog(context, gejala),
child: Container(
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
gejala['nama'] ?? '-',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
), ),
IconButton( ),
icon: Icon(Icons.delete, color: Colors.red), SizedBox(height: 4),
Text(
gejala['kode'] ?? '-',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
),
),
),
SizedBox(width: 8),
// Button hapus di luar card
Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
icon: Icon(Icons.delete, color: Colors.white),
onPressed: () => _konfirmasiHapus(gejala['id']), onPressed: () => _konfirmasiHapus(gejala['id']),
), ),
],
),
), ),
], ],
), ),
);
},
), ),
DataRow( ),
cells: [ SizedBox(height: 16),
DataCell(Container()), // Pagination
DataCell(Container()), if (filteredGejalaList.length > rowsPerPage)
DataCell( Row(
Align( mainAxisAlignment: MainAxisAlignment.center,
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
icon: Icon(Icons.chevron_left), icon: Icon(Icons.chevron_left),
@ -249,30 +322,29 @@ Widget build(BuildContext context) {
? () => setState(() => currentPage--) ? () => setState(() => currentPage--)
: null, : null,
), ),
Text(' ${currentPage + 1}'), Text(
'Halaman ${currentPage + 1}',
style: TextStyle(fontSize: 16),
),
IconButton( IconButton(
icon: Icon(Icons.chevron_right), icon: Icon(Icons.chevron_right),
onPressed: onPressed: (currentPage + 1) * rowsPerPage < filteredGejalaList.length
(currentPage + 1) * rowsPerPage < gejalaList.length
? () => setState(() => currentPage++) ? () => setState(() => currentPage++)
: null, : null,
), ),
], ],
), ),
),
),
DataCell(Container()),
],
),
], ],
), ),
), ),
), ),
),
),
], ],
), ),
floatingActionButton: FloatingActionButton(
onPressed: _tambahGejala,
child: Icon(Icons.add),
backgroundColor: Color(0xFF9DC08D),
),
); );
} }
} }

View File

@ -11,11 +11,21 @@ class HamaPage extends StatefulWidget {
class _HamaPageState extends State<HamaPage> { class _HamaPageState extends State<HamaPage> {
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
List<Map<String, dynamic>> hamaList = []; List<Map<String, dynamic>> hamaList = [];
List<Map<String, dynamic>> filteredHamaList = [];
TextEditingController searchController = TextEditingController();
bool isSearchVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchHama(); _fetchHama();
searchController.addListener(_filterHama);
}
@override
void dispose() {
searchController.dispose();
super.dispose();
} }
Future<void> _fetchHama() async { Future<void> _fetchHama() async {
@ -23,12 +33,35 @@ class _HamaPageState extends State<HamaPage> {
List<Map<String, dynamic>> data = await apiService.getHama(); List<Map<String, dynamic>> data = await apiService.getHama();
setState(() { setState(() {
hamaList = data; hamaList = data;
filteredHamaList = data;
}); });
} catch (e) { } catch (e) {
print("Error fetching data: $e"); print("Error fetching data: $e");
} }
} }
void _filterHama() {
String query = searchController.text.toLowerCase();
setState(() {
filteredHamaList = hamaList.where((hama) {
String nama = (hama['nama'] ?? '').toLowerCase();
String kode = (hama['kode'] ?? '').toLowerCase();
return nama.contains(query) || kode.contains(query);
}).toList();
currentPage = 0; // Reset pagination saat search
});
}
void _toggleSearch() {
setState(() {
isSearchVisible = !isSearchVisible;
if (!isSearchVisible) {
searchController.clear();
filteredHamaList = hamaList;
}
});
}
// 🔹 Hapus gejala dari API // 🔹 Hapus gejala dari API
void _hapusHama(int id) async { void _hapusHama(int id) async {
try { try {
@ -45,7 +78,7 @@ class _HamaPageState extends State<HamaPage> {
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('Konfirmasi Hapus'), title: Text('Konfirmasi Hapus'),
content: Text('Apakah Anda yakin ingin menghapus gejala ini?'), content: Text('Apakah Anda yakin ingin menghapus hama ini?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -67,6 +100,71 @@ class _HamaPageState extends State<HamaPage> {
); );
} }
void _navigateToEdit(Map<String, dynamic> hama) {
// Parse nilai_pakar dengan aman
double nilaiPakar = 0.0;
if (hama['nilai_pakar'] != null) {
// Coba parse jika string
if (hama['nilai_pakar'] is String) {
try {
String nilaiStr = hama['nilai_pakar'].toString().trim();
if (nilaiStr.isNotEmpty) {
nilaiPakar = double.parse(nilaiStr.replaceAll(',', '.'));
}
} catch (e) {
print("Error parsing nilai_pakar: $e");
}
}
// Langsung gunakan jika sudah double
else if (hama['nilai_pakar'] is double) {
nilaiPakar = hama['nilai_pakar'];
}
// Jika int, konversi ke double
else if (hama['nilai_pakar'] is int) {
nilaiPakar = hama['nilai_pakar'].toDouble();
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditHamaPage(
idHama: hama['id'],
namaAwal: hama['nama'] ?? '',
deskripsiAwal: hama['deskripsi'] ?? '',
penangananAwal: hama['penanganan'] ?? '',
gambarUrl: hama['foto'] ?? '',
nilai_pakar: nilaiPakar,
onHamaUpdated: _fetchHama,
),
),
);
}
void _showPopupMenu(BuildContext context, int id) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Pilih Aksi'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(Icons.delete, color: Colors.red),
title: Text('Hapus Hama'),
onTap: () {
Navigator.pop(context);
_konfirmasiHapus(id);
},
),
],
),
);
},
);
}
//pagination //pagination
int currentPage = 0; int currentPage = 0;
int rowsPerPage = 10; int rowsPerPage = 10;
@ -74,203 +172,169 @@ class _HamaPageState extends State<HamaPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int start = currentPage * rowsPerPage; int start = currentPage * rowsPerPage;
int end = int end = (start + rowsPerPage < filteredHamaList.length)
(start + rowsPerPage < hamaList.length)
? start + rowsPerPage ? start + rowsPerPage
: hamaList.length; : filteredHamaList.length;
List currentPageData = hamaList.sublist(start, end); List currentPageData = filteredHamaList.sublist(start, end);
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Halaman Hama'), backgroundColor: Color(0xFF9DC08D)), appBar: AppBar(
title: Text('Halaman Hama'),
backgroundColor: Color(0xFF9DC08D),
),
body: Column( body: Column(
children: [ children: [
SizedBox(height: 20), SizedBox(height: 16),
Row( // Search Button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Padding( ElevatedButton.icon(
padding: const EdgeInsets.only(right: 20.0), onPressed: _toggleSearch,
child: ElevatedButton( icon: Icon(Icons.search),
onPressed: () { label: Text('Cari'),
Navigator.push( style: ElevatedButton.styleFrom(
context, backgroundColor: Color(0xFF9DC08D),
MaterialPageRoute( foregroundColor: Colors.white,
builder:
(context) => TambahHamaPage(
onHamaAdded:
_fetchHama, // Panggil fungsi refresh setelah tambah
),
),
);
},
child: Text(
'Tambah Hama',
style: TextStyle(color: Colors.green[200]),
),
), ),
), ),
], ],
), ),
SizedBox(height: 20), ),
// Search Field (conditional)
if (isSearchVisible)
Container(
margin: EdgeInsets.all(16),
child: TextField(
controller: searchController,
autofocus: true,
decoration: InputDecoration(
hintText: 'Cari nama hama...',
prefixIcon: Icon(Icons.search),
suffixIcon: IconButton(
icon: Icon(Icons.close),
onPressed: _toggleSearch,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
filled: true,
fillColor: Colors.grey[100],
),
),
),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView( child: Column(
scrollDirection: Axis.vertical, children: [
child: SingleChildScrollView( // Data List
scrollDirection: Axis.horizontal, Expanded(
child: DataTable( child: ListView.builder(
columnSpacing: 20, itemCount: currentPageData.length,
headingRowColor: MaterialStateColor.resolveWith( itemBuilder: (context, index) {
(states) => const Color(0xFF9DC08D), final hama = currentPageData[index];
return Container(
margin: EdgeInsets.only(bottom: 12),
child: Row(
children: [
// Card dengan nama hama
Expanded(
child: Card(
elevation: 2,
child: InkWell(
onTap: () => _navigateToEdit(hama),
child: Container(
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
hama['nama'] ?? '-',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
), ),
columns: [
DataColumn(label: SizedBox(width: 35, child: Text('No'))),
DataColumn(
label: SizedBox(width: 50, child: Text('Kode')),
), ),
DataColumn( SizedBox(height: 4),
label: SizedBox(width: 100, child: Text('Nama')), Text(
hama['kode'] ?? '-',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
), ),
DataColumn(
label: SizedBox(width: 100, child: Text('Deskripsi')),
),
DataColumn(
label: SizedBox(width: 100, child: Text('Penanganan')),
),
DataColumn(
label: SizedBox(width: 50, child: Text('Aksi')),
), ),
], ],
rows: [
...currentPageData.map(
(hama) => DataRow(
cells: [
DataCell(
Text((hamaList.indexOf(hama) + 1).toString()),
), ),
DataCell(Text(hama['kode'] ?? '-')),
DataCell(Text(hama['nama'] ?? '-')),
DataCell(Text(hama['deskripsi'] ?? '-')),
DataCell(Text(hama['penanganan'] ?? '-')),
DataCell(
Row(
children: [
IconButton(
icon: Icon(
Icons.edit,
color: Color(0xFF9DC08D),
), ),
onPressed: () {
// Parse nilai_pakar dengan aman
double nilaiPakar = 0.0;
if (hama['nilai_pakar'] != null) {
// Coba parse jika string
if (hama['nilai_pakar'] is String) {
try {
String nilaiStr =
hama['nilai_pakar']
.toString()
.trim();
if (nilaiStr.isNotEmpty) {
nilaiPakar = double.parse(
nilaiStr.replaceAll(',', '.'),
);
}
} catch (e) {
print(
"Error parsing nilai_pakar: $e",
);
}
}
// Langsung gunakan jika sudah double
else if (hama['nilai_pakar']
is double) {
nilaiPakar = hama['nilai_pakar'];
}
// Jika int, konversi ke double
else if (hama['nilai_pakar'] is int) {
nilaiPakar =
hama['nilai_pakar'].toDouble();
}
}
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => EditHamaPage(
idHama:
hama['id'], // pastikan 'hama' adalah Map dari API kamu
namaAwal: hama['nama'] ?? '',
deskripsiAwal:
hama['deskripsi'] ?? '',
penangananAwal:
hama['penanganan'] ?? '',
gambarUrl: hama['foto'] ?? '',
nilai_pakar: nilaiPakar,
onHamaUpdated:
_fetchHama, // fungsi untuk refresh list setelah update
), ),
), ),
),
SizedBox(width: 8),
// Button hapus di luar card
Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
icon: Icon(Icons.delete, color: Colors.white),
onPressed: () => _konfirmasiHapus(hama['id']),
),
),
],
),
); );
}, },
), ),
IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed:
() => _konfirmasiHapus(hama['id']),
), ),
], SizedBox(height: 16),
), // Pagination
), if (filteredHamaList.length > rowsPerPage)
], Row(
), mainAxisAlignment: MainAxisAlignment.center,
),
DataRow(
cells: [
DataCell(Container()),
DataCell(Container()),
DataCell(
Align(
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
icon: Icon(Icons.chevron_left), icon: Icon(Icons.chevron_left),
onPressed: onPressed: currentPage > 0
currentPage > 0 ? () => setState(() => currentPage--)
? () =>
setState(() => currentPage--)
: null, : null,
), ),
Text(' ${currentPage + 1}'), Text(
'Halaman ${currentPage + 1}',
style: TextStyle(fontSize: 16),
),
IconButton( IconButton(
icon: Icon(Icons.chevron_right), icon: Icon(Icons.chevron_right),
onPressed: onPressed: (currentPage + 1) * rowsPerPage < filteredHamaList.length
(currentPage + 1) * rowsPerPage < ? () => setState(() => currentPage++)
hamaList.length
? () =>
setState(() => currentPage++)
: null, : null,
), ),
], ],
), ),
),
),
DataCell(Container()),
DataCell(Container()),
DataCell(Container()),
],
),
], ],
), ),
), ),
), ),
),
),
], ],
), ),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TambahHamaPage(
onHamaAdded: _fetchHama,
),
),
);
},
child: Icon(Icons.add),
backgroundColor: Color(0xFF9DC08D),
),
); );
} }
} }

View File

@ -12,11 +12,21 @@ class PenyakitPage extends StatefulWidget {
class _PenyakitPageState extends State<PenyakitPage> { class _PenyakitPageState extends State<PenyakitPage> {
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
List<Map<String, dynamic>> penyakitList = []; List<Map<String, dynamic>> penyakitList = [];
List<Map<String, dynamic>> filteredPenyakitList = [];
TextEditingController searchController = TextEditingController();
bool isSearchVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetchPenyakit(); _fetchPenyakit();
searchController.addListener(_filterPenyakit);
}
@override
void dispose() {
searchController.dispose();
super.dispose();
} }
Future<void> _fetchPenyakit() async { Future<void> _fetchPenyakit() async {
@ -24,19 +34,42 @@ class _PenyakitPageState extends State<PenyakitPage> {
List<Map<String, dynamic>> data = await apiService.getPenyakit(); List<Map<String, dynamic>> data = await apiService.getPenyakit();
setState(() { setState(() {
penyakitList = data; penyakitList = data;
filteredPenyakitList = data;
}); });
} catch (e) { } catch (e) {
print("Error fetching data: $e"); print("Error fetching data: $e");
} }
} }
// 🔹 Hapus gejala dari API void _filterPenyakit() {
String query = searchController.text.toLowerCase();
setState(() {
filteredPenyakitList = penyakitList.where((penyakit) {
String nama = (penyakit['nama'] ?? '').toLowerCase();
String kode = (penyakit['kode'] ?? '').toLowerCase();
return nama.contains(query) || kode.contains(query);
}).toList();
currentPage = 0; // Reset pagination saat search
});
}
void _toggleSearch() {
setState(() {
isSearchVisible = !isSearchVisible;
if (!isSearchVisible) {
searchController.clear();
filteredPenyakitList = penyakitList;
}
});
}
// 🔹 Hapus penyakit dari API
void _hapusPenyakit(int id) async { void _hapusPenyakit(int id) async {
try { try {
await apiService.deletePenyakit(id); await apiService.deletePenyakit(id);
_fetchPenyakit(); // Refresh data setelah hapus _fetchPenyakit(); // Refresh data setelah hapus
} catch (e) { } catch (e) {
print('Error hapus gejala: $e'); print('Error hapus penyakit: $e');
} }
} }
@ -46,7 +79,7 @@ class _PenyakitPageState extends State<PenyakitPage> {
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('Konfirmasi Hapus'), title: Text('Konfirmasi Hapus'),
content: Text('Apakah Anda yakin ingin menghapus gejala ini?'), content: Text('Apakah Anda yakin ingin menghapus penyakit ini?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -68,6 +101,47 @@ class _PenyakitPageState extends State<PenyakitPage> {
); );
} }
void _navigateToEdit(Map<String, dynamic> penyakit) {
// Parse nilai_pakar dengan aman
double nilaiPakar = 0.0;
if (penyakit['nilai_pakar'] != null) {
// Coba parse jika string
if (penyakit['nilai_pakar'] is String) {
try {
String nilaiStr = penyakit['nilai_pakar'].toString().trim();
if (nilaiStr.isNotEmpty) {
nilaiPakar = double.parse(nilaiStr.replaceAll(',', '.'));
}
} catch (e) {
print("Error parsing nilai_pakar: $e");
}
}
// Langsung gunakan jika sudah double
else if (penyakit['nilai_pakar'] is double) {
nilaiPakar = penyakit['nilai_pakar'];
}
// Jika int, konversi ke double
else if (penyakit['nilai_pakar'] is int) {
nilaiPakar = penyakit['nilai_pakar'].toDouble();
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditPenyakitPage(
idPenyakit: penyakit['id'],
namaAwal: penyakit['nama'] ?? '',
deskripsiAwal: penyakit['deskripsi'] ?? '',
penangananAwal: penyakit['penanganan'] ?? '',
gambarUrl: penyakit['foto'] ?? '',
nilai_pakar: nilaiPakar,
onPenyakitUpdated: _fetchPenyakit,
),
),
);
}
//pagination //pagination
int currentPage = 0; int currentPage = 0;
int rowsPerPage = 10; int rowsPerPage = 10;
@ -75,203 +149,169 @@ class _PenyakitPageState extends State<PenyakitPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int start = currentPage * rowsPerPage; int start = currentPage * rowsPerPage;
int end = int end = (start + rowsPerPage < filteredPenyakitList.length)
(start + rowsPerPage < penyakitList.length)
? start + rowsPerPage ? start + rowsPerPage
: penyakitList.length; : filteredPenyakitList.length;
List currentPageData = penyakitList.sublist(start, end); List currentPageData = filteredPenyakitList.sublist(start, end);
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Halaman Penyakit'), backgroundColor: Color(0xFF9DC08D)), appBar: AppBar(
title: Text('Halaman Penyakit'),
backgroundColor: Color(0xFF9DC08D),
),
body: Column( body: Column(
children: [ children: [
SizedBox(height: 20), SizedBox(height: 16),
Row( // Search Button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Padding( ElevatedButton.icon(
padding: const EdgeInsets.only(right: 20.0), onPressed: _toggleSearch,
child: ElevatedButton( icon: Icon(Icons.search),
onPressed: () { label: Text('Cari'),
Navigator.push( style: ElevatedButton.styleFrom(
context, backgroundColor: Color(0xFF9DC08D),
MaterialPageRoute( foregroundColor: Colors.white,
builder:
(context) => TambahPenyakitPage(
onPenyakitAdded:
_fetchPenyakit, // Panggil fungsi refresh setelah tambah
),
),
);
}, // Fungsi untuk menambah data penyakit
child: Text(
'Tambah Penyakit',
style: TextStyle(color: Colors.green[200]),
),
), ),
), ),
], ],
), ),
SizedBox(height: 20), ),
// Search Field (conditional)
if (isSearchVisible)
Container(
margin: EdgeInsets.all(16),
child: TextField(
controller: searchController,
autofocus: true,
decoration: InputDecoration(
hintText: 'Cari nama atau kode penyakit...',
prefixIcon: Icon(Icons.search),
suffixIcon: IconButton(
icon: Icon(Icons.close),
onPressed: _toggleSearch,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
filled: true,
fillColor: Colors.grey[100],
),
),
),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView( child: Column(
scrollDirection: Axis.vertical, children: [
child: SingleChildScrollView( // Data List
scrollDirection: Axis.horizontal, Expanded(
child: DataTable( child: ListView.builder(
columnSpacing: 20, itemCount: currentPageData.length,
headingRowColor: MaterialStateColor.resolveWith( itemBuilder: (context, index) {
(states) => const Color(0xFF9DC08D), final penyakit = currentPageData[index];
return Container(
margin: EdgeInsets.only(bottom: 12),
child: Row(
children: [
// Card dengan nama penyakit
Expanded(
child: Card(
elevation: 2,
child: InkWell(
onTap: () => _navigateToEdit(penyakit),
child: Container(
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
penyakit['nama'] ?? '-',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
), ),
columns: [
DataColumn(label: SizedBox(width: 35, child: Text('No'))),
DataColumn(
label: SizedBox(width: 50, child: Text('Kode')),
), ),
DataColumn( SizedBox(height: 4),
label: SizedBox(width: 100, child: Text('Nama')), Text(
penyakit['kode'] ?? '-',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
), ),
DataColumn(
label: SizedBox(width: 100, child: Text('Deskripsi')),
),
DataColumn(
label: SizedBox(width: 100, child: Text('Penanganan')),
),
DataColumn(
label: SizedBox(width: 50, child: Text('Aksi')),
), ),
], ],
rows: [
...currentPageData.map(
(penyakit) => DataRow(
cells: [
DataCell(
Text((penyakitList.indexOf(penyakit) + 1).toString()),
), ),
DataCell(Text(penyakit['kode'] ?? '-')),
DataCell(Text(penyakit['nama'] ?? '-')),
DataCell(Text(penyakit['deskripsi'] ?? '-')),
DataCell(Text(penyakit['penanganan'] ?? '-')),
DataCell(
Row(
children: [
IconButton(
icon: Icon(
Icons.edit,
color: Color(0xFF9DC08D),
), ),
onPressed: () {
// Parse nilai_pakar dengan aman
double nilaiPakar = 0.0;
if (penyakit['nilai_pakar'] != null) {
// Coba parse jika string
if (penyakit['nilai_pakar'] is String) {
try {
String nilaiStr =
penyakit['nilai_pakar']
.toString()
.trim();
if (nilaiStr.isNotEmpty) {
nilaiPakar = double.parse(
nilaiStr.replaceAll(',', '.'),
);
}
} catch (e) {
print(
"Error parsing nilai_pakar: $e",
);
}
}
// Langsung gunakan jika sudah double
else if (penyakit['nilai_pakar']
is double) {
nilaiPakar = penyakit['nilai_pakar'];
}
// Jika int, konversi ke double
else if (penyakit['nilai_pakar'] is int) {
nilaiPakar =
penyakit['nilai_pakar'].toDouble();
}
}
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => EditPenyakitPage(
idPenyakit:
penyakit['id'], // pastikan 'hama' adalah Map dari API kamu
namaAwal: penyakit['nama'] ?? '',
deskripsiAwal:
penyakit['deskripsi'] ?? '',
penangananAwal:
penyakit['penanganan'] ?? '',
gambarUrl:
penyakit['foto'] ?? '',
nilai_pakar: nilaiPakar,
onPenyakitUpdated:
_fetchPenyakit, // fungsi untuk refresh list setelah update
), ),
), ),
),
SizedBox(width: 8),
// Button hapus di luar card
Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
icon: Icon(Icons.delete, color: Colors.white),
onPressed: () => _konfirmasiHapus(penyakit['id']),
),
),
],
),
); );
}, },
), ),
IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed:
() => _konfirmasiHapus(penyakit['id']),
), ),
], SizedBox(height: 16),
), // Pagination
), if (filteredPenyakitList.length > rowsPerPage)
], Row(
), mainAxisAlignment: MainAxisAlignment.center,
),
DataRow(
cells: [
DataCell(Container()),
DataCell(Container()),
DataCell(
Align(
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
icon: Icon(Icons.chevron_left), icon: Icon(Icons.chevron_left),
onPressed: onPressed: currentPage > 0
currentPage > 0 ? () => setState(() => currentPage--)
? () =>
setState(() => currentPage--)
: null, : null,
), ),
Text(' ${currentPage + 1}'), Text(
'Halaman ${currentPage + 1}',
style: TextStyle(fontSize: 16),
),
IconButton( IconButton(
icon: Icon(Icons.chevron_right), icon: Icon(Icons.chevron_right),
onPressed: onPressed: (currentPage + 1) * rowsPerPage < filteredPenyakitList.length
(currentPage + 1) * rowsPerPage < ? () => setState(() => currentPage++)
penyakitList.length
? () =>
setState(() => currentPage++)
: null, : null,
), ),
], ],
), ),
),
),
DataCell(Container()),
DataCell(Container()),
DataCell(Container()),
],
),
], ],
), ),
), ),
), ),
),
),
], ],
), ),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TambahPenyakitPage(
onPenyakitAdded: _fetchPenyakit,
),
),
);
},
child: Icon(Icons.add),
backgroundColor: Color(0xFF9DC08D),
),
); );
} }
} }

View File

@ -3,7 +3,6 @@ import 'package:SIBAYAM/admin/edit_rule_page.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:SIBAYAM/api_services/api_services.dart'; import 'package:SIBAYAM/api_services/api_services.dart';
import 'tambah_rule_page.dart'; import 'tambah_rule_page.dart';
import 'edit_hama_page.dart';
class RulePage extends StatefulWidget { class RulePage extends StatefulWidget {
const RulePage({Key? key}) : super(key: key); const RulePage({Key? key}) : super(key: key);
@ -18,8 +17,13 @@ class _RulePageState extends State<RulePage> {
List<Map<String, dynamic>> hamaList = []; List<Map<String, dynamic>> hamaList = [];
List<dynamic> rules = []; List<dynamic> rules = [];
List<dynamic> filteredRules = [];
bool isLoading = true; bool isLoading = true;
// Search and filter variables
TextEditingController searchController = TextEditingController();
String selectedFilter = 'Semua'; // 'Semua', 'Penyakit', 'Hama'
// Pagination variables // Pagination variables
int currentPage = 0; int currentPage = 0;
int rowsPerPage = 10; int rowsPerPage = 10;
@ -28,9 +32,57 @@ class _RulePageState extends State<RulePage> {
void initState() { void initState() {
super.initState(); super.initState();
fetchRules(); fetchRules();
searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
searchController.removeListener(_onSearchChanged);
searchController.dispose();
super.dispose();
}
void _onSearchChanged() {
_filterRules();
}
void _filterRules() {
setState(() {
filteredRules = rules.where((rule) {
// Filter berdasarkan kategori
bool categoryMatch = true;
if (selectedFilter == 'Penyakit') {
categoryMatch = rule['id_penyakit'] != null;
} else if (selectedFilter == 'Hama') {
categoryMatch = rule['id_hama'] != null;
}
// Filter berdasarkan search text
bool searchMatch = true;
if (searchController.text.isNotEmpty) {
final searchText = searchController.text.toLowerCase();
final namaKategori = rule['id_penyakit'] != null
? (rule['nama_penyakit'] ?? '').toLowerCase()
: (rule['nama_hama'] ?? '').toLowerCase();
final namaGejala = (rule['nama_gejala'] ?? '').toLowerCase();
searchMatch = namaKategori.contains(searchText) ||
namaGejala.contains(searchText);
}
return categoryMatch && searchMatch;
}).toList();
// Reset ke halaman pertama setelah filter
currentPage = 0;
});
} }
void fetchRules() async { void fetchRules() async {
setState(() {
isLoading = true;
});
final apiService = ApiService(); final apiService = ApiService();
try { try {
@ -49,12 +101,12 @@ class _RulePageState extends State<RulePage> {
...rulesPenyakit.map((rule) { ...rulesPenyakit.map((rule) {
final gejala = gejalaList.firstWhere( final gejala = gejalaList.firstWhere(
(item) => item['id'] == rule['id_gejala'], (item) => item['id'] == rule['id_gejala'],
orElse: () => {'nama': '-'}, orElse: () => {'nama': 'Gejala tidak ditemukan'},
); );
final penyakit = penyakitList.firstWhere( final penyakit = penyakitList.firstWhere(
(item) => item['id'] == rule['id_penyakit'], (item) => item['id'] == rule['id_penyakit'],
orElse: () => {'nama': '-'}, orElse: () => {'nama': 'Penyakit tidak ditemukan'},
); );
return { return {
@ -66,20 +118,19 @@ class _RulePageState extends State<RulePage> {
'nama_penyakit': penyakit['nama'], 'nama_penyakit': penyakit['nama'],
'nama_hama': null, 'nama_hama': null,
'nilai_pakar': rule['nilai_pakar'], 'nilai_pakar': rule['nilai_pakar'],
'type': 'Penyakit',
}; };
}), }),
// Mengolah rules hama // Mengolah rules hama
...rulesHama.map((rule) { ...rulesHama.map((rule) {
// Mencari gejala berdasarkan id
final gejala = gejalaList.firstWhere( final gejala = gejalaList.firstWhere(
(item) => item['id'] == rule['id_gejala'], (item) => item['id'] == rule['id_gejala'],
orElse: () => {'nama': 'TIDAK DITEMUKAN'}, orElse: () => {'nama': 'Gejala tidak ditemukan'},
); );
// Mencari hama berdasarkan id
final hama = hamaList.firstWhere( final hama = hamaList.firstWhere(
(item) => item['id'] == rule['id_hama'], (item) => item['id'] == rule['id_hama'],
orElse: () => {'nama': 'TIDAK DITEMUKAN'}, orElse: () => {'nama': 'Hama tidak ditemukan'},
); );
return { return {
@ -91,15 +142,23 @@ class _RulePageState extends State<RulePage> {
'nama_penyakit': null, 'nama_penyakit': null,
'nama_hama': hama['nama'], 'nama_hama': hama['nama'],
'nilai_pakar': rule['nilai_pakar'], 'nilai_pakar': rule['nilai_pakar'],
'type': 'Hama',
}; };
}), }),
]; ];
setState(() { setState(() {
rules = enrichedRules; rules = enrichedRules;
filteredRules = enrichedRules;
}); });
} catch (e) { } catch (e) {
print('Terjadi kesalahan saat memuat data: $e'); print('Terjadi kesalahan saat memuat data: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal memuat data: $e'),
backgroundColor: Colors.red,
),
);
} finally { } finally {
setState(() { setState(() {
isLoading = false; isLoading = false;
@ -108,21 +167,47 @@ class _RulePageState extends State<RulePage> {
} }
Future<void> deleteRule(Map<String, dynamic> rule) async { Future<void> deleteRule(Map<String, dynamic> rule) async {
// Tampilkan dialog konfirmasi
bool? confirm = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Konfirmasi Hapus'),
content: Text('Apakah Anda yakin ingin menghapus rule ini?'),
actions: [
TextButton(
child: Text('Batal'),
onPressed: () => Navigator.of(context).pop(false),
),
TextButton(
child: Text('Hapus', style: TextStyle(color: Colors.red)),
onPressed: () => Navigator.of(context).pop(true),
),
],
);
},
);
if (confirm != true) return;
try { try {
http.Response res; http.Response res;
// Tentukan fungsi delete berdasarkan isi rule // Tentukan fungsi delete berdasarkan isi rule
if (rule['id_hama'] != null) { if (rule['id_hama'] != null) {
res = await ApiService.deleteRuleHama(rule['id']); // Fungsi API untuk delete hama res = await ApiService.deleteRuleHama(rule['id']);
} else if (rule['id_penyakit'] != null) { } else if (rule['id_penyakit'] != null) {
res = await ApiService.deleteRulePenyakit(rule['id']); // Fungsi API untuk delete penyakit res = await ApiService.deleteRulePenyakit(rule['id']);
} else { } else {
throw Exception("Data rule tidak valid (tidak ada id_hama atau id_penyakit)"); throw Exception("Data rule tidak valid");
} }
if (res.statusCode == 200) { if (res.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Rule berhasil dihapus")) SnackBar(
content: Text("Rule berhasil dihapus"),
backgroundColor: Colors.green,
),
); );
fetchRules(); // Refresh data setelah delete fetchRules(); // Refresh data setelah delete
} else { } else {
@ -130,7 +215,10 @@ class _RulePageState extends State<RulePage> {
} }
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Terjadi kesalahan saat menghapus: $e")), SnackBar(
content: Text("Terjadi kesalahan saat menghapus: $e"),
backgroundColor: Colors.red,
),
); );
} }
} }
@ -138,27 +226,80 @@ class _RulePageState extends State<RulePage> {
// Get paginated data // Get paginated data
List<dynamic> get paginatedRules { List<dynamic> get paginatedRules {
final startIndex = currentPage * rowsPerPage; final startIndex = currentPage * rowsPerPage;
final endIndex = startIndex + rowsPerPage > rules.length ? rules.length : startIndex + rowsPerPage; final endIndex = startIndex + rowsPerPage > filteredRules.length
? filteredRules.length
: startIndex + rowsPerPage;
if (startIndex >= rules.length) { if (startIndex >= filteredRules.length) {
return []; return [];
} }
return rules.sublist(startIndex, endIndex); return filteredRules.sublist(startIndex, endIndex);
} }
@override Widget _buildSearchAndFilter() {
Widget build(BuildContext context) { return Card(
return Scaffold( elevation: 2,
appBar: AppBar(title: const Text('Data Rules'), backgroundColor: Color(0xFF9DC08D)), child: Padding(
body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
// Button untuk tambah rule hama Expanded(
flex: 2,
child: TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'Cari berdasarkan nama penyakit, hama, atau gejala...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
),
),
SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField<String>(
value: selectedFilter,
decoration: InputDecoration(
labelText: 'Filter Kategori',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
items: ['Semua', 'Penyakit', 'Hama'].map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
selectedFilter = newValue!;
_filterRules();
});
},
),
),
],
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total: ${filteredRules.length} rule(s)',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey[600],
),
),
Row(
children: [
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
@ -166,31 +307,27 @@ class _RulePageState extends State<RulePage> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => TambahRulePage( builder: (context) => TambahRulePage(
isEditing: false, isEditing: false,
isEditingHama: true, // Menandakan ini adalah rule hama isEditingHama: true,
selectedRuleIds: [], selectedRuleIds: [],
selectedGejalaIds: [], selectedGejalaIds: [],
nilaiPakarList: [], nilaiPakarList: [],
selectedHamaId: null, selectedHamaId: null,
selectedPenyakitId: null, selectedPenyakitId: null,
showHamaOnly: true, // Parameter baru untuk menampilkan hanya dropdown hama showHamaOnly: true,
), ),
), ),
).then((_) => fetchRules()); ).then((_) => fetchRules());
}, },
icon: Icon(Icons.bug_report, size: 16,), icon: Icon(Icons.bug_report, size: 16),
label: Text( label: Text("Rule Hama", style: TextStyle(fontSize: 12)),
"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 padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
minimumSize: Size(0, 32), // Tinggi minimum lebih kecil minimumSize: Size(0, 36),
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap
), ),
), ),
SizedBox(width: 10), SizedBox(width: 8),
// Button untuk tambah rule penyakit
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
@ -198,85 +335,194 @@ class _RulePageState extends State<RulePage> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => TambahRulePage( builder: (context) => TambahRulePage(
isEditing: false, isEditing: false,
isEditingHama: false, // Menandakan ini adalah rule penyakit isEditingHama: false,
selectedRuleIds: [], selectedRuleIds: [],
selectedGejalaIds: [], selectedGejalaIds: [],
nilaiPakarList: [], nilaiPakarList: [],
selectedHamaId: null, selectedHamaId: null,
selectedPenyakitId: null, selectedPenyakitId: null,
showPenyakitOnly: true, // Parameter baru untuk menampilkan hanya dropdown penyakit showPenyakitOnly: true,
), ),
), ),
).then((_) => fetchRules()); ).then((_) => fetchRules());
}, },
icon: Icon(Icons.healing, size: 16,), icon: Icon(Icons.healing, size: 16),
label: Text( label: Text("Rule Penyakit", style: TextStyle(fontSize: 12)),
"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 padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
minimumSize: Size(0, 32), // Tinggi minimum lebih kecil minimumSize: Size(0, 36),
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap
), ),
), ),
], ],
), ),
const SizedBox(height: 16), ],
isLoading ),
? const Center(child: CircularProgressIndicator()) ],
: Expanded( ),
),
);
}
Widget _buildDataList() {
if (paginatedRules.isEmpty) {
return Container(
padding: EdgeInsets.all(32),
child: Column( child: Column(
children: [ children: [
Expanded( Icon(Icons.inbox, size: 64, color: Colors.grey[400]),
child: SingleChildScrollView( SizedBox(height: 16),
scrollDirection: Axis.horizontal, Text(
child: SingleChildScrollView( 'Tidak ada data rule',
scrollDirection: Axis.vertical, style: TextStyle(
child: DataTable( fontSize: 16,
headingRowColor: MaterialStateProperty.resolveWith<Color>( color: Colors.grey[600],
(Set<MaterialState> states) { ),
return Color(0xFF9DC08D); // Apply color to all header rows ),
}, if (searchController.text.isNotEmpty || selectedFilter != 'Semua')
TextButton(
onPressed: () {
setState(() {
searchController.clear();
selectedFilter = 'Semua';
_filterRules();
});
},
child: Text('Reset Filter'),
), ),
columns: const [
DataColumn(label: Text('No')),
DataColumn(label: Text('Hama & Penyakit')),
DataColumn(label: Text('Gejala')),
DataColumn(label: Text('Nilai Pakar')),
DataColumn(label: Text('Aksi')),
], ],
rows: List.generate(paginatedRules.length, (index) { ),
);
}
return ListView.builder(
itemCount: paginatedRules.length,
itemBuilder: (context, index) {
final rule = paginatedRules[index]; final rule = paginatedRules[index];
final displayIndex = currentPage * rowsPerPage + index + 1;
final namaKategori = rule['id_penyakit'] != null final namaKategori = rule['id_penyakit'] != null
? rule['nama_penyakit'] ?? '-' ? rule['nama_penyakit'] ?? '-'
: rule['nama_hama'] ?? '-'; : rule['nama_hama'] ?? '-';
final isPenyakit = rule['id_penyakit'] != null; final kategori = rule['type'] ?? 'Unknown';
final isHama = rule['id_hama'] != null;
return DataRow( return Container(
cells: [ margin: EdgeInsets.only(bottom: 12),
DataCell(Text(displayIndex.toString())), child: Row(
DataCell(Text(namaKategori)), children: [
DataCell(Text(rule['nama_gejala'] ?? '-')), // Card dengan data rule
DataCell(Text(rule['nilai_pakar']?.toString() ?? '-')), Expanded(
DataCell( child: Card(
elevation: 2,
child: InkWell(
onTap: () => _navigateToEdit(rule),
child: Container(
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Kategori badge dan nama
Row( Row(
children: [ children: [
IconButton( Container(
icon: const Icon( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
Icons.edit, decoration: BoxDecoration(
color: Colors.orange, color: isHama ? Colors.green[100] : Colors.blue[100],
borderRadius: BorderRadius.circular(12),
), ),
onPressed: () { child: Text(
kategori,
style: TextStyle(
color: isHama ? Colors.green[800] : Colors.blue[800],
fontWeight: FontWeight.w500,
fontSize: 10,
),
),
),
SizedBox(width: 8),
Expanded(
child: Text(
namaKategori,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
SizedBox(height: 8),
// Gejala
Text(
'Gejala: ${rule['nama_gejala'] ?? '-'}',
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4),
// Nilai pakar
Row(
children: [
Text(
'Nilai Pakar: ',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Color(0xFF9DC08D),
borderRadius: BorderRadius.circular(8),
),
child: Text(
rule['nilai_pakar']?.toString() ?? '-',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xFFFFFFFF),
fontSize: 12,
),
),
),
],
),
],
),
),
),
),
),
SizedBox(width: 8),
// Button hapus di luar card
Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
icon: Icon(Icons.delete, color: Colors.white),
onPressed: () => deleteRule(rule),
),
),
],
),
);
},
);
}
void _navigateToEdit(Map<String, dynamic> rule) {
if (rule != null && if (rule != null &&
rule['id'] != null && rule['id'] != null &&
rule['id_gejala'] != null && rule['id_gejala'] != null &&
rule['nilai_pakar'] != null) { rule['nilai_pakar'] != null) {
// Tentukan jenis rule untuk editing
final bool editingHama = rule['id_hama'] != null; final bool editingHama = rule['id_hama'] != null;
Navigator.push( Navigator.push(
@ -290,66 +536,104 @@ class _RulePageState extends State<RulePage> {
nilaiPakarList: [(rule['nilai_pakar'] as num).toDouble()], nilaiPakarList: [(rule['nilai_pakar'] as num).toDouble()],
selectedHamaId: rule['id_hama'] as int?, selectedHamaId: rule['id_hama'] as int?,
selectedPenyakitId: rule['id_penyakit'] as int?, selectedPenyakitId: rule['id_penyakit'] as int?,
// Tambahkan parameter untuk menentukan dropdown yang ditampilkan
showHamaOnly: editingHama, showHamaOnly: editingHama,
showPenyakitOnly: !editingHama, showPenyakitOnly: !editingHama,
), ),
), ),
).then((_) => fetchRules()); ).then((_) => fetchRules());
} else { } else {
// Tampilkan pesan error jika data rule tidak lengkap
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text("Data rule tidak lengkap atau tidak valid"), content: Text("Data rule tidak lengkap"),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
// Debug info
print("Rule data: $rule");
} }
}, }
),
IconButton( Widget _buildPaginationControls() {
icon: const Icon( final totalPages = (filteredRules.length / rowsPerPage).ceil();
Icons.delete,
color: Colors.red, if (totalPages <= 1) return SizedBox.shrink();
),
onPressed: () { return Card(
deleteRule(rule); elevation: 1,
}, child: Container(
), padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
],
),
),
],
);
}),
),
),
),
),
// Pagination controls
Container(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(
'Menampilkan ${(currentPage * rowsPerPage) + 1} - ${((currentPage + 1) * rowsPerPage > filteredRules.length) ? filteredRules.length : (currentPage + 1) * rowsPerPage} dari ${filteredRules.length}',
style: TextStyle(color: Colors.grey[600]),
),
Row(
children: [
IconButton(
icon: Icon(Icons.first_page),
onPressed: currentPage > 0
? () => setState(() => currentPage = 0)
: null,
),
IconButton( IconButton(
icon: Icon(Icons.chevron_left), icon: Icon(Icons.chevron_left),
onPressed: currentPage > 0 onPressed: currentPage > 0
? () => setState(() => currentPage--) ? () => setState(() => currentPage--)
: null, : null,
), ),
Text('Halaman ${currentPage + 1} dari ${(rules.length / rowsPerPage).ceil()}'), Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Text('${currentPage + 1} / $totalPages'),
),
IconButton( IconButton(
icon: Icon(Icons.chevron_right), icon: Icon(Icons.chevron_right),
onPressed: (currentPage + 1) * rowsPerPage < rules.length onPressed: (currentPage + 1) * rowsPerPage < filteredRules.length
? () => setState(() => currentPage++) ? () => setState(() => currentPage++)
: null, : null,
), ),
IconButton(
icon: Icon(Icons.last_page),
onPressed: (currentPage + 1) * rowsPerPage < filteredRules.length
? () => setState(() => currentPage = totalPages - 1)
: null,
),
], ],
), ),
],
), ),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Halaman Aturan'),
backgroundColor: Color(0xFF9DC08D),
elevation: 0,
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: fetchRules,
tooltip: 'Refresh Data',
),
],
),
body: isLoading
? Center(child: CircularProgressIndicator())
: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_buildSearchAndFilter(),
SizedBox(height: 16),
Expanded(
child: Column(
children: [
Expanded(child: _buildDataList()),
SizedBox(height: 8),
_buildPaginationControls(),
], ],
), ),
), ),

View File

@ -42,33 +42,40 @@ class _TambahHamaPageState extends State<TambahHamaPage> {
deskripsiController.text.isNotEmpty && deskripsiController.text.isNotEmpty &&
penangananController.text.isNotEmpty) { penangananController.text.isNotEmpty) {
try { try {
double? nilaipakar; // Kirim nilai pakar sebagai double, 0.0 jika tidak diisi
if (nilaiPakarController.text.isNotEmpty) { double nilaiPakar = 0.0;
if (nilaiPakarController.text.trim().isNotEmpty) {
String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); String nilaiInput = nilaiPakarController.text.replaceAll(',', '.');
nilaipakar = double.parse(nilaiInput); nilaiPakar = double.parse(nilaiInput);
} }
print("Debug - Nilai Pakar Double: $nilaiPakar");
print("Debug - Nama: ${namaController.text}");
print("Debug - Deskripsi: ${deskripsiController.text}");
print("Debug - Penanganan: ${penangananController.text}");
print("Debug - Nilai Pakar: $nilaiPakar");
print("Debug - Image File: $_pickedFile");
await apiService.createHama( await apiService.createHama(
namaController.text, namaController.text,
deskripsiController.text, deskripsiController.text,
penangananController.text, penangananController.text,
_pickedFile, _pickedFile,
nilaipakar, // boleh null nilaiPakar,
); );
widget.onHamaAdded(); widget.onHamaAdded();
Navigator.pop(context); Navigator.pop(context);
_showDialog('Berhasil', 'Data hama berhasil ditambahkan.'); _showDialog('Berhasil', 'Data hama berhasil ditambahkan.');
} catch (e) { } catch (e) {
_showDialog('Gagal', 'Gagal menambahkan data hama.'); _showDialog('Gagal', 'Gagal menambahkan data hama: $e');
print("Error adding hama: $e"); print("Error adding hama: $e");
} }
} else { } else {
_showDialog('Error', 'Semua field harus diisi (kecuali nilai pakar).'); _showDialog('Error', 'Nama, deskripsi, dan penanganan hama harus diisi.');
} }
} }
void _showDialog(String title, String message) { void _showDialog(String title, String message) {
showDialog( showDialog(
context: context, context: context,
@ -149,8 +156,11 @@ class _TambahHamaPageState extends State<TambahHamaPage> {
SizedBox(height: 15), SizedBox(height: 15),
// TextField( // TextField(
// controller: nilaiPakarController, // controller: nilaiPakarController,
// decoration: InputDecoration(labelText: 'Nilai Pakar'), // decoration: InputDecoration(
// maxLines: 3, // labelText: 'Nilai Pakar (Optional)',
// hintText: 'Masukkan nilai pakar (opsional)',
// ),
// keyboardType: TextInputType.numberWithOptions(decimal: true),
// ), // ),
SizedBox(height: 15), SizedBox(height: 15),
Text('Foto'), Text('Foto'),

View File

@ -39,11 +39,23 @@ class _TambahPenyakitPageState extends State<TambahPenyakitPage> {
Future<void> _simpanPenyakit() async { Future<void> _simpanPenyakit() async {
if (namaController.text.isNotEmpty && if (namaController.text.isNotEmpty &&
deskripsiController.text.isNotEmpty && deskripsiController.text.isNotEmpty &&
penangananController.text.isNotEmpty && penangananController.text.isNotEmpty) {
nilaiPakarController.text.isNotEmpty) {
try { try {
// Kirim nilai pakar sebagai double, 0.0 jika tidak diisi
double nilaiPakar = 0.0;
if (nilaiPakarController.text.trim().isNotEmpty) {
String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); String nilaiInput = nilaiPakarController.text.replaceAll(',', '.');
double nilaiPakar = double.parse(nilaiInput); nilaiPakar = double.parse(nilaiInput);
}
print("Debug - Nilai Pakar Double: $nilaiPakar");
print("Debug - Nama: ${namaController.text}");
print("Debug - Deskripsi: ${deskripsiController.text}");
print("Debug - Penanganan: ${penangananController.text}");
print("Debug - Nilai Pakar: $nilaiPakar");
print("Debug - Image File: $_pickedFile");
await apiService.createPenyakit( await apiService.createPenyakit(
namaController.text, namaController.text,
deskripsiController.text, deskripsiController.text,
@ -53,13 +65,13 @@ class _TambahPenyakitPageState extends State<TambahPenyakitPage> {
); );
widget.onPenyakitAdded(); widget.onPenyakitAdded();
Navigator.pop(context); Navigator.pop(context);
_showDialog('Berhasil', 'Data penyakit berhasil ditambahkan.'); _showDialog('Berhasil', 'Data hama berhasil ditambahkan.');
} catch (e) { } catch (e) {
_showDialog('Gagal', 'Gagal menambahkan data penyakit.'); _showDialog('Gagal', 'Gagal menambahkan data hama: $e');
print("Error adding penyakit: $e"); print("Error adding hama: $e");
} }
} else { } else {
_showDialog('Error', 'Semua field harus diisi.'); _showDialog('Error', 'Nama, deskripsi, dan penanganan hama harus diisi.');
} }
} }
@ -141,11 +153,11 @@ class _TambahPenyakitPageState extends State<TambahPenyakitPage> {
maxLines: 3, maxLines: 3,
), ),
SizedBox(height: 15), SizedBox(height: 15),
TextField( // TextField(
controller: nilaiPakarController, // controller: nilaiPakarController,
decoration: InputDecoration(labelText: 'nilai pakar'), // decoration: InputDecoration(labelText: 'nilai pakar'),
maxLines: 3, // maxLines: 3,
), // ),
SizedBox(height: 15), SizedBox(height: 15),
(_webImage != null) (_webImage != null)
? Image.memory( ? Image.memory(

View File

@ -9,6 +9,7 @@ class UserListPage extends StatefulWidget {
class _UserListPageState extends State<UserListPage> { class _UserListPageState extends State<UserListPage> {
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
List<Map<String, dynamic>> users = []; List<Map<String, dynamic>> users = [];
List<Map<String, dynamic>> filteredUsers = [];
bool isLoading = true; bool isLoading = true;
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
@ -17,6 +18,7 @@ class _UserListPageState extends State<UserListPage> {
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
final _alamatController = TextEditingController(); final _alamatController = TextEditingController();
final _nomorTeleponController = TextEditingController(); final _nomorTeleponController = TextEditingController();
final _searchController = TextEditingController();
@override @override
void initState() { void initState() {
@ -26,12 +28,12 @@ class _UserListPageState extends State<UserListPage> {
@override @override
void dispose() { void dispose() {
// Dispose controllers in dispose method
_nameController.dispose(); _nameController.dispose();
_emailController.dispose(); _emailController.dispose();
_passwordController.dispose(); _passwordController.dispose();
_alamatController.dispose(); _alamatController.dispose();
_nomorTeleponController.dispose(); _nomorTeleponController.dispose();
_searchController.dispose();
super.dispose(); super.dispose();
} }
@ -45,17 +47,14 @@ class _UserListPageState extends State<UserListPage> {
nomorTelepon: _nomorTeleponController.text, nomorTelepon: _nomorTeleponController.text,
); );
// Clear form
_nameController.clear(); _nameController.clear();
_emailController.clear(); _emailController.clear();
_passwordController.clear(); _passwordController.clear();
_alamatController.clear(); _alamatController.clear();
_nomorTeleponController.clear(); _nomorTeleponController.clear();
// Refresh user list
await _loadUsers(); await _loadUsers();
// Show success message
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('User berhasil ditambahkan'), content: Text('User berhasil ditambahkan'),
@ -74,7 +73,6 @@ class _UserListPageState extends State<UserListPage> {
Future<void> _updateUser(Map<String, dynamic> user) async { Future<void> _updateUser(Map<String, dynamic> user) async {
try { try {
// Hanya kirim password jika diisi
String? newPassword = String? newPassword =
_passwordController.text.isEmpty ? null : _passwordController.text; _passwordController.text.isEmpty ? null : _passwordController.text;
@ -82,22 +80,19 @@ class _UserListPageState extends State<UserListPage> {
id: user['id'], id: user['id'],
name: _nameController.text, name: _nameController.text,
email: _emailController.text, email: _emailController.text,
password: newPassword, // Kirim null jika password kosong password: newPassword,
alamat: _alamatController.text, alamat: _alamatController.text,
nomorTelepon: _nomorTeleponController.text, nomorTelepon: _nomorTeleponController.text,
); );
// Clear form
_nameController.clear(); _nameController.clear();
_emailController.clear(); _emailController.clear();
_passwordController.clear(); _passwordController.clear();
_alamatController.clear(); _alamatController.clear();
_nomorTeleponController.clear(); _nomorTeleponController.clear();
// Refresh user list
await _loadUsers(); await _loadUsers();
// Show success message with password info
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
@ -118,31 +113,10 @@ class _UserListPageState extends State<UserListPage> {
} }
} }
Future<void> _deleteUser(int userId) async {
try {
await apiService.deleteUser(userId);
await _loadUsers();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('User berhasil dihapus'),
backgroundColor: Colors.green,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal menghapus user: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
void _showDeleteConfirmation(Map<String, dynamic> user) { void _showDeleteConfirmation(Map<String, dynamic> user) {
showDialog( showDialog(
context: context, context: context,
builder: builder: (context) => AlertDialog(
(context) => AlertDialog(
title: Text('Konfirmasi Hapus'), title: Text('Konfirmasi Hapus'),
content: Text( content: Text(
'Apakah Anda yakin ingin menghapus user ${user['name']}?', 'Apakah Anda yakin ingin menghapus user ${user['name']}?',
@ -155,9 +129,9 @@ class _UserListPageState extends State<UserListPage> {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
try { try {
Navigator.pop(context); // Close dialog first Navigator.pop(context);
await apiService.deleteUser(user['id']); await apiService.deleteUser(user['id']);
await _loadUsers(); // Refresh the list await _loadUsers();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@ -183,17 +157,15 @@ class _UserListPageState extends State<UserListPage> {
} }
void _showUpdateDialog(Map<String, dynamic> user) { void _showUpdateDialog(Map<String, dynamic> user) {
// Pre-fill form with existing user data
_nameController.text = user['name'] ?? ''; _nameController.text = user['name'] ?? '';
_emailController.text = user['email'] ?? ''; _emailController.text = user['email'] ?? '';
_alamatController.text = user['alamat'] ?? ''; _alamatController.text = user['alamat'] ?? '';
_nomorTeleponController.text = user['nomorTelepon'] ?? ''; _nomorTeleponController.text = user['nomorTelepon'] ?? '';
_passwordController.text = ''; // Empty for security _passwordController.text = '';
showDialog( showDialog(
context: context, context: context,
builder: builder: (context) => AlertDialog(
(context) => AlertDialog(
title: Text('Update User'), title: Text('Update User'),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Form( child: Form(
@ -204,18 +176,14 @@ class _UserListPageState extends State<UserListPage> {
TextFormField( TextFormField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration(labelText: 'Nama'), decoration: InputDecoration(labelText: 'Nama'),
validator: validator: (value) =>
(value) => value?.isEmpty ?? true ? 'Nama tidak boleh kosong' : null,
value?.isEmpty ?? true
? 'Nama tidak boleh kosong'
: null,
), ),
TextFormField( TextFormField(
controller: _emailController, controller: _emailController,
decoration: InputDecoration(labelText: 'Email'), decoration: InputDecoration(labelText: 'Email'),
validator: (value) { validator: (value) {
if (value?.isEmpty ?? true) if (value?.isEmpty ?? true) return 'Email tidak boleh kosong';
return 'Email tidak boleh kosong';
if (!value!.contains('@')) return 'Email tidak valid'; if (!value!.contains('@')) return 'Email tidak valid';
return null; return null;
}, },
@ -224,14 +192,12 @@ class _UserListPageState extends State<UserListPage> {
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',
), ),
obscureText: true, obscureText: true,
validator: (value) { validator: (value) {
if (value?.isNotEmpty ?? false) { if (value?.isNotEmpty ?? false) {
if (value!.length < 6) if (value!.length < 6) return 'Password minimal 6 karakter';
return 'Password minimal 6 karakter';
} }
return null; return null;
}, },
@ -239,21 +205,15 @@ class _UserListPageState extends State<UserListPage> {
TextFormField( TextFormField(
controller: _alamatController, controller: _alamatController,
decoration: InputDecoration(labelText: 'Alamat'), decoration: InputDecoration(labelText: 'Alamat'),
validator: validator: (value) =>
(value) => value?.isEmpty ?? true ? 'Alamat tidak boleh kosong' : null,
value?.isEmpty ?? true
? 'Alamat tidak boleh kosong'
: null,
), ),
TextFormField( TextFormField(
controller: _nomorTeleponController, controller: _nomorTeleponController,
decoration: InputDecoration(labelText: 'Nomor Telepon'), decoration: InputDecoration(labelText: 'Nomor Telepon'),
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
validator: validator: (value) =>
(value) => value?.isEmpty ?? true ? 'Nomor telepon tidak boleh kosong' : null,
value?.isEmpty ?? true
? 'Nomor telepon tidak boleh kosong'
: null,
), ),
], ],
), ),
@ -282,10 +242,15 @@ class _UserListPageState extends State<UserListPage> {
} }
void _showAddUserDialog() { void _showAddUserDialog() {
_nameController.clear();
_emailController.clear();
_passwordController.clear();
_alamatController.clear();
_nomorTeleponController.clear();
showDialog( showDialog(
context: context, context: context,
builder: builder: (context) => AlertDialog(
(context) => AlertDialog(
title: Text('Tambah User Baru'), title: Text('Tambah User Baru'),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Form( child: Form(
@ -382,6 +347,7 @@ class _UserListPageState extends State<UserListPage> {
final userList = await apiService.getUsers(); final userList = await apiService.getUsers();
setState(() { setState(() {
users = userList; users = userList;
filteredUsers = userList;
isLoading = false; isLoading = false;
}); });
} catch (e) { } catch (e) {
@ -392,6 +358,35 @@ class _UserListPageState extends State<UserListPage> {
} }
} }
void _filterUsers(String query) {
setState(() {
if (query.isEmpty) {
filteredUsers = users;
} else {
filteredUsers = users.where((user) {
final name = user['name']?.toString().toLowerCase() ?? '';
final role = user['role']?.toString().toLowerCase() ?? '';
final searchQuery = query.toLowerCase();
return name.contains(searchQuery) || role.contains(searchQuery);
}).toList();
}
});
}
void _navigateToUserDetail(Map<String, dynamic> user) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserDetailPage(
user: user,
onUserUpdated: _loadUsers,
onUserDeleted: _loadUsers,
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -404,77 +399,475 @@ class _UserListPageState extends State<UserListPage> {
backgroundColor: Color(0xFF9DC08D), backgroundColor: Color(0xFF9DC08D),
child: Icon(Icons.add), child: Icon(Icons.add),
), ),
body: body: isLoading
isLoading
? Center(child: CircularProgressIndicator()) ? Center(child: CircularProgressIndicator())
: SingleChildScrollView( : Column(
scrollDirection: Axis.horizontal, children: [
child: SingleChildScrollView( // Search Bar
child: DataTable(
columnSpacing: 20,
columns: [
DataColumn(label: Text('Nama')),
DataColumn(label: Text('Email')),
DataColumn(label: Text('Alamat')),
// DataColumn(label: Text('No. Telepon')),
DataColumn(label: Text('Role')),
DataColumn(label: Text('Aksi')),
],
rows:
users.map((user) {
return DataRow(
cells: [
DataCell(Text(user['name'] ?? '-')),
DataCell(Text(user['email'] ?? '-')),
DataCell(Text(user['alamat'] ?? '-')),
// DataCell(Text(user['nomorTelepon'] ?? '-')),
DataCell(
Container( Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.all(16),
horizontal: 8, child: TextField(
vertical: 4, controller: _searchController,
onChanged: _filterUsers,
decoration: InputDecoration(
hintText: 'Cari berdasarkan nama atau role...',
prefixIcon: Icon(Icons.search, color: Color(0xFF9DC08D)),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: Icon(Icons.clear, color: Colors.grey),
onPressed: () {
_searchController.clear();
_filterUsers('');
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
), ),
decoration: BoxDecoration( focusedBorder: OutlineInputBorder(
color: borderRadius: BorderRadius.circular(12),
user['role'] == 'admin' borderSide: BorderSide(color: Color(0xFF9DC08D), width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
filled: true,
fillColor: Colors.grey[50],
),
),
),
// User List
Expanded(
child: filteredUsers.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_searchController.text.isNotEmpty
? Icons.search_off
: Icons.people_outline,
size: 64,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
_searchController.text.isNotEmpty
? 'Tidak ada pengguna yang ditemukan'
: 'Tidak ada pengguna',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
if (_searchController.text.isNotEmpty) ...[
SizedBox(height: 8),
Text(
'Coba kata kunci lain',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
],
),
)
: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: filteredUsers.length,
itemBuilder: (context, index) {
final user = filteredUsers[index];
return Card(
elevation: 2,
margin: EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundColor: user['role'] == 'admin'
? Colors.blue.withOpacity(0.2) ? Colors.blue.withOpacity(0.2)
: Colors.green.withOpacity(0.2), : Colors.green.withOpacity(0.2),
child: Icon(
user['role'] == 'admin' ? Icons.admin_panel_settings : Icons.person,
color: user['role'] == 'admin' ? Colors.blue : Colors.green,
),
),
title: Text(
user['name'] ?? 'Nama tidak tersedia',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
subtitle: Container(
margin: EdgeInsets.only(top: 4),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: user['role'] == 'admin'
? Colors.blue.withOpacity(0.1)
: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
user['role'] ?? 'user', user['role'] ?? 'user',
style: TextStyle( style: TextStyle(
color: color: user['role'] == 'admin' ? Colors.blue : Colors.green,
user['role'] == 'admin' fontSize: 12,
? Colors.blue fontWeight: FontWeight.w500,
: Colors.green,
), ),
), ),
), ),
trailing: Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => _navigateToUserDetail(user),
), ),
DataCell( );
Row( },
),
),
],
),
);
}
}
// Halaman Detail User
class UserDetailPage extends StatelessWidget {
final Map<String, dynamic> user;
final VoidCallback onUserUpdated;
final VoidCallback onUserDeleted;
const UserDetailPage({
Key? key,
required this.user,
required this.onUserUpdated,
required this.onUserDeleted,
}) : super(key: key);
void _showDeleteConfirmation(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Konfirmasi Hapus'),
content: Text(
'Apakah Anda yakin ingin menghapus user ${user['name']}?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Batal'),
),
ElevatedButton(
onPressed: () async {
try {
Navigator.pop(context); // Close dialog
final apiService = ApiService();
await apiService.deleteUser(user['id']);
Navigator.pop(context); // Go back to list
onUserDeleted(); // Refresh list
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('User berhasil dihapus'),
backgroundColor: Colors.green,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal menghapus user: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: Text('Hapus'),
),
],
),
);
}
void _showUpdateDialog(BuildContext context) {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController(text: user['name'] ?? '');
final _emailController = TextEditingController(text: user['email'] ?? '');
final _alamatController = TextEditingController(text: user['alamat'] ?? '');
final _nomorTeleponController = TextEditingController(text: user['nomorTelepon'] ?? '');
final _passwordController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Update User'),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(labelText: 'Nama'),
validator: (value) =>
value?.isEmpty ?? true ? 'Nama tidak boleh kosong' : null,
),
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value?.isEmpty ?? true) return 'Email tidak boleh kosong';
if (!value!.contains('@')) return 'Email tidak valid';
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password Baru',
helperText: 'Kosongkan jika tidak ingin mengubah password',
),
obscureText: true,
validator: (value) {
if (value?.isNotEmpty ?? false) {
if (value!.length < 6) return 'Password minimal 6 karakter';
}
return null;
},
),
TextFormField(
controller: _alamatController,
decoration: InputDecoration(labelText: 'Alamat'),
validator: (value) =>
value?.isEmpty ?? true ? 'Alamat tidak boleh kosong' : null,
),
TextFormField(
controller: _nomorTeleponController,
decoration: InputDecoration(labelText: 'Nomor Telepon'),
keyboardType: TextInputType.phone,
validator: (value) =>
value?.isEmpty ?? true ? 'Nomor telepon tidak boleh kosong' : null,
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Batal'),
),
ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
try {
Navigator.pop(context);
String? newPassword = _passwordController.text.isEmpty
? null
: _passwordController.text;
final apiService = ApiService();
await apiService.updateUser(
id: user['id'],
name: _nameController.text,
email: _emailController.text,
password: newPassword,
alamat: _alamatController.text,
nomorTelepon: _nomorTeleponController.text,
);
Navigator.pop(context); // Go back to list
onUserUpdated(); // Refresh list
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
newPassword != null
? 'User berhasil diperbarui termasuk password'
: 'User berhasil diperbarui',
),
backgroundColor: Colors.green,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal memperbarui user: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF9DC08D),
),
child: Text('Update'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Detail Pengguna'),
backgroundColor: Color(0xFF9DC08D),
actions: [
IconButton( IconButton(
icon: Icon(Icons.edit), icon: Icon(Icons.edit),
onPressed: () => _showUpdateDialog(user), onPressed: () => _showUpdateDialog(context),
), ),
IconButton( IconButton(
icon: Icon( icon: Icon(Icons.delete, color: Colors.red),
Icons.delete, onPressed: () => _showDeleteConfirmation(context),
color: Colors.red, ),
],
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Profile Header
Container(
width: double.infinity,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Color(0xFF9DC08D).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: user['role'] == 'admin'
? Colors.blue.withOpacity(0.2)
: Colors.green.withOpacity(0.2),
child: Icon(
user['role'] == 'admin'
? Icons.admin_panel_settings
: Icons.person,
size: 40,
color: user['role'] == 'admin' ? Colors.blue : Colors.green,
),
),
SizedBox(height: 12),
Text(
user['name'] ?? 'Nama tidak tersedia',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: user['role'] == 'admin'
? Colors.blue.withOpacity(0.2)
: Colors.green.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
),
child: Text(
user['role'] ?? 'user',
style: TextStyle(
color: user['role'] == 'admin' ? Colors.blue : Colors.green,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
SizedBox(height: 24),
// Detail Information
Text(
'Informasi Detail',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
SizedBox(height: 16),
_buildDetailItem(
icon: Icons.email,
title: 'Email',
value: user['email'] ?? 'Email tidak tersedia',
),
_buildDetailItem(
icon: Icons.location_on,
title: 'Alamat',
value: user['alamat'] ?? 'Alamat tidak tersedia',
),
_buildDetailItem(
icon: Icons.phone,
title: 'Nomor Telepon',
value: user['nomorTelepon'] ?? 'Nomor telepon tidak tersedia',
),
],
),
),
);
}
Widget _buildDetailItem({
required IconData icon,
required String title,
required String value,
}) {
return Container(
margin: EdgeInsets.only(bottom: 16),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
children: [
Icon(
icon,
color: Color(0xFF9DC08D),
size: 24,
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
), ),
onPressed:
() => _showDeleteConfirmation(user),
), ),
], ],
), ),
), ),
], ],
);
}).toList(),
),
),
), ),
); );
} }

View File

@ -899,7 +899,7 @@ Future<List<Map<String, dynamic>>> getAllHistori() async {
String deskripsi, String deskripsi,
String penanganan, String penanganan,
XFile? pickedFile, XFile? pickedFile,
double nilai_pakar double? nilai_pakar
) async { ) async {
try { try {
var uri = Uri.parse(penyakitUrl); var uri = Uri.parse(penyakitUrl);

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:SIBAYAM/user/before_login.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -62,7 +63,9 @@ class _LoginPageState extends State<LoginPage> {
} }
try { try {
var url = Uri.parse("https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/auth/login"); var url = Uri.parse(
"https://beckend-sistem-pakar-diagnosa-penyakit.onrender.com/api/auth/login",
);
var response = await http.post( var response = await http.post(
url, url,
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
@ -120,7 +123,10 @@ class _LoginPageState extends State<LoginPage> {
child: IconButton( child: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white), icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => BeforeLogin()),
);
}, },
), ),
), ),