From db83fb1c2fc7f13ab7b97b186e630b072a49427c Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 14 Jun 2025 23:11:05 +0700 Subject: [PATCH] update layout admin for get data --- backend/app.js | 2 - backend/swagger.js | 2 +- frontend/lib/admin/admin_histori_page.dart | 738 ++++++++++---- frontend/lib/admin/admin_page.dart | 45 +- frontend/lib/admin/edit_hama_page.dart | 36 +- frontend/lib/admin/edit_penyakit_page.dart | 36 +- frontend/lib/admin/gejala_page.dart | 414 ++++---- frontend/lib/admin/hama_page.dart | 424 ++++---- frontend/lib/admin/penyakit_page.dart | 422 ++++---- frontend/lib/admin/rule_page.dart | 694 +++++++++---- frontend/lib/admin/tambah_hama_page.dart | 70 +- frontend/lib/admin/tambah_penyakit_page.dart | 40 +- frontend/lib/admin/user_list_page.dart | 989 +++++++++++++------ frontend/lib/api_services/api_services.dart | 2 +- frontend/lib/user/login_page.dart | 10 +- 15 files changed, 2569 insertions(+), 1355 deletions(-) diff --git a/backend/app.js b/backend/app.js index f4afd1e..c2b9022 100644 --- a/backend/app.js +++ b/backend/app.js @@ -19,8 +19,6 @@ dotenv.config(); const app = express(); - - // Middlewares app.use(express.json()); app.use(cors()); diff --git a/backend/swagger.js b/backend/swagger.js index 3547917..717921c 100644 --- a/backend/swagger.js +++ b/backend/swagger.js @@ -11,7 +11,7 @@ const swaggerOptions = { }, servers: [ { - url: 'https://backend-sistem-pakar-diagnosa-penya.vercel.app', + url: 'https://localhost:5000', // Development server URL description: 'Production Server' }, ], diff --git a/frontend/lib/admin/admin_histori_page.dart b/frontend/lib/admin/admin_histori_page.dart index 951b399..ac45f2a 100644 --- a/frontend/lib/admin/admin_histori_page.dart +++ b/frontend/lib/admin/admin_histori_page.dart @@ -11,9 +11,14 @@ class _AdminHistoriPageState extends State { final ApiService apiService = ApiService(); List> historiData = []; List> groupedHistoriData = []; + List> filteredHistoriData = []; // Data yang sudah difilter bool isLoading = true; String? error; + // Search variables + TextEditingController searchController = TextEditingController(); + String searchQuery = ''; + // Pagination variables int _rowsPerPage = 10; int _currentPage = 0; @@ -24,6 +29,35 @@ class _AdminHistoriPageState extends State { void initState() { super.initState(); _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 _loadHistoriData() async { @@ -34,7 +68,9 @@ class _AdminHistoriPageState extends State { }); // 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 uniqueUserIds = allHistori .where((histori) => histori['userId'] != null) .map((histori) => histori['userId'].toString()) @@ -60,6 +96,7 @@ class _AdminHistoriPageState extends State { setState(() { historiData = detailedHistori; // Simpan data asli jika perlu groupedHistoriData = groupedData; // Data yang sudah dikelompokkan + filteredHistoriData = List.from(groupedData); // Initialize filtered data _updatePagination(0); // Set halaman pertama isLoading = false; }); @@ -104,8 +141,8 @@ class _AdminHistoriPageState extends State { diagnosa = 'Tidak ada diagnosa'; } - // Ambil nama user dari kolom 'nama' atau 'name' (sesuaikan dengan struktur data Anda) - String userName = item['name']?.toString() ?? 'User ID: ${item['userId']}'; + // Ambil nama user dari kolom 'nama' atau 'name' + String userName = item['name']?.toString() ?? 'User ID: ${item['userId']}'; // Buat composite key: userId + waktu + diagnosa String key = '${item['userId']}_${formattedTime}_$diagnosa'; @@ -116,7 +153,7 @@ class _AdminHistoriPageState extends State { groupedMap[key] = { 'userId': item['userId'], - 'userName': userName, // Menampilkan nama user, bukan ID + 'userName': userName, 'diagnosa': diagnosa, 'tanggal_diagnosa': item['tanggal_diagnosa'], 'tanggal_display': displayDate, @@ -124,7 +161,8 @@ class _AdminHistoriPageState extends State { 'hasil': item['hasil'], 'penyakit_nama': item['penyakit_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 { !groupedMap[key]!['gejala'].contains(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 @@ -147,22 +188,32 @@ class _AdminHistoriPageState extends State { // Update pagination void _updatePagination(int page) { _currentPage = page; - _totalPages = (groupedHistoriData.length / _rowsPerPage).ceil(); + _totalPages = (filteredHistoriData.length / _rowsPerPage).ceil(); int startIndex = page * _rowsPerPage; int endIndex = (page + 1) * _rowsPerPage; - if (endIndex > groupedHistoriData.length) { - endIndex = groupedHistoriData.length; + if (endIndex > filteredHistoriData.length) { + endIndex = filteredHistoriData.length; } - if (startIndex >= groupedHistoriData.length) { + if (startIndex >= filteredHistoriData.length) { _currentPageData = []; } else { - _currentPageData = groupedHistoriData.sublist(startIndex, endIndex); + _currentPageData = filteredHistoriData.sublist(startIndex, endIndex); } } + // Navigasi ke halaman detail + void _navigateToDetail(Map histori) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DetailHistoriPage(histori: histori), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -170,224 +221,477 @@ class _AdminHistoriPageState extends State { title: Text('Riwayat Diagnosa'), backgroundColor: Color(0xFF9DC08D), ), - body: - isLoading - ? Center(child: CircularProgressIndicator()) - : error != null - ? Center(child: Text('Error: $error')) - : groupedHistoriData.isEmpty - ? Center(child: Text('Tidak ada data riwayat diagnosa')) - : Column( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SingleChildScrollView( - child: DataTable( - columnSpacing: 20, - headingRowColor: MaterialStateProperty.all( - Color(0xFF9DC08D).withOpacity(0.3), + body: isLoading + ? Center(child: CircularProgressIndicator()) + : error != null + ? Center(child: Text('Error: $error')) + : groupedHistoriData.isEmpty + ? Center(child: Text('Tidak ada data riwayat diagnosa')) + : Column( + 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( + child: filteredHistoriData.isEmpty && searchQuery.isNotEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: 64, + color: Colors.grey[400], + ), + SizedBox(height: 16), + Text( + 'Tidak ada hasil untuk "${searchController.text}"', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 8), + Text( + 'Coba gunakan kata kunci yang berbeda', + style: TextStyle( + fontSize: 14, + color: Colors.grey[500], + ), + ), + ], ), - columns: [ - DataColumn( - label: Text( - 'Nama User', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - DataColumn( - label: Text( - 'Gejala', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - 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 - String gejalaText = "Tidak ada gejala"; - if (histori['gejala'] != null && - (histori['gejala'] as List).isNotEmpty) { - gejalaText = (histori['gejala'] as List).join( - ', ', - ); - } - - return DataRow( - cells: [ - DataCell(Text(histori['userName'] ?? 'User tidak ditemukan')), - DataCell( - Container( - constraints: BoxConstraints( - maxWidth: 200, - ), - child: Tooltip( - message: gejalaText, - child: Text( - gejalaText, - overflow: TextOverflow.ellipsis, + ) + : ListView.builder( + itemCount: _currentPageData.length, + itemBuilder: (context, index) { + final histori = _currentPageData[index]; + + return Container( + margin: EdgeInsets.only(bottom: 12, left: 16, right: 16), + child: Row( + children: [ + // Card dengan informasi histori + Expanded( + child: Card( + elevation: 2, + child: InkWell( + onTap: () => _navigateToDetail(histori), + child: Container( + padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Nama User + Text( + histori['userName'] ?? 'User tidak ditemukan', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8), + // Diagnosa + Text( + histori['diagnosa'] ?? 'Tidak ada diagnosa', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 4), + // Tanggal + Text( + histori['tanggal_display'] ?? '', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], ), ), ), ), - DataCell( - Text( - histori['diagnosa'] ?? - 'Tidak ada diagnosa', - style: TextStyle( - - fontWeight: FontWeight.w500, - ), - ), + ), + SizedBox(width: 8), + // Button detail di luar card + Container( + decoration: BoxDecoration( + color: Color(0xFF9DC08D), + borderRadius: BorderRadius.circular(8), ), - DataCell( - Text(_formatHasil(histori['hasil'])), + child: IconButton( + icon: Icon(Icons.info_outline, color: Colors.white), + onPressed: () => _navigateToDetail(histori), + tooltip: 'Lihat Detail', ), - DataCell( - Text(histori['tanggal_display'] ?? ''), - ), - ], - ); - }).toList(), - ), - ), - ), - ), // Pagination controls - Container( - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(Icons.first_page, size: 18), - padding: EdgeInsets.all(4), - constraints: BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - onPressed: - _currentPage > 0 - ? () { - setState(() { - _updatePagination(0); - }); - } - : null, - ), - IconButton( - icon: Icon(Icons.chevron_left, size: 18), - padding: EdgeInsets.all(4), - constraints: BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - onPressed: - _currentPage > 0 - ? () { - setState(() { - _updatePagination(_currentPage - 1); - }); - } - : null, - ), - SizedBox(width: 8), - Text( - '${_currentPage + 1} / $_totalPages', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(width: 8), - IconButton( - icon: Icon(Icons.chevron_right, size: 18), - padding: EdgeInsets.all(4), - constraints: BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - onPressed: - _currentPage < _totalPages - 1 - ? () { - setState(() { - _updatePagination(_currentPage + 1); - }); - } - : null, - ), - IconButton( - icon: Icon(Icons.last_page, size: 18), - padding: EdgeInsets.all(4), - constraints: BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - onPressed: - _currentPage < _totalPages - 1 - ? () { - setState(() { - _updatePagination(_totalPages - 1); - }); - } - : null, - ), - ], - ), - ), // Rows per page selector - Container( - padding: EdgeInsets.only(bottom: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Rows per page: ', - style: TextStyle(fontSize: 12), - ), - DropdownButton( - value: _rowsPerPage, - isDense: true, - menuMaxHeight: 200, - items: - [10, 20, 50, 100].map((value) { - return DropdownMenuItem( - value: value, - child: Text('$value'), - ); - }).toList(), - onChanged: (value) { - setState(() { - _rowsPerPage = value!; - _updatePagination( - 0, - ); // Kembali ke halaman pertama - }); + ), + ], + ), + ); }, ), - ], - ), + ), + + // Pagination controls + Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon(Icons.first_page, size: 18), + padding: EdgeInsets.all(4), + constraints: BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + onPressed: _currentPage > 0 + ? () { + setState(() { + _updatePagination(0); + }); + } + : null, + ), + IconButton( + icon: Icon(Icons.chevron_left, size: 18), + padding: EdgeInsets.all(4), + constraints: BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + onPressed: _currentPage > 0 + ? () { + setState(() { + _updatePagination(_currentPage - 1); + }); + } + : null, + ), + SizedBox(width: 8), + Text( + '${_currentPage + 1} / $_totalPages', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 8), + IconButton( + icon: Icon(Icons.chevron_right, size: 18), + padding: EdgeInsets.all(4), + constraints: BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + onPressed: _currentPage < _totalPages - 1 + ? () { + setState(() { + _updatePagination(_currentPage + 1); + }); + } + : null, + ), + IconButton( + icon: Icon(Icons.last_page, size: 18), + padding: EdgeInsets.all(4), + constraints: BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + onPressed: _currentPage < _totalPages - 1 + ? () { + setState(() { + _updatePagination(_totalPages - 1); + }); + } + : null, + ), + ], ), - ], + ), + + // Rows per page selector + Container( + padding: EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Rows per page: ', + style: TextStyle(fontSize: 12), + ), + DropdownButton( + value: _rowsPerPage, + isDense: true, + menuMaxHeight: 200, + items: [10, 20, 50, 100].map((value) { + return DropdownMenuItem( + value: value, + child: Text('$value'), + ); + }).toList(), + onChanged: (value) { + setState(() { + _rowsPerPage = value!; + _updatePagination(0); + }); + }, + ), + ], + ), + ), + ], + ), + ); + } + + Color _getDiagnosaColor(Map 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)}%'; + } +} + +// Halaman Detail Histori +class DetailHistoriPage extends StatelessWidget { + final Map 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, + ), + ), + ), + ], ); } diff --git a/frontend/lib/admin/admin_page.dart b/frontend/lib/admin/admin_page.dart index 2eb1796..4987a41 100644 --- a/frontend/lib/admin/admin_page.dart +++ b/frontend/lib/admin/admin_page.dart @@ -48,6 +48,25 @@ class _AdminPageState extends State { await prefs.setInt(LAST_KNOWN_COUNT_KEY, _lastKnownDiagnosisCount); } + // Method untuk menghitung jumlah diagnosa berdasarkan tanggal unik + int _countUniqueByDate(List historiList) { + Set 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 Future _loadDashboardData() async { try { @@ -78,21 +97,33 @@ class _AdminPageState extends State { pestCount = hamaList.length; 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(); - int currentCount = allHistori.length; + print("Total histori records: ${allHistori.length}"); + + // Hitung jumlah diagnosa berdasarkan tanggal unik + int currentUniqueCount = _countUniqueByDate(allHistori); + print("Unique diagnosis dates: $currentUniqueCount"); - if (currentCount > _lastKnownDiagnosisCount) { - int newDiagnoses = currentCount - _lastKnownDiagnosisCount; + // Update diagnosis count berdasarkan tanggal unik + if (currentUniqueCount > _lastKnownDiagnosisCount) { + int newDiagnoses = currentUniqueCount - _lastKnownDiagnosisCount; diagnosisCount += newDiagnoses; - _lastKnownDiagnosisCount = currentCount; + _lastKnownDiagnosisCount = currentUniqueCount; // Save the updated counts await _saveCounts(); - print("New diagnoses added: $newDiagnoses"); + print("New unique diagnosis dates added: $newDiagnoses"); print("Total diagnosis count: $diagnosisCount"); + } else { + // Jika tidak ada penambahan, set ke current unique count + diagnosisCount = currentUniqueCount; + _lastKnownDiagnosisCount = currentUniqueCount; + await _saveCounts(); } + } catch (e) { print("Error loading dashboard data: $e"); } finally { @@ -282,4 +313,4 @@ class _AdminPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/frontend/lib/admin/edit_hama_page.dart b/frontend/lib/admin/edit_hama_page.dart index f25749a..e6c3256 100644 --- a/frontend/lib/admin/edit_hama_page.dart +++ b/frontend/lib/admin/edit_hama_page.dart @@ -288,24 +288,24 @@ class _EditHamaPageState extends State { maxLines: 3, ), SizedBox(height: 20), - TextField( - controller: _nilaiPakarController, - decoration: InputDecoration( - labelText: 'Nilai Pakar', - hintText: 'Contoh: 0.5', - ), - keyboardType: TextInputType.numberWithOptions(decimal: true), - onChanged: (value) { - // Validate as user types (optional) - try { - if (value.isNotEmpty) { - double.parse(value.replaceAll(',', '.')); - } - } catch (e) { - // Could show validation error here - } - }, - ), + // TextField( + // controller: _nilaiPakarController, + // decoration: InputDecoration( + // labelText: 'Nilai Pakar', + // hintText: 'Contoh: 0.5', + // ), + // keyboardType: TextInputType.numberWithOptions(decimal: true), + // onChanged: (value) { + // // Validate as user types (optional) + // try { + // if (value.isNotEmpty) { + // double.parse(value.replaceAll(',', '.')); + // } + // } catch (e) { + // // Could show validation error here + // } + // }, + // ), SizedBox(height: 20), Text( 'Foto Hama', diff --git a/frontend/lib/admin/edit_penyakit_page.dart b/frontend/lib/admin/edit_penyakit_page.dart index 8e397ac..6123520 100644 --- a/frontend/lib/admin/edit_penyakit_page.dart +++ b/frontend/lib/admin/edit_penyakit_page.dart @@ -287,24 +287,24 @@ class _EditPenyakitPageState extends State { maxLines: 3, ), SizedBox(height: 20), - TextField( - controller: _nilaiPakarController, - decoration: InputDecoration( - labelText: 'Nilai Pakar', - hintText: 'Contoh: 0.5', - ), - keyboardType: TextInputType.numberWithOptions(decimal: true), - onChanged: (value) { - // Validate as user types (optional) - try { - if (value.isNotEmpty) { - double.parse(value.replaceAll(',', '.')); - } - } catch (e) { - // Could show validation error here - } - }, - ), + // TextField( + // controller: _nilaiPakarController, + // decoration: InputDecoration( + // labelText: 'Nilai Pakar', + // hintText: 'Contoh: 0.5', + // ), + // keyboardType: TextInputType.numberWithOptions(decimal: true), + // onChanged: (value) { + // // Validate as user types (optional) + // try { + // if (value.isNotEmpty) { + // double.parse(value.replaceAll(',', '.')); + // } + // } catch (e) { + // // Could show validation error here + // } + // }, + // ), SizedBox(height: 20), Text( 'Foto Penyakit', diff --git a/frontend/lib/admin/gejala_page.dart b/frontend/lib/admin/gejala_page.dart index ff7b290..f51d6c4 100644 --- a/frontend/lib/admin/gejala_page.dart +++ b/frontend/lib/admin/gejala_page.dart @@ -9,13 +9,22 @@ class GejalaPage extends StatefulWidget { class _GejalaPageState extends State { final ApiService apiService = ApiService(); List> gejalaList = []; + List> filteredGejalaList = []; + TextEditingController searchController = TextEditingController(); + bool isSearchVisible = false; @override void initState() { super.initState(); fetchGejala(); + searchController.addListener(_filterGejala); + } + + @override + void dispose() { + searchController.dispose(); + super.dispose(); } - // 🔹 Ambil data gejala dari API Future fetchGejala() async { @@ -23,12 +32,35 @@ class _GejalaPageState extends State { final data = await apiService.getGejala(); setState(() { gejalaList = data; + filteredGejalaList = data; }); } catch (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 void _tambahGejala() { TextEditingController namaController = TextEditingController(); @@ -70,51 +102,49 @@ class _GejalaPageState extends State { } void showEditDialog(BuildContext context, Map gejala) { - final TextEditingController editNamaController = TextEditingController(text: gejala['nama'] ?? ''); + final TextEditingController editNamaController = TextEditingController(text: gejala['nama'] ?? ''); - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text( - 'Edit Hama', - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: editNamaController, - decoration: InputDecoration( - labelText: 'Nama', + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Edit Gejala'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: editNamaController, + decoration: InputDecoration( + labelText: 'Nama', + ), ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Batal'), + ), + ElevatedButton( + onPressed: () async { + try { + await apiService.updateGejala( + gejala['id'], + editNamaController.text + ); + fetchGejala(); + Navigator.pop(context); + } catch (e) { + print("Error updating gejala: $e"); + } + }, + child: Text('Simpan', style: TextStyle(color: Colors.black)), ), ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Batal'), - ), - ElevatedButton( - onPressed: () async { - try { - await apiService.updateGejala( - gejala['id'], - editNamaController.text - ); - fetchGejala(); - Navigator.pop(context); - } catch (e) { - print("Error updating gejala: $e"); - } - }, - child: Text('Simpan', style: TextStyle(color: Colors.black)), - ), - ], - ); - }, - ); -} + ); + }, + ); + } // 🔹 Hapus gejala dari API void _hapusGejala(int id) async { @@ -127,152 +157,194 @@ class _GejalaPageState extends State { } void _konfirmasiHapus(int id) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Konfirmasi Hapus'), - content: Text('Apakah Anda yakin ingin menghapus gejala ini?'), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); // Tutup pop-up tanpa menghapus - }, - child: Text('Tidak'), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(context); // Tutup pop-up - _hapusGejala(id); // Lanjutkan proses hapus - }, - child: Text('Ya, Hapus'), - style: ElevatedButton.styleFrom(backgroundColor: Colors.red), - ), - ], - ); - }, - ); -} + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Konfirmasi Hapus'), + content: Text('Apakah Anda yakin ingin menghapus gejala ini?'), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); // Tutup pop-up tanpa menghapus + }, + child: Text('Tidak'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); // Tutup pop-up + _hapusGejala(id); // Lanjutkan proses hapus + }, + child: Text('Ya, Hapus'), + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + ), + ], + ); + }, + ); + } - - -//pagination + //pagination int currentPage = 0; int rowsPerPage = 10; @override -Widget build(BuildContext context) { - int start = currentPage * rowsPerPage; - int end = (start + rowsPerPage < gejalaList.length) - ? start + rowsPerPage - : gejalaList.length; - List currentPageData = gejalaList.sublist(start, end); + Widget build(BuildContext context) { + int start = currentPage * rowsPerPage; + int end = (start + rowsPerPage < filteredGejalaList.length) + ? start + rowsPerPage + : filteredGejalaList.length; + List currentPageData = filteredGejalaList.sublist(start, end); - return Scaffold( - appBar: AppBar( - title: Text('Halaman Gejala'), - ), - body: Column( - children: [ - SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: ElevatedButton( - onPressed: _tambahGejala, - child: Text( - 'Tambah Gejala', - style: TextStyle(color: Colors.green[200]), + return Scaffold( + appBar: AppBar( + title: Text('Halaman Gejala'), + backgroundColor: Color(0xFF9DC08D), + ), + body: Column( + children: [ + SizedBox(height: 16), + // Search Button + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton.icon( + onPressed: _toggleSearch, + icon: Icon(Icons.search), + label: Text('Cari'), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF9DC08D), + foregroundColor: Colors.white, + ), + ), + ], + ), + ), + // 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], ), ), ), - ], - ), - SizedBox(height: 20), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SingleChildScrollView( - 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: [ - IconButton( - icon: Icon(Icons.edit, color: Color(0xFF9DC08D)), - onPressed: () => showEditDialog(context, gejala), + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + // Data List + Expanded( + 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, + ), + ), + SizedBox(height: 4), + Text( + gejala['kode'] ?? '-', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ), ), - IconButton( - icon: Icon(Icons.delete, color: Colors.red), + ), + 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']), ), - ], - ), + ), + ], ), - ], - ), + ); + }, ), - DataRow( - cells: [ - DataCell(Container()), - DataCell(Container()), - DataCell( - Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(Icons.chevron_left), - onPressed: currentPage > 0 - ? () => setState(() => currentPage--) - : null, - ), - Text(' ${currentPage + 1}'), - IconButton( - icon: Icon(Icons.chevron_right), - onPressed: - (currentPage + 1) * rowsPerPage < gejalaList.length - ? () => setState(() => currentPage++) - : null, - ), - ], - ), - ), + ), + SizedBox(height: 16), + // Pagination + if (filteredGejalaList.length > rowsPerPage) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.chevron_left), + onPressed: currentPage > 0 + ? () => setState(() => currentPage--) + : null, + ), + Text( + 'Halaman ${currentPage + 1}', + style: TextStyle(fontSize: 16), + ), + IconButton( + icon: Icon(Icons.chevron_right), + onPressed: (currentPage + 1) * rowsPerPage < filteredGejalaList.length + ? () => setState(() => currentPage++) + : null, ), - DataCell(Container()), ], ), - ], - ), + ], ), ), ), - ), - ], - ), - ); -} - -} + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _tambahGejala, + child: Icon(Icons.add), + backgroundColor: Color(0xFF9DC08D), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/admin/hama_page.dart b/frontend/lib/admin/hama_page.dart index 1b0edd8..9e331db 100644 --- a/frontend/lib/admin/hama_page.dart +++ b/frontend/lib/admin/hama_page.dart @@ -11,11 +11,21 @@ class HamaPage extends StatefulWidget { class _HamaPageState extends State { final ApiService apiService = ApiService(); List> hamaList = []; + List> filteredHamaList = []; + TextEditingController searchController = TextEditingController(); + bool isSearchVisible = false; @override void initState() { super.initState(); _fetchHama(); + searchController.addListener(_filterHama); + } + + @override + void dispose() { + searchController.dispose(); + super.dispose(); } Future _fetchHama() async { @@ -23,12 +33,35 @@ class _HamaPageState extends State { List> data = await apiService.getHama(); setState(() { hamaList = data; + filteredHamaList = data; }); } catch (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 void _hapusHama(int id) async { try { @@ -45,7 +78,7 @@ class _HamaPageState extends State { builder: (context) { return AlertDialog( title: Text('Konfirmasi Hapus'), - content: Text('Apakah Anda yakin ingin menghapus gejala ini?'), + content: Text('Apakah Anda yakin ingin menghapus hama ini?'), actions: [ TextButton( onPressed: () { @@ -67,6 +100,71 @@ class _HamaPageState extends State { ); } + void _navigateToEdit(Map 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 int currentPage = 0; int rowsPerPage = 10; @@ -74,203 +172,169 @@ class _HamaPageState extends State { @override Widget build(BuildContext context) { int start = currentPage * rowsPerPage; - int end = - (start + rowsPerPage < hamaList.length) - ? start + rowsPerPage - : hamaList.length; - List currentPageData = hamaList.sublist(start, end); + int end = (start + rowsPerPage < filteredHamaList.length) + ? start + rowsPerPage + : filteredHamaList.length; + List currentPageData = filteredHamaList.sublist(start, end); + return Scaffold( - appBar: AppBar(title: Text('Halaman Hama'), backgroundColor: Color(0xFF9DC08D)), + appBar: AppBar( + title: Text('Halaman Hama'), + backgroundColor: Color(0xFF9DC08D), + ), body: Column( children: [ - SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: - (context) => TambahHamaPage( - onHamaAdded: - _fetchHama, // Panggil fungsi refresh setelah tambah - ), - ), - ); - }, - child: Text( - 'Tambah Hama', - style: TextStyle(color: Colors.green[200]), + SizedBox(height: 16), + // Search Button + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton.icon( + onPressed: _toggleSearch, + icon: Icon(Icons.search), + label: Text('Cari'), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF9DC08D), + foregroundColor: Colors.white, ), ), + ], + ), + ), + // 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], + ), ), - ], - ), - SizedBox(height: 20), + ), Expanded( child: Padding( - padding: const EdgeInsets.all(8.0), - child: SingleChildScrollView( - 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: 50, child: Text('Kode')), - ), - DataColumn( - label: SizedBox(width: 100, child: Text('Nama')), - ), - 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), + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + // Data List + Expanded( + child: ListView.builder( + itemCount: currentPageData.length, + itemBuilder: (context, index) { + 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, + ), + ), + SizedBox(height: 4), + Text( + hama['kode'] ?? '-', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), ), - 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 - ), - ), - ); - }, ), - - IconButton( - icon: Icon(Icons.delete, color: Colors.red), - onPressed: - () => _konfirmasiHapus(hama['id']), - ), - ], + ), ), - ), - ], - ), - ), - DataRow( - cells: [ - DataCell(Container()), - DataCell(Container()), - DataCell( - Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(Icons.chevron_left), - onPressed: - currentPage > 0 - ? () => - setState(() => currentPage--) - : null, - ), - Text(' ${currentPage + 1}'), - IconButton( - icon: Icon(Icons.chevron_right), - onPressed: - (currentPage + 1) * rowsPerPage < - hamaList.length - ? () => - setState(() => currentPage++) - : null, - ), - ], + 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']), + ), ), - ), + ], ), - DataCell(Container()), - DataCell(Container()), - DataCell(Container()), - ], - ), - ], + ); + }, + ), ), - ), + SizedBox(height: 16), + // Pagination + if (filteredHamaList.length > rowsPerPage) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.chevron_left), + onPressed: currentPage > 0 + ? () => setState(() => currentPage--) + : null, + ), + Text( + 'Halaman ${currentPage + 1}', + style: TextStyle(fontSize: 16), + ), + IconButton( + icon: Icon(Icons.chevron_right), + onPressed: (currentPage + 1) * rowsPerPage < filteredHamaList.length + ? () => setState(() => currentPage++) + : null, + ), + ], + ), + ], ), ), ), ], ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TambahHamaPage( + onHamaAdded: _fetchHama, + ), + ), + ); + }, + child: Icon(Icons.add), + backgroundColor: Color(0xFF9DC08D), + ), ); } -} +} \ No newline at end of file diff --git a/frontend/lib/admin/penyakit_page.dart b/frontend/lib/admin/penyakit_page.dart index 1c7e280..fe333de 100644 --- a/frontend/lib/admin/penyakit_page.dart +++ b/frontend/lib/admin/penyakit_page.dart @@ -12,11 +12,21 @@ class PenyakitPage extends StatefulWidget { class _PenyakitPageState extends State { final ApiService apiService = ApiService(); List> penyakitList = []; + List> filteredPenyakitList = []; + TextEditingController searchController = TextEditingController(); + bool isSearchVisible = false; @override void initState() { super.initState(); _fetchPenyakit(); + searchController.addListener(_filterPenyakit); + } + + @override + void dispose() { + searchController.dispose(); + super.dispose(); } Future _fetchPenyakit() async { @@ -24,19 +34,42 @@ class _PenyakitPageState extends State { List> data = await apiService.getPenyakit(); setState(() { penyakitList = data; + filteredPenyakitList = data; }); } catch (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 { try { await apiService.deletePenyakit(id); _fetchPenyakit(); // Refresh data setelah hapus } catch (e) { - print('Error hapus gejala: $e'); + print('Error hapus penyakit: $e'); } } @@ -46,7 +79,7 @@ class _PenyakitPageState extends State { builder: (context) { return AlertDialog( title: Text('Konfirmasi Hapus'), - content: Text('Apakah Anda yakin ingin menghapus gejala ini?'), + content: Text('Apakah Anda yakin ingin menghapus penyakit ini?'), actions: [ TextButton( onPressed: () { @@ -68,6 +101,47 @@ class _PenyakitPageState extends State { ); } + void _navigateToEdit(Map 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 int currentPage = 0; int rowsPerPage = 10; @@ -75,203 +149,169 @@ class _PenyakitPageState extends State { @override Widget build(BuildContext context) { int start = currentPage * rowsPerPage; - int end = - (start + rowsPerPage < penyakitList.length) - ? start + rowsPerPage - : penyakitList.length; - List currentPageData = penyakitList.sublist(start, end); + int end = (start + rowsPerPage < filteredPenyakitList.length) + ? start + rowsPerPage + : filteredPenyakitList.length; + List currentPageData = filteredPenyakitList.sublist(start, end); + return Scaffold( - appBar: AppBar(title: Text('Halaman Penyakit'), backgroundColor: Color(0xFF9DC08D)), + appBar: AppBar( + title: Text('Halaman Penyakit'), + backgroundColor: Color(0xFF9DC08D), + ), body: Column( children: [ - SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - 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), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SingleChildScrollView( - 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: 50, child: Text('Kode')), - ), - DataColumn( - label: SizedBox(width: 100, child: Text('Nama')), - ), - 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 - ), - ), - ); - }, - ), - IconButton( - icon: Icon(Icons.delete, color: Colors.red), - onPressed: - () => _konfirmasiHapus(penyakit['id']), - ), - ], - ), - ), - ], - ), - ), - DataRow( - cells: [ - DataCell(Container()), - DataCell(Container()), - DataCell( - Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(Icons.chevron_left), - onPressed: - currentPage > 0 - ? () => - setState(() => currentPage--) - : null, - ), - Text(' ${currentPage + 1}'), - IconButton( - icon: Icon(Icons.chevron_right), - onPressed: - (currentPage + 1) * rowsPerPage < - penyakitList.length - ? () => - setState(() => currentPage++) - : null, - ), - ], - ), - ), - ), - DataCell(Container()), - DataCell(Container()), - DataCell(Container()), - ], - ), - ], + SizedBox(height: 16), + // Search Button + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton.icon( + onPressed: _toggleSearch, + icon: Icon(Icons.search), + label: Text('Cari'), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF9DC08D), + foregroundColor: Colors.white, ), ), + ], + ), + ), + // 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( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + // Data List + Expanded( + child: ListView.builder( + itemCount: currentPageData.length, + itemBuilder: (context, index) { + 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, + ), + ), + SizedBox(height: 4), + Text( + penyakit['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(penyakit['id']), + ), + ), + ], + ), + ); + }, + ), + ), + SizedBox(height: 16), + // Pagination + if (filteredPenyakitList.length > rowsPerPage) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.chevron_left), + onPressed: currentPage > 0 + ? () => setState(() => currentPage--) + : null, + ), + Text( + 'Halaman ${currentPage + 1}', + style: TextStyle(fontSize: 16), + ), + IconButton( + icon: Icon(Icons.chevron_right), + onPressed: (currentPage + 1) * rowsPerPage < filteredPenyakitList.length + ? () => setState(() => currentPage++) + : null, + ), + ], + ), + ], ), ), ), ], ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TambahPenyakitPage( + onPenyakitAdded: _fetchPenyakit, + ), + ), + ); + }, + child: Icon(Icons.add), + backgroundColor: Color(0xFF9DC08D), + ), ); } -} +} \ No newline at end of file diff --git a/frontend/lib/admin/rule_page.dart b/frontend/lib/admin/rule_page.dart index daf2e4f..ed97f51 100644 --- a/frontend/lib/admin/rule_page.dart +++ b/frontend/lib/admin/rule_page.dart @@ -3,7 +3,6 @@ import 'package:SIBAYAM/admin/edit_rule_page.dart'; import 'package:http/http.dart' as http; import 'package:SIBAYAM/api_services/api_services.dart'; import 'tambah_rule_page.dart'; -import 'edit_hama_page.dart'; class RulePage extends StatefulWidget { const RulePage({Key? key}) : super(key: key); @@ -18,8 +17,13 @@ class _RulePageState extends State { List> hamaList = []; List rules = []; + List filteredRules = []; bool isLoading = true; + // Search and filter variables + TextEditingController searchController = TextEditingController(); + String selectedFilter = 'Semua'; // 'Semua', 'Penyakit', 'Hama' + // Pagination variables int currentPage = 0; int rowsPerPage = 10; @@ -28,9 +32,57 @@ class _RulePageState extends State { void initState() { super.initState(); 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 { + setState(() { + isLoading = true; + }); + final apiService = ApiService(); try { @@ -49,12 +101,12 @@ class _RulePageState extends State { ...rulesPenyakit.map((rule) { final gejala = gejalaList.firstWhere( (item) => item['id'] == rule['id_gejala'], - orElse: () => {'nama': '-'}, + orElse: () => {'nama': 'Gejala tidak ditemukan'}, ); final penyakit = penyakitList.firstWhere( (item) => item['id'] == rule['id_penyakit'], - orElse: () => {'nama': '-'}, + orElse: () => {'nama': 'Penyakit tidak ditemukan'}, ); return { @@ -66,20 +118,19 @@ class _RulePageState extends State { 'nama_penyakit': penyakit['nama'], 'nama_hama': null, 'nilai_pakar': rule['nilai_pakar'], + 'type': 'Penyakit', }; }), // Mengolah rules hama ...rulesHama.map((rule) { - // Mencari gejala berdasarkan id final gejala = gejalaList.firstWhere( (item) => item['id'] == rule['id_gejala'], - orElse: () => {'nama': 'TIDAK DITEMUKAN'}, + orElse: () => {'nama': 'Gejala tidak ditemukan'}, ); - // Mencari hama berdasarkan id final hama = hamaList.firstWhere( (item) => item['id'] == rule['id_hama'], - orElse: () => {'nama': 'TIDAK DITEMUKAN'}, + orElse: () => {'nama': 'Hama tidak ditemukan'}, ); return { @@ -91,15 +142,23 @@ class _RulePageState extends State { 'nama_penyakit': null, 'nama_hama': hama['nama'], 'nilai_pakar': rule['nilai_pakar'], + 'type': 'Hama', }; }), ]; setState(() { rules = enrichedRules; + filteredRules = enrichedRules; }); } catch (e) { print('Terjadi kesalahan saat memuat data: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Gagal memuat data: $e'), + backgroundColor: Colors.red, + ), + ); } finally { setState(() { isLoading = false; @@ -108,21 +167,47 @@ class _RulePageState extends State { } Future deleteRule(Map rule) async { + // Tampilkan dialog konfirmasi + bool? confirm = await showDialog( + 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 { http.Response res; // Tentukan fungsi delete berdasarkan isi rule 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) { - res = await ApiService.deleteRulePenyakit(rule['id']); // Fungsi API untuk delete penyakit + res = await ApiService.deleteRulePenyakit(rule['id']); } else { - throw Exception("Data rule tidak valid (tidak ada id_hama atau id_penyakit)"); + throw Exception("Data rule tidak valid"); } if (res.statusCode == 200) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Rule berhasil dihapus")) + SnackBar( + content: Text("Rule berhasil dihapus"), + backgroundColor: Colors.green, + ), ); fetchRules(); // Refresh data setelah delete } else { @@ -130,7 +215,10 @@ class _RulePageState extends State { } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Terjadi kesalahan saat menghapus: $e")), + SnackBar( + content: Text("Terjadi kesalahan saat menghapus: $e"), + backgroundColor: Colors.red, + ), ); } } @@ -138,224 +226,420 @@ class _RulePageState extends State { // Get paginated data List get paginatedRules { 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 rules.sublist(startIndex, endIndex); + return filteredRules.sublist(startIndex, endIndex); } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Data Rules'), backgroundColor: Color(0xFF9DC08D)), - body: Padding( + Widget _buildSearchAndFilter() { + return Card( + elevation: 2, + child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Row( - mainAxisAlignment: MainAxisAlignment.end, children: [ - // Button untuk tambah rule hama - ElevatedButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TambahRulePage( - isEditing: false, - isEditingHama: true, // Menandakan ini adalah rule hama - selectedRuleIds: [], - selectedGejalaIds: [], - nilaiPakarList: [], - selectedHamaId: null, - selectedPenyakitId: null, - showHamaOnly: true, // Parameter baru untuk menampilkan hanya dropdown 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), ), - ).then((_) => fetchRules()); - }, - icon: Icon(Icons.bug_report, size: 16,), - label: Text( - "Tambah Rule Hama", - style: TextStyle(fontSize: 12)), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), // Padding lebih kecil - minimumSize: Size(0, 32), // Tinggi minimum lebih kecil - tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), + ), ), ), - SizedBox(width: 10), - // Button untuk tambah rule penyakit - ElevatedButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TambahRulePage( - isEditing: false, - isEditingHama: false, // Menandakan ini adalah rule penyakit - selectedRuleIds: [], - selectedGejalaIds: [], - nilaiPakarList: [], - selectedHamaId: null, - selectedPenyakitId: null, - showPenyakitOnly: true, // Parameter baru untuk menampilkan hanya dropdown penyakit - ), + SizedBox(width: 16), + Expanded( + child: DropdownButtonFormField( + value: selectedFilter, + decoration: InputDecoration( + labelText: 'Filter Kategori', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), ), - ).then((_) => fetchRules()); - }, - icon: Icon(Icons.healing, size: 16,), - label: Text( - "Tambah Rule Penyakit", - style: TextStyle(fontSize: 12),), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), // Padding lebih kecil - minimumSize: Size(0, 32), // Tinggi minimum lebih kecil - tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), + ), + items: ['Semua', 'Penyakit', 'Hama'].map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + selectedFilter = newValue!; + _filterRules(); + }); + }, ), ), ], ), - const SizedBox(height: 16), - isLoading - ? const Center(child: CircularProgressIndicator()) - : Expanded( - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: DataTable( - headingRowColor: MaterialStateProperty.resolveWith( - (Set states) { - return Color(0xFF9DC08D); // Apply color to all header rows - }, - ), - 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) { - final rule = paginatedRules[index]; - final displayIndex = currentPage * rowsPerPage + index + 1; - - final namaKategori = rule['id_penyakit'] != null - ? rule['nama_penyakit'] ?? '-' - : rule['nama_hama'] ?? '-'; - - final isPenyakit = rule['id_penyakit'] != null; - - return DataRow( - cells: [ - DataCell(Text(displayIndex.toString())), - DataCell(Text(namaKategori)), - DataCell(Text(rule['nama_gejala'] ?? '-')), - DataCell(Text(rule['nilai_pakar']?.toString() ?? '-')), - DataCell( - Row( - children: [ - IconButton( - icon: const Icon( - Icons.edit, - color: Colors.orange, - ), - onPressed: () { - if (rule != null && - rule['id'] != null && - rule['id_gejala'] != null && - rule['nilai_pakar'] != null) { - // Tentukan jenis rule untuk editing - final bool editingHama = rule['id_hama'] != null; - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EditRulePage( - isEditing: true, - isEditingHama: editingHama, - selectedRuleIds: [rule['id'] as int], - selectedGejalaIds: [rule['id_gejala'] as int], - nilaiPakarList: [(rule['nilai_pakar'] as num).toDouble()], - selectedHamaId: rule['id_hama'] as int?, - selectedPenyakitId: rule['id_penyakit'] as int?, - // Tambahkan parameter untuk menentukan dropdown yang ditampilkan - showHamaOnly: editingHama, - showPenyakitOnly: !editingHama, - ), - ), - ).then((_) => fetchRules()); - } else { - // Tampilkan pesan error jika data rule tidak lengkap - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Data rule tidak lengkap atau tidak valid"), - backgroundColor: Colors.red, - ), - ); - // Debug info - print("Rule data: $rule"); - } - }, - ), - IconButton( - icon: const Icon( - Icons.delete, - color: Colors.red, - ), - onPressed: () { - deleteRule(rule); - }, - ), - ], - ), - ), - ], - ); - }), + 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( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TambahRulePage( + isEditing: false, + isEditingHama: true, + selectedRuleIds: [], + selectedGejalaIds: [], + nilaiPakarList: [], + selectedHamaId: null, + selectedPenyakitId: null, + showHamaOnly: true, ), ), - ), - ), - // Pagination controls - Container( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.chevron_left), - onPressed: currentPage > 0 - ? () => setState(() => currentPage--) - : null, - ), - Text('Halaman ${currentPage + 1} dari ${(rules.length / rowsPerPage).ceil()}'), - IconButton( - icon: Icon(Icons.chevron_right), - onPressed: (currentPage + 1) * rowsPerPage < rules.length - ? () => setState(() => currentPage++) - : null, - ), - ], - ), - ), - ], + ).then((_) => fetchRules()); + }, + icon: Icon(Icons.bug_report, size: 16), + label: Text("Rule Hama", style: TextStyle(fontSize: 12)), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + minimumSize: Size(0, 36), + ), ), - ), + SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TambahRulePage( + isEditing: false, + isEditingHama: false, + selectedRuleIds: [], + selectedGejalaIds: [], + nilaiPakarList: [], + selectedHamaId: null, + selectedPenyakitId: null, + showPenyakitOnly: true, + ), + ), + ).then((_) => fetchRules()); + }, + icon: Icon(Icons.healing, size: 16), + label: Text("Rule Penyakit", style: TextStyle(fontSize: 12)), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + minimumSize: Size(0, 36), + ), + ), + ], + ), + ], + ), ], ), ), ); } + + Widget _buildDataList() { + if (paginatedRules.isEmpty) { + return Container( + padding: EdgeInsets.all(32), + child: Column( + children: [ + Icon(Icons.inbox, size: 64, color: Colors.grey[400]), + SizedBox(height: 16), + Text( + 'Tidak ada data rule', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + ), + if (searchController.text.isNotEmpty || selectedFilter != 'Semua') + TextButton( + onPressed: () { + setState(() { + searchController.clear(); + selectedFilter = 'Semua'; + _filterRules(); + }); + }, + child: Text('Reset Filter'), + ), + ], + ), + ); + } + + return ListView.builder( + itemCount: paginatedRules.length, + itemBuilder: (context, index) { + final rule = paginatedRules[index]; + + final namaKategori = rule['id_penyakit'] != null + ? rule['nama_penyakit'] ?? '-' + : rule['nama_hama'] ?? '-'; + + final kategori = rule['type'] ?? 'Unknown'; + final isHama = rule['id_hama'] != null; + + return Container( + margin: EdgeInsets.only(bottom: 12), + child: Row( + children: [ + // Card dengan data rule + Expanded( + 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( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isHama ? Colors.green[100] : Colors.blue[100], + borderRadius: BorderRadius.circular(12), + ), + 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 rule) { + if (rule != null && + rule['id'] != null && + rule['id_gejala'] != null && + rule['nilai_pakar'] != null) { + final bool editingHama = rule['id_hama'] != null; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditRulePage( + isEditing: true, + isEditingHama: editingHama, + selectedRuleIds: [rule['id'] as int], + selectedGejalaIds: [rule['id_gejala'] as int], + nilaiPakarList: [(rule['nilai_pakar'] as num).toDouble()], + selectedHamaId: rule['id_hama'] as int?, + selectedPenyakitId: rule['id_penyakit'] as int?, + showHamaOnly: editingHama, + showPenyakitOnly: !editingHama, + ), + ), + ).then((_) => fetchRules()); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Data rule tidak lengkap"), + backgroundColor: Colors.red, + ), + ); + } + } + + Widget _buildPaginationControls() { + final totalPages = (filteredRules.length / rowsPerPage).ceil(); + + if (totalPages <= 1) return SizedBox.shrink(); + + return Card( + elevation: 1, + child: Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + 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( + icon: Icon(Icons.chevron_left), + onPressed: currentPage > 0 + ? () => setState(() => currentPage--) + : null, + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Text('${currentPage + 1} / $totalPages'), + ), + IconButton( + icon: Icon(Icons.chevron_right), + onPressed: (currentPage + 1) * rowsPerPage < filteredRules.length + ? () => setState(() => currentPage++) + : 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(), + ], + ), + ), + ], + ), + ), + ); + } } \ No newline at end of file diff --git a/frontend/lib/admin/tambah_hama_page.dart b/frontend/lib/admin/tambah_hama_page.dart index 342a165..1931bf5 100644 --- a/frontend/lib/admin/tambah_hama_page.dart +++ b/frontend/lib/admin/tambah_hama_page.dart @@ -38,36 +38,43 @@ class _TambahHamaPageState extends State { } Future _simpanHama() async { - if (namaController.text.isNotEmpty && - deskripsiController.text.isNotEmpty && - penangananController.text.isNotEmpty) { - try { - double? nilaipakar; - if (nilaiPakarController.text.isNotEmpty) { - String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); - nilaipakar = double.parse(nilaiInput); + if (namaController.text.isNotEmpty && + deskripsiController.text.isNotEmpty && + penangananController.text.isNotEmpty) { + 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(',', '.'); + 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( + namaController.text, + deskripsiController.text, + penangananController.text, + _pickedFile, + nilaiPakar, + ); + widget.onHamaAdded(); + Navigator.pop(context); + _showDialog('Berhasil', 'Data hama berhasil ditambahkan.'); + } catch (e) { + _showDialog('Gagal', 'Gagal menambahkan data hama: $e'); + print("Error adding hama: $e"); } - - await apiService.createHama( - namaController.text, - deskripsiController.text, - penangananController.text, - _pickedFile, - nilaipakar, // boleh null - ); - - widget.onHamaAdded(); - Navigator.pop(context); - _showDialog('Berhasil', 'Data hama berhasil ditambahkan.'); - } catch (e) { - _showDialog('Gagal', 'Gagal menambahkan data hama.'); - print("Error adding hama: $e"); + } else { + _showDialog('Error', 'Nama, deskripsi, dan penanganan hama harus diisi.'); } - } else { - _showDialog('Error', 'Semua field harus diisi (kecuali nilai pakar).'); } -} - void _showDialog(String title, String message) { showDialog( @@ -149,8 +156,11 @@ class _TambahHamaPageState extends State { SizedBox(height: 15), // TextField( // controller: nilaiPakarController, - // decoration: InputDecoration(labelText: 'Nilai Pakar'), - // maxLines: 3, + // decoration: InputDecoration( + // labelText: 'Nilai Pakar (Optional)', + // hintText: 'Masukkan nilai pakar (opsional)', + // ), + // keyboardType: TextInputType.numberWithOptions(decimal: true), // ), SizedBox(height: 15), Text('Foto'), @@ -197,4 +207,4 @@ class _TambahHamaPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/frontend/lib/admin/tambah_penyakit_page.dart b/frontend/lib/admin/tambah_penyakit_page.dart index 79de263..a45dadc 100644 --- a/frontend/lib/admin/tambah_penyakit_page.dart +++ b/frontend/lib/admin/tambah_penyakit_page.dart @@ -39,27 +39,39 @@ class _TambahPenyakitPageState extends State { Future _simpanPenyakit() async { if (namaController.text.isNotEmpty && deskripsiController.text.isNotEmpty && - penangananController.text.isNotEmpty && - nilaiPakarController.text.isNotEmpty) { + penangananController.text.isNotEmpty) { try { - String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); - double nilaiPakar = double.parse(nilaiInput); + // Kirim nilai pakar sebagai double, 0.0 jika tidak diisi + double nilaiPakar = 0.0; + if (nilaiPakarController.text.trim().isNotEmpty) { + String nilaiInput = nilaiPakarController.text.replaceAll(',', '.'); + 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( namaController.text, deskripsiController.text, penangananController.text, _pickedFile, - nilaiPakar, + nilaiPakar, ); widget.onPenyakitAdded(); Navigator.pop(context); - _showDialog('Berhasil', 'Data penyakit berhasil ditambahkan.'); + _showDialog('Berhasil', 'Data hama berhasil ditambahkan.'); } catch (e) { - _showDialog('Gagal', 'Gagal menambahkan data penyakit.'); - print("Error adding penyakit: $e"); + _showDialog('Gagal', 'Gagal menambahkan data hama: $e'); + print("Error adding hama: $e"); } } else { - _showDialog('Error', 'Semua field harus diisi.'); + _showDialog('Error', 'Nama, deskripsi, dan penanganan hama harus diisi.'); } } @@ -141,11 +153,11 @@ class _TambahPenyakitPageState extends State { maxLines: 3, ), SizedBox(height: 15), - TextField( - controller: nilaiPakarController, - decoration: InputDecoration(labelText: 'nilai pakar'), - maxLines: 3, - ), + // TextField( + // controller: nilaiPakarController, + // decoration: InputDecoration(labelText: 'nilai pakar'), + // maxLines: 3, + // ), SizedBox(height: 15), (_webImage != null) ? Image.memory( diff --git a/frontend/lib/admin/user_list_page.dart b/frontend/lib/admin/user_list_page.dart index 036df11..a59fd6d 100644 --- a/frontend/lib/admin/user_list_page.dart +++ b/frontend/lib/admin/user_list_page.dart @@ -9,6 +9,7 @@ class UserListPage extends StatefulWidget { class _UserListPageState extends State { final ApiService apiService = ApiService(); List> users = []; + List> filteredUsers = []; bool isLoading = true; final _formKey = GlobalKey(); @@ -17,6 +18,7 @@ class _UserListPageState extends State { final _passwordController = TextEditingController(); final _alamatController = TextEditingController(); final _nomorTeleponController = TextEditingController(); + final _searchController = TextEditingController(); @override void initState() { @@ -26,12 +28,12 @@ class _UserListPageState extends State { @override void dispose() { - // Dispose controllers in dispose method _nameController.dispose(); _emailController.dispose(); _passwordController.dispose(); _alamatController.dispose(); _nomorTeleponController.dispose(); + _searchController.dispose(); super.dispose(); } @@ -45,17 +47,14 @@ class _UserListPageState extends State { nomorTelepon: _nomorTeleponController.text, ); - // Clear form _nameController.clear(); _emailController.clear(); _passwordController.clear(); _alamatController.clear(); _nomorTeleponController.clear(); - // Refresh user list await _loadUsers(); - // Show success message ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('User berhasil ditambahkan'), @@ -74,7 +73,6 @@ class _UserListPageState extends State { Future _updateUser(Map user) async { try { - // Hanya kirim password jika diisi String? newPassword = _passwordController.text.isEmpty ? null : _passwordController.text; @@ -82,22 +80,19 @@ class _UserListPageState extends State { id: user['id'], name: _nameController.text, email: _emailController.text, - password: newPassword, // Kirim null jika password kosong + password: newPassword, alamat: _alamatController.text, nomorTelepon: _nomorTeleponController.text, ); - // Clear form _nameController.clear(); _emailController.clear(); _passwordController.clear(); _alamatController.clear(); _nomorTeleponController.clear(); - // Refresh user list await _loadUsers(); - // Show success message with password info ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -118,262 +113,232 @@ class _UserListPageState extends State { } } - Future _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 user) { 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 first - await apiService.deleteUser(user['id']); - await _loadUsers(); // Refresh the 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'), - ), - ], + 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); + await apiService.deleteUser(user['id']); + 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, + ), + ); + } + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + child: Text('Hapus'), + ), + ], + ), ); } void _showUpdateDialog(Map user) { - // Pre-fill form with existing user data _nameController.text = user['name'] ?? ''; _emailController.text = user['email'] ?? ''; _alamatController.text = user['alamat'] ?? ''; _nomorTeleponController.text = user['nomorTelepon'] ?? ''; - _passwordController.text = ''; // Empty for security + _passwordController.text = ''; showDialog( context: context, - builder: - (context) => AlertDialog( - title: Text('Update User'), - content: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - 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, - ), - ], + builder: (context) => AlertDialog( + title: Text('Update User'), + content: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + 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: () { - if (_formKey.currentState!.validate()) { - Navigator.pop(context); - _updateUser(user); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF9DC08D), - ), - child: Text('Update'), - ), - ], ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Batal'), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + Navigator.pop(context); + _updateUser(user); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF9DC08D), + ), + child: Text('Update'), + ), + ], + ), ); } void _showAddUserDialog() { + _nameController.clear(); + _emailController.clear(); + _passwordController.clear(); + _alamatController.clear(); + _nomorTeleponController.clear(); + showDialog( context: context, - builder: - (context) => AlertDialog( - title: Text('Tambah User Baru'), - content: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: _nameController, - decoration: InputDecoration(labelText: 'Nama'), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Nama tidak boleh kosong'; - } - return null; - }, - ), - TextFormField( - controller: _emailController, - decoration: InputDecoration(labelText: 'Email'), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Email tidak boleh kosong'; - } - if (!value.contains('@')) { - return 'Email tidak valid'; - } - return null; - }, - ), - TextFormField( - controller: _passwordController, - decoration: InputDecoration(labelText: 'Password'), - obscureText: true, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Password tidak boleh kosong'; - } - if (value.length < 6) { - return 'Password minimal 6 karakter'; - } - return null; - }, - ), - TextFormField( - controller: _alamatController, - decoration: InputDecoration(labelText: 'Alamat'), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Alamat tidak boleh kosong'; - } - return null; - }, - ), - TextFormField( - controller: _nomorTeleponController, - decoration: InputDecoration(labelText: 'Nomor Telepon'), - keyboardType: TextInputType.phone, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Nomor telepon tidak boleh kosong'; - } - return null; - }, - ), - ], + builder: (context) => AlertDialog( + title: Text('Tambah User Baru'), + content: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: _nameController, + decoration: InputDecoration(labelText: 'Nama'), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nama tidak boleh kosong'; + } + return null; + }, ), - ), + TextFormField( + controller: _emailController, + decoration: InputDecoration(labelText: 'Email'), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Email tidak boleh kosong'; + } + if (!value.contains('@')) { + return 'Email tidak valid'; + } + return null; + }, + ), + TextFormField( + controller: _passwordController, + decoration: InputDecoration(labelText: 'Password'), + obscureText: true, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password tidak boleh kosong'; + } + if (value.length < 6) { + return 'Password minimal 6 karakter'; + } + return null; + }, + ), + TextFormField( + controller: _alamatController, + decoration: InputDecoration(labelText: 'Alamat'), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Alamat tidak boleh kosong'; + } + return null; + }, + ), + TextFormField( + controller: _nomorTeleponController, + decoration: InputDecoration(labelText: 'Nomor Telepon'), + keyboardType: TextInputType.phone, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nomor telepon tidak boleh kosong'; + } + return null; + }, + ), + ], ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Batal'), - ), - ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - Navigator.pop(context); - _addUser(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF9DC08D), - ), - child: Text('Simpan'), - ), - ], ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Batal'), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + Navigator.pop(context); + _addUser(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF9DC08D), + ), + child: Text('Simpan'), + ), + ], + ), ); } @@ -382,6 +347,7 @@ class _UserListPageState extends State { final userList = await apiService.getUsers(); setState(() { users = userList; + filteredUsers = userList; isLoading = false; }); } catch (e) { @@ -392,6 +358,35 @@ class _UserListPageState extends State { } } + 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 user) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UserDetailPage( + user: user, + onUserUpdated: _loadUsers, + onUserDeleted: _loadUsers, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -404,78 +399,476 @@ class _UserListPageState extends State { backgroundColor: Color(0xFF9DC08D), child: Icon(Icons.add), ), - body: - isLoading - ? Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SingleChildScrollView( - 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( - padding: EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, + body: isLoading + ? Center(child: CircularProgressIndicator()) + : Column( + children: [ + // Search Bar + Container( + padding: EdgeInsets.all(16), + child: TextField( + 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]!), + ), + 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], + ), + ), + ), + // 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.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.2) - : Colors.green.withOpacity(0.2), + color: user['role'] == 'admin' + ? Colors.blue.withOpacity(0.1) + : Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( user['role'] ?? 'user', style: TextStyle( - color: - user['role'] == 'admin' - ? Colors.blue - : Colors.green, + color: user['role'] == 'admin' ? Colors.blue : Colors.green, + fontSize: 12, + fontWeight: FontWeight.w500, ), ), ), + trailing: Icon(Icons.arrow_forward_ios, size: 16), + onTap: () => _navigateToUserDetail(user), ), - DataCell( - Row( - children: [ - IconButton( - icon: Icon(Icons.edit), - onPressed: () => _showUpdateDialog(user), - ), - IconButton( - icon: Icon( - Icons.delete, - color: Colors.red, - ), - onPressed: - () => _showDeleteConfirmation(user), - ), - ], - ), - ), - ], - ); - }).toList(), - ), + ); + }, + ), ), - ), + ], + ), ); } } + +// Halaman Detail User +class UserDetailPage extends StatelessWidget { + final Map 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(); + 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: [ + 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( + icon: Icon(Icons.edit), + onPressed: () => _showUpdateDialog(context), + ), + IconButton( + icon: Icon(Icons.delete, color: Colors.red), + onPressed: () => _showDeleteConfirmation(context), + ), + ], + ), + 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, + ), + ), + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/api_services/api_services.dart b/frontend/lib/api_services/api_services.dart index 01fcf31..55df731 100644 --- a/frontend/lib/api_services/api_services.dart +++ b/frontend/lib/api_services/api_services.dart @@ -899,7 +899,7 @@ Future>> getAllHistori() async { String deskripsi, String penanganan, XFile? pickedFile, - double nilai_pakar + double? nilai_pakar ) async { try { var uri = Uri.parse(penyakitUrl); diff --git a/frontend/lib/user/login_page.dart b/frontend/lib/user/login_page.dart index d47e68f..e5edf05 100644 --- a/frontend/lib/user/login_page.dart +++ b/frontend/lib/user/login_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:SIBAYAM/user/before_login.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; @@ -62,7 +63,9 @@ class _LoginPageState extends State { } 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( url, headers: {"Content-Type": "application/json"}, @@ -120,7 +123,10 @@ class _LoginPageState extends State { child: IconButton( icon: Icon(Icons.arrow_back, color: Colors.white), onPressed: () { - Navigator.pop(context); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => BeforeLogin()), + ); }, ), ),