update layout admin for get data
This commit is contained in:
parent
213eb6273c
commit
db83fb1c2f
|
@ -19,8 +19,6 @@ dotenv.config();
|
|||
const app = express();
|
||||
|
||||
|
||||
|
||||
|
||||
// Middlewares
|
||||
app.use(express.json());
|
||||
app.use(cors());
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
],
|
||||
|
|
|
@ -11,9 +11,14 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
final ApiService apiService = ApiService();
|
||||
List<Map<String, dynamic>> historiData = [];
|
||||
List<Map<String, dynamic>> groupedHistoriData = [];
|
||||
List<Map<String, dynamic>> 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<AdminHistoriPage> {
|
|||
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<void> _loadHistoriData() async {
|
||||
|
@ -34,7 +68,9 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
});
|
||||
|
||||
// Dapatkan semua histori terlebih dahulu
|
||||
final allHistori = await apiService.getAllHistori(); // Kumpulkan semua userIds yang unik
|
||||
final allHistori = await apiService.getAllHistori();
|
||||
|
||||
// Kumpulkan semua userIds yang unik
|
||||
Set<String> uniqueUserIds = allHistori
|
||||
.where((histori) => histori['userId'] != null)
|
||||
.map((histori) => histori['userId'].toString())
|
||||
|
@ -60,6 +96,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
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,7 +141,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
diagnosa = 'Tidak ada diagnosa';
|
||||
}
|
||||
|
||||
// Ambil nama user dari kolom 'nama' atau 'name' (sesuaikan dengan struktur data Anda)
|
||||
// Ambil nama user dari kolom 'nama' atau 'name'
|
||||
String userName = item['name']?.toString() ?? 'User ID: ${item['userId']}';
|
||||
|
||||
// Buat composite key: userId + waktu + diagnosa
|
||||
|
@ -116,7 +153,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
|
||||
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<AdminHistoriPage> {
|
|||
'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<AdminHistoriPage> {
|
|||
!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<AdminHistoriPage> {
|
|||
// 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<String, dynamic> histori) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DetailHistoriPage(histori: histori),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -170,8 +221,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
title: Text('Riwayat Diagnosa'),
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
),
|
||||
body:
|
||||
isLoading
|
||||
body: isLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: error != null
|
||||
? Center(child: Text('Error: $error'))
|
||||
|
@ -179,98 +229,147 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
? 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: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SingleChildScrollView(
|
||||
child: DataTable(
|
||||
columnSpacing: 20,
|
||||
headingRowColor: MaterialStateProperty.all(
|
||||
Color(0xFF9DC08D).withOpacity(0.3),
|
||||
child: filteredHistoriData.isEmpty && searchQuery.isNotEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 64,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Nama User',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Tidak ada hasil untuk "${searchController.text}"',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
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),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Coba gunakan kata kunci yang berbeda',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
],
|
||||
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(
|
||||
', ',
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: _currentPageData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final histori = _currentPageData[index];
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
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['diagnosa'] ??
|
||||
'Tidak ada diagnosa',
|
||||
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(_formatHasil(histori['hasil'])),
|
||||
),
|
||||
DataCell(
|
||||
Text(histori['tanggal_display'] ?? ''),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
// Button detail di luar card
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF9DC08D),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.info_outline, color: Colors.white),
|
||||
onPressed: () => _navigateToDetail(histori),
|
||||
tooltip: 'Lihat Detail',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
), // Pagination controls
|
||||
|
||||
// Pagination controls
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
||||
child: Row(
|
||||
|
@ -284,8 +383,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
onPressed:
|
||||
_currentPage > 0
|
||||
onPressed: _currentPage > 0
|
||||
? () {
|
||||
setState(() {
|
||||
_updatePagination(0);
|
||||
|
@ -300,8 +398,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
onPressed:
|
||||
_currentPage > 0
|
||||
onPressed: _currentPage > 0
|
||||
? () {
|
||||
setState(() {
|
||||
_updatePagination(_currentPage - 1);
|
||||
|
@ -325,8 +422,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
onPressed:
|
||||
_currentPage < _totalPages - 1
|
||||
onPressed: _currentPage < _totalPages - 1
|
||||
? () {
|
||||
setState(() {
|
||||
_updatePagination(_currentPage + 1);
|
||||
|
@ -341,8 +437,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
onPressed:
|
||||
_currentPage < _totalPages - 1
|
||||
onPressed: _currentPage < _totalPages - 1
|
||||
? () {
|
||||
setState(() {
|
||||
_updatePagination(_totalPages - 1);
|
||||
|
@ -352,7 +447,9 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
),
|
||||
],
|
||||
),
|
||||
), // Rows per page selector
|
||||
),
|
||||
|
||||
// Rows per page selector
|
||||
Container(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
|
@ -367,8 +464,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
value: _rowsPerPage,
|
||||
isDense: true,
|
||||
menuMaxHeight: 200,
|
||||
items:
|
||||
[10, 20, 50, 100].map((value) {
|
||||
items: [10, 20, 50, 100].map((value) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: value,
|
||||
child: Text('$value'),
|
||||
|
@ -377,9 +473,7 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
onChanged: (value) {
|
||||
setState(() {
|
||||
_rowsPerPage = value!;
|
||||
_updatePagination(
|
||||
0,
|
||||
); // Kembali ke halaman pertama
|
||||
_updatePagination(0);
|
||||
});
|
||||
},
|
||||
),
|
||||
|
@ -406,3 +500,213 @@ class _AdminHistoriPageState extends State<AdminHistoriPage> {
|
|||
return '${(hasilValue * 100).toStringAsFixed(2)}%';
|
||||
}
|
||||
}
|
||||
|
||||
// Halaman Detail Histori
|
||||
class DetailHistoriPage extends StatelessWidget {
|
||||
final Map<String, dynamic> histori;
|
||||
|
||||
const DetailHistoriPage({Key? key, required this.histori}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Gabungkan semua gejala menjadi satu string
|
||||
String gejalaText = "Tidak ada gejala";
|
||||
if (histori['gejala'] != null && (histori['gejala'] as List).isNotEmpty) {
|
||||
gejalaText = (histori['gejala'] as List).join(', ');
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Detail Riwayat Diagnosa'),
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF9DC08D).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Informasi Diagnosa',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Nama User
|
||||
_buildDetailRow(
|
||||
'Nama User',
|
||||
histori['userName'] ?? 'User tidak ditemukan',
|
||||
Icons.person,
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Tanggal Diagnosa
|
||||
_buildDetailRow(
|
||||
'Tanggal Diagnosa',
|
||||
histori['tanggal_display'] ?? '',
|
||||
Icons.calendar_today,
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Diagnosa
|
||||
_buildDetailRow(
|
||||
'Diagnosa',
|
||||
histori['diagnosa'] ?? 'Tidak ada diagnosa',
|
||||
Icons.medical_services,
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Hasil
|
||||
_buildDetailRow(
|
||||
'Hasil',
|
||||
_formatHasil(histori['hasil']),
|
||||
Icons.analytics,
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Gejala
|
||||
_buildDetailSection(
|
||||
'Gejala yang Dipilih',
|
||||
gejalaText,
|
||||
Icons.list_alt,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(
|
||||
String label,
|
||||
String value,
|
||||
IconData icon, {
|
||||
Color? valueColor,
|
||||
}) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
': ',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: valueColor ?? Colors.black87,
|
||||
fontWeight: valueColor != null ? FontWeight.w500 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailSection(
|
||||
String label,
|
||||
String value,
|
||||
IconData icon,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getDiagnosaColor(Map<String, dynamic> histori) {
|
||||
if (histori['penyakit_nama'] != null) {
|
||||
return Colors.red[700]!;
|
||||
} else if (histori['hama_nama'] != null) {
|
||||
return Colors.amber[800]!;
|
||||
}
|
||||
return Colors.black;
|
||||
}
|
||||
|
||||
String _formatHasil(dynamic hasil) {
|
||||
if (hasil == null) return '0%';
|
||||
double hasilValue = double.tryParse(hasil.toString()) ?? 0.0;
|
||||
return '${(hasilValue * 100).toStringAsFixed(2)}%';
|
||||
}
|
||||
}
|
|
@ -48,6 +48,25 @@ class _AdminPageState extends State<AdminPage> {
|
|||
await prefs.setInt(LAST_KNOWN_COUNT_KEY, _lastKnownDiagnosisCount);
|
||||
}
|
||||
|
||||
// Method untuk menghitung jumlah diagnosa berdasarkan tanggal unik
|
||||
int _countUniqueByDate(List<dynamic> historiList) {
|
||||
Set<String> uniqueDates = {};
|
||||
|
||||
for (var histori in historiList) {
|
||||
// Ambil tanggal_diagnosa dari data
|
||||
String? tanggalDiagnosa = histori['tanggal_diagnosa']?.toString();
|
||||
|
||||
if (tanggalDiagnosa != null && tanggalDiagnosa.isNotEmpty) {
|
||||
// Extract hanya tanggal (YYYY-MM-DD) tanpa waktu
|
||||
String dateOnly = tanggalDiagnosa.split(' ')[0];
|
||||
uniqueDates.add(dateOnly);
|
||||
}
|
||||
}
|
||||
|
||||
print("Unique dates found: ${uniqueDates.toList()}");
|
||||
return uniqueDates.length;
|
||||
}
|
||||
|
||||
// Method untuk memuat data dashboard dari API
|
||||
Future<void> _loadDashboardData() async {
|
||||
try {
|
||||
|
@ -78,21 +97,33 @@ class _AdminPageState extends State<AdminPage> {
|
|||
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}");
|
||||
|
||||
if (currentCount > _lastKnownDiagnosisCount) {
|
||||
int newDiagnoses = currentCount - _lastKnownDiagnosisCount;
|
||||
// Hitung jumlah diagnosa berdasarkan tanggal unik
|
||||
int currentUniqueCount = _countUniqueByDate(allHistori);
|
||||
print("Unique diagnosis dates: $currentUniqueCount");
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -288,24 +288,24 @@ class _EditHamaPageState extends State<EditHamaPage> {
|
|||
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',
|
||||
|
|
|
@ -287,24 +287,24 @@ class _EditPenyakitPageState extends State<EditPenyakitPage> {
|
|||
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',
|
||||
|
|
|
@ -9,13 +9,22 @@ class GejalaPage extends StatefulWidget {
|
|||
class _GejalaPageState extends State<GejalaPage> {
|
||||
final ApiService apiService = ApiService();
|
||||
List<Map<String, dynamic>> gejalaList = [];
|
||||
List<Map<String, dynamic>> 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<void> fetchGejala() async {
|
||||
|
@ -23,12 +32,35 @@ class _GejalaPageState extends State<GejalaPage> {
|
|||
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();
|
||||
|
@ -76,9 +108,7 @@ class _GejalaPageState extends State<GejalaPage> {
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Edit Hama',
|
||||
),
|
||||
title: Text('Edit Gejala'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@ -114,7 +144,7 @@ class _GejalaPageState extends State<GejalaPage> {
|
|||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔹 Hapus gejala dari API
|
||||
void _hapusGejala(int id) async {
|
||||
|
@ -152,96 +182,139 @@ class _GejalaPageState extends State<GejalaPage> {
|
|||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//pagination
|
||||
//pagination
|
||||
int currentPage = 0;
|
||||
int rowsPerPage = 10;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context) {
|
||||
int start = currentPage * rowsPerPage;
|
||||
int end = (start + rowsPerPage < gejalaList.length)
|
||||
int end = (start + rowsPerPage < filteredGejalaList.length)
|
||||
? start + rowsPerPage
|
||||
: gejalaList.length;
|
||||
List currentPageData = gejalaList.sublist(start, end);
|
||||
: filteredGejalaList.length;
|
||||
List currentPageData = filteredGejalaList.sublist(start, end);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Halaman Gejala'),
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
SizedBox(height: 16),
|
||||
// Search Button
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: 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]),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _toggleSearch,
|
||||
icon: Icon(Icons.search),
|
||||
label: Text('Cari'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
),
|
||||
// Search Field (conditional)
|
||||
if (isSearchVisible)
|
||||
Container(
|
||||
margin: EdgeInsets.all(16),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari nama atau kode gejala...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: _toggleSearch,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
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(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit, color: Color(0xFF9DC08D)),
|
||||
onPressed: () => showEditDialog(context, gejala),
|
||||
// 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,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.red),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
gejala['kode'] ?? '-',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
// Button hapus di luar card
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.white),
|
||||
onPressed: () => _konfirmasiHapus(gejala['id']),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
DataRow(
|
||||
cells: [
|
||||
DataCell(Container()),
|
||||
DataCell(Container()),
|
||||
DataCell(
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
// Pagination
|
||||
if (filteredGejalaList.length > rowsPerPage)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.chevron_left),
|
||||
|
@ -249,30 +322,29 @@ Widget build(BuildContext context) {
|
|||
? () => setState(() => currentPage--)
|
||||
: null,
|
||||
),
|
||||
Text(' ${currentPage + 1}'),
|
||||
Text(
|
||||
'Halaman ${currentPage + 1}',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.chevron_right),
|
||||
onPressed:
|
||||
(currentPage + 1) * rowsPerPage < gejalaList.length
|
||||
onPressed: (currentPage + 1) * rowsPerPage < filteredGejalaList.length
|
||||
? () => setState(() => currentPage++)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(Container()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _tambahGejala,
|
||||
child: Icon(Icons.add),
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -11,11 +11,21 @@ class HamaPage extends StatefulWidget {
|
|||
class _HamaPageState extends State<HamaPage> {
|
||||
final ApiService apiService = ApiService();
|
||||
List<Map<String, dynamic>> hamaList = [];
|
||||
List<Map<String, dynamic>> filteredHamaList = [];
|
||||
TextEditingController searchController = TextEditingController();
|
||||
bool isSearchVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchHama();
|
||||
searchController.addListener(_filterHama);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchHama() async {
|
||||
|
@ -23,12 +33,35 @@ class _HamaPageState extends State<HamaPage> {
|
|||
List<Map<String, dynamic>> 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<HamaPage> {
|
|||
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<HamaPage> {
|
|||
);
|
||||
}
|
||||
|
||||
void _navigateToEdit(Map<String, dynamic> hama) {
|
||||
// Parse nilai_pakar dengan aman
|
||||
double nilaiPakar = 0.0;
|
||||
if (hama['nilai_pakar'] != null) {
|
||||
// Coba parse jika string
|
||||
if (hama['nilai_pakar'] is String) {
|
||||
try {
|
||||
String nilaiStr = hama['nilai_pakar'].toString().trim();
|
||||
if (nilaiStr.isNotEmpty) {
|
||||
nilaiPakar = double.parse(nilaiStr.replaceAll(',', '.'));
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error parsing nilai_pakar: $e");
|
||||
}
|
||||
}
|
||||
// Langsung gunakan jika sudah double
|
||||
else if (hama['nilai_pakar'] is double) {
|
||||
nilaiPakar = hama['nilai_pakar'];
|
||||
}
|
||||
// Jika int, konversi ke double
|
||||
else if (hama['nilai_pakar'] is int) {
|
||||
nilaiPakar = hama['nilai_pakar'].toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EditHamaPage(
|
||||
idHama: hama['id'],
|
||||
namaAwal: hama['nama'] ?? '',
|
||||
deskripsiAwal: hama['deskripsi'] ?? '',
|
||||
penangananAwal: hama['penanganan'] ?? '',
|
||||
gambarUrl: hama['foto'] ?? '',
|
||||
nilai_pakar: nilaiPakar,
|
||||
onHamaUpdated: _fetchHama,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPopupMenu(BuildContext context, int id) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Pilih Aksi'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(Icons.delete, color: Colors.red),
|
||||
title: Text('Hapus Hama'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_konfirmasiHapus(id);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//pagination
|
||||
int currentPage = 0;
|
||||
int rowsPerPage = 10;
|
||||
|
@ -74,203 +172,169 @@ class _HamaPageState extends State<HamaPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int start = currentPage * rowsPerPage;
|
||||
int end =
|
||||
(start + rowsPerPage < hamaList.length)
|
||||
int end = (start + rowsPerPage < filteredHamaList.length)
|
||||
? start + rowsPerPage
|
||||
: hamaList.length;
|
||||
List currentPageData = hamaList.sublist(start, end);
|
||||
: 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(
|
||||
SizedBox(height: 16),
|
||||
// Search Button
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: 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]),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _toggleSearch,
|
||||
icon: Icon(Icons.search),
|
||||
label: Text('Cari'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
),
|
||||
// Search Field (conditional)
|
||||
if (isSearchVisible)
|
||||
Container(
|
||||
margin: EdgeInsets.all(16),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari nama hama...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: _toggleSearch,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
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),
|
||||
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,
|
||||
),
|
||||
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')),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
hama['kode'] ?? '-',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
DataColumn(
|
||||
label: SizedBox(width: 100, child: Text('Deskripsi')),
|
||||
),
|
||||
DataColumn(
|
||||
label: SizedBox(width: 100, child: Text('Penanganan')),
|
||||
),
|
||||
DataColumn(
|
||||
label: SizedBox(width: 50, child: Text('Aksi')),
|
||||
),
|
||||
],
|
||||
rows: [
|
||||
...currentPageData.map(
|
||||
(hama) => DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text((hamaList.indexOf(hama) + 1).toString()),
|
||||
),
|
||||
DataCell(Text(hama['kode'] ?? '-')),
|
||||
DataCell(Text(hama['nama'] ?? '-')),
|
||||
DataCell(Text(hama['deskripsi'] ?? '-')),
|
||||
DataCell(Text(hama['penanganan'] ?? '-')),
|
||||
DataCell(
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
onPressed: () {
|
||||
// Parse nilai_pakar dengan aman
|
||||
double nilaiPakar = 0.0;
|
||||
if (hama['nilai_pakar'] != null) {
|
||||
// Coba parse jika string
|
||||
if (hama['nilai_pakar'] is String) {
|
||||
try {
|
||||
String nilaiStr =
|
||||
hama['nilai_pakar']
|
||||
.toString()
|
||||
.trim();
|
||||
if (nilaiStr.isNotEmpty) {
|
||||
nilaiPakar = double.parse(
|
||||
nilaiStr.replaceAll(',', '.'),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print(
|
||||
"Error parsing nilai_pakar: $e",
|
||||
);
|
||||
}
|
||||
}
|
||||
// Langsung gunakan jika sudah double
|
||||
else if (hama['nilai_pakar']
|
||||
is double) {
|
||||
nilaiPakar = hama['nilai_pakar'];
|
||||
}
|
||||
// Jika int, konversi ke double
|
||||
else if (hama['nilai_pakar'] is int) {
|
||||
nilaiPakar =
|
||||
hama['nilai_pakar'].toDouble();
|
||||
}
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(context) => EditHamaPage(
|
||||
idHama:
|
||||
hama['id'], // pastikan 'hama' adalah Map dari API kamu
|
||||
namaAwal: hama['nama'] ?? '',
|
||||
deskripsiAwal:
|
||||
hama['deskripsi'] ?? '',
|
||||
penangananAwal:
|
||||
hama['penanganan'] ?? '',
|
||||
gambarUrl: hama['foto'] ?? '',
|
||||
nilai_pakar: nilaiPakar,
|
||||
onHamaUpdated:
|
||||
_fetchHama, // fungsi untuk refresh list setelah update
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
// Button hapus di luar card
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.white),
|
||||
onPressed: () => _konfirmasiHapus(hama['id']),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.red),
|
||||
onPressed:
|
||||
() => _konfirmasiHapus(hama['id']),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DataRow(
|
||||
cells: [
|
||||
DataCell(Container()),
|
||||
DataCell(Container()),
|
||||
DataCell(
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
SizedBox(height: 16),
|
||||
// Pagination
|
||||
if (filteredHamaList.length > rowsPerPage)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.chevron_left),
|
||||
onPressed:
|
||||
currentPage > 0
|
||||
? () =>
|
||||
setState(() => currentPage--)
|
||||
onPressed: currentPage > 0
|
||||
? () => setState(() => currentPage--)
|
||||
: null,
|
||||
),
|
||||
Text(' ${currentPage + 1}'),
|
||||
Text(
|
||||
'Halaman ${currentPage + 1}',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.chevron_right),
|
||||
onPressed:
|
||||
(currentPage + 1) * rowsPerPage <
|
||||
hamaList.length
|
||||
? () =>
|
||||
setState(() => currentPage++)
|
||||
onPressed: (currentPage + 1) * rowsPerPage < filteredHamaList.length
|
||||
? () => setState(() => currentPage++)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(Container()),
|
||||
DataCell(Container()),
|
||||
DataCell(Container()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TambahHamaPage(
|
||||
onHamaAdded: _fetchHama,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Icon(Icons.add),
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,11 +12,21 @@ class PenyakitPage extends StatefulWidget {
|
|||
class _PenyakitPageState extends State<PenyakitPage> {
|
||||
final ApiService apiService = ApiService();
|
||||
List<Map<String, dynamic>> penyakitList = [];
|
||||
List<Map<String, dynamic>> filteredPenyakitList = [];
|
||||
TextEditingController searchController = TextEditingController();
|
||||
bool isSearchVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchPenyakit();
|
||||
searchController.addListener(_filterPenyakit);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchPenyakit() async {
|
||||
|
@ -24,19 +34,42 @@ class _PenyakitPageState extends State<PenyakitPage> {
|
|||
List<Map<String, dynamic>> 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<PenyakitPage> {
|
|||
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<PenyakitPage> {
|
|||
);
|
||||
}
|
||||
|
||||
void _navigateToEdit(Map<String, dynamic> penyakit) {
|
||||
// Parse nilai_pakar dengan aman
|
||||
double nilaiPakar = 0.0;
|
||||
if (penyakit['nilai_pakar'] != null) {
|
||||
// Coba parse jika string
|
||||
if (penyakit['nilai_pakar'] is String) {
|
||||
try {
|
||||
String nilaiStr = penyakit['nilai_pakar'].toString().trim();
|
||||
if (nilaiStr.isNotEmpty) {
|
||||
nilaiPakar = double.parse(nilaiStr.replaceAll(',', '.'));
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error parsing nilai_pakar: $e");
|
||||
}
|
||||
}
|
||||
// Langsung gunakan jika sudah double
|
||||
else if (penyakit['nilai_pakar'] is double) {
|
||||
nilaiPakar = penyakit['nilai_pakar'];
|
||||
}
|
||||
// Jika int, konversi ke double
|
||||
else if (penyakit['nilai_pakar'] is int) {
|
||||
nilaiPakar = penyakit['nilai_pakar'].toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EditPenyakitPage(
|
||||
idPenyakit: penyakit['id'],
|
||||
namaAwal: penyakit['nama'] ?? '',
|
||||
deskripsiAwal: penyakit['deskripsi'] ?? '',
|
||||
penangananAwal: penyakit['penanganan'] ?? '',
|
||||
gambarUrl: penyakit['foto'] ?? '',
|
||||
nilai_pakar: nilaiPakar,
|
||||
onPenyakitUpdated: _fetchPenyakit,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//pagination
|
||||
int currentPage = 0;
|
||||
int rowsPerPage = 10;
|
||||
|
@ -75,203 +149,169 @@ class _PenyakitPageState extends State<PenyakitPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int start = currentPage * rowsPerPage;
|
||||
int end =
|
||||
(start + rowsPerPage < penyakitList.length)
|
||||
int end = (start + rowsPerPage < filteredPenyakitList.length)
|
||||
? start + rowsPerPage
|
||||
: penyakitList.length;
|
||||
List currentPageData = penyakitList.sublist(start, end);
|
||||
: 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(
|
||||
SizedBox(height: 16),
|
||||
// Search Button
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: 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]),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _toggleSearch,
|
||||
icon: Icon(Icons.search),
|
||||
label: Text('Cari'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
),
|
||||
// Search Field (conditional)
|
||||
if (isSearchVisible)
|
||||
Container(
|
||||
margin: EdgeInsets.all(16),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari nama atau kode 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(8.0),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
columnSpacing: 20,
|
||||
headingRowColor: MaterialStateColor.resolveWith(
|
||||
(states) => const Color(0xFF9DC08D),
|
||||
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,
|
||||
),
|
||||
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')),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
penyakit['kode'] ?? '-',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
DataColumn(
|
||||
label: SizedBox(width: 100, child: Text('Deskripsi')),
|
||||
),
|
||||
DataColumn(
|
||||
label: SizedBox(width: 100, child: Text('Penanganan')),
|
||||
),
|
||||
DataColumn(
|
||||
label: SizedBox(width: 50, child: Text('Aksi')),
|
||||
),
|
||||
],
|
||||
rows: [
|
||||
...currentPageData.map(
|
||||
(penyakit) => DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text((penyakitList.indexOf(penyakit) + 1).toString()),
|
||||
),
|
||||
DataCell(Text(penyakit['kode'] ?? '-')),
|
||||
DataCell(Text(penyakit['nama'] ?? '-')),
|
||||
DataCell(Text(penyakit['deskripsi'] ?? '-')),
|
||||
DataCell(Text(penyakit['penanganan'] ?? '-')),
|
||||
DataCell(
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: Color(0xFF9DC08D),
|
||||
),
|
||||
onPressed: () {
|
||||
// Parse nilai_pakar dengan aman
|
||||
double nilaiPakar = 0.0;
|
||||
if (penyakit['nilai_pakar'] != null) {
|
||||
// Coba parse jika string
|
||||
if (penyakit['nilai_pakar'] is String) {
|
||||
try {
|
||||
String nilaiStr =
|
||||
penyakit['nilai_pakar']
|
||||
.toString()
|
||||
.trim();
|
||||
if (nilaiStr.isNotEmpty) {
|
||||
nilaiPakar = double.parse(
|
||||
nilaiStr.replaceAll(',', '.'),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print(
|
||||
"Error parsing nilai_pakar: $e",
|
||||
);
|
||||
}
|
||||
}
|
||||
// Langsung gunakan jika sudah double
|
||||
else if (penyakit['nilai_pakar']
|
||||
is double) {
|
||||
nilaiPakar = penyakit['nilai_pakar'];
|
||||
}
|
||||
// Jika int, konversi ke double
|
||||
else if (penyakit['nilai_pakar'] is int) {
|
||||
nilaiPakar =
|
||||
penyakit['nilai_pakar'].toDouble();
|
||||
}
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(context) => EditPenyakitPage(
|
||||
idPenyakit:
|
||||
penyakit['id'], // pastikan 'hama' adalah Map dari API kamu
|
||||
namaAwal: penyakit['nama'] ?? '',
|
||||
deskripsiAwal:
|
||||
penyakit['deskripsi'] ?? '',
|
||||
penangananAwal:
|
||||
penyakit['penanganan'] ?? '',
|
||||
gambarUrl:
|
||||
penyakit['foto'] ?? '',
|
||||
nilai_pakar: nilaiPakar,
|
||||
onPenyakitUpdated:
|
||||
_fetchPenyakit, // fungsi untuk refresh list setelah update
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
// Button hapus di luar card
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.white),
|
||||
onPressed: () => _konfirmasiHapus(penyakit['id']),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.red),
|
||||
onPressed:
|
||||
() => _konfirmasiHapus(penyakit['id']),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DataRow(
|
||||
cells: [
|
||||
DataCell(Container()),
|
||||
DataCell(Container()),
|
||||
DataCell(
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
SizedBox(height: 16),
|
||||
// Pagination
|
||||
if (filteredPenyakitList.length > rowsPerPage)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.chevron_left),
|
||||
onPressed:
|
||||
currentPage > 0
|
||||
? () =>
|
||||
setState(() => currentPage--)
|
||||
onPressed: currentPage > 0
|
||||
? () => setState(() => currentPage--)
|
||||
: null,
|
||||
),
|
||||
Text(' ${currentPage + 1}'),
|
||||
Text(
|
||||
'Halaman ${currentPage + 1}',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.chevron_right),
|
||||
onPressed:
|
||||
(currentPage + 1) * rowsPerPage <
|
||||
penyakitList.length
|
||||
? () =>
|
||||
setState(() => currentPage++)
|
||||
onPressed: (currentPage + 1) * rowsPerPage < filteredPenyakitList.length
|
||||
? () => setState(() => currentPage++)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(Container()),
|
||||
DataCell(Container()),
|
||||
DataCell(Container()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TambahPenyakitPage(
|
||||
onPenyakitAdded: _fetchPenyakit,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Icon(Icons.add),
|
||||
backgroundColor: Color(0xFF9DC08D),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<RulePage> {
|
|||
List<Map<String, dynamic>> hamaList = [];
|
||||
|
||||
List<dynamic> rules = [];
|
||||
List<dynamic> 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<RulePage> {
|
|||
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<RulePage> {
|
|||
...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<RulePage> {
|
|||
'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<RulePage> {
|
|||
'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<RulePage> {
|
|||
}
|
||||
|
||||
Future<void> deleteRule(Map<String, dynamic> rule) async {
|
||||
// Tampilkan dialog konfirmasi
|
||||
bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Konfirmasi Hapus'),
|
||||
content: Text('Apakah Anda yakin ingin menghapus rule ini?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Batal'),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Hapus', style: TextStyle(color: Colors.red)),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
try {
|
||||
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<RulePage> {
|
|||
}
|
||||
} 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,27 +226,80 @@ class _RulePageState extends State<RulePage> {
|
|||
// Get paginated data
|
||||
List<dynamic> 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
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari berdasarkan nama penyakit, hama, atau gejala...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: selectedFilter,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Filter Kategori',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
items: ['Semua', 'Penyakit', 'Hama'].map((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
selectedFilter = newValue!;
|
||||
_filterRules();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Total: ${filteredRules.length} rule(s)',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
|
@ -166,31 +307,27 @@ class _RulePageState extends State<RulePage> {
|
|||
MaterialPageRoute(
|
||||
builder: (context) => TambahRulePage(
|
||||
isEditing: false,
|
||||
isEditingHama: true, // Menandakan ini adalah rule hama
|
||||
isEditingHama: true,
|
||||
selectedRuleIds: [],
|
||||
selectedGejalaIds: [],
|
||||
nilaiPakarList: [],
|
||||
selectedHamaId: null,
|
||||
selectedPenyakitId: null,
|
||||
showHamaOnly: true, // Parameter baru untuk menampilkan hanya dropdown hama
|
||||
showHamaOnly: true,
|
||||
),
|
||||
),
|
||||
).then((_) => fetchRules());
|
||||
},
|
||||
icon: Icon(Icons.bug_report, size: 16,),
|
||||
label: Text(
|
||||
"Tambah Rule Hama",
|
||||
style: TextStyle(fontSize: 12)),
|
||||
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: 8, vertical: 6), // Padding lebih kecil
|
||||
minimumSize: Size(0, 32), // Tinggi minimum lebih kecil
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
minimumSize: Size(0, 36),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
// Button untuk tambah rule penyakit
|
||||
SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
|
@ -198,85 +335,194 @@ class _RulePageState extends State<RulePage> {
|
|||
MaterialPageRoute(
|
||||
builder: (context) => TambahRulePage(
|
||||
isEditing: false,
|
||||
isEditingHama: false, // Menandakan ini adalah rule penyakit
|
||||
isEditingHama: false,
|
||||
selectedRuleIds: [],
|
||||
selectedGejalaIds: [],
|
||||
nilaiPakarList: [],
|
||||
selectedHamaId: null,
|
||||
selectedPenyakitId: null,
|
||||
showPenyakitOnly: true, // Parameter baru untuk menampilkan hanya dropdown penyakit
|
||||
showPenyakitOnly: true,
|
||||
),
|
||||
),
|
||||
).then((_) => fetchRules());
|
||||
},
|
||||
icon: Icon(Icons.healing, size: 16,),
|
||||
label: Text(
|
||||
"Tambah Rule Penyakit",
|
||||
style: TextStyle(fontSize: 12),),
|
||||
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: 8, vertical: 6), // Padding lebih kecil
|
||||
minimumSize: Size(0, 32), // Tinggi minimum lebih kecil
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Mengurangi area tap
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
minimumSize: Size(0, 36),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Expanded(
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataList() {
|
||||
if (paginatedRules.isEmpty) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: DataTable(
|
||||
headingRowColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
return Color(0xFF9DC08D); // Apply color to all header rows
|
||||
},
|
||||
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'),
|
||||
),
|
||||
columns: const [
|
||||
DataColumn(label: Text('No')),
|
||||
DataColumn(label: Text('Hama & Penyakit')),
|
||||
DataColumn(label: Text('Gejala')),
|
||||
DataColumn(label: Text('Nilai Pakar')),
|
||||
DataColumn(label: Text('Aksi')),
|
||||
],
|
||||
rows: List.generate(paginatedRules.length, (index) {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: paginatedRules.length,
|
||||
itemBuilder: (context, index) {
|
||||
final rule = paginatedRules[index];
|
||||
final displayIndex = currentPage * rowsPerPage + index + 1;
|
||||
|
||||
final namaKategori = rule['id_penyakit'] != null
|
||||
? rule['nama_penyakit'] ?? '-'
|
||||
: rule['nama_hama'] ?? '-';
|
||||
|
||||
final isPenyakit = rule['id_penyakit'] != null;
|
||||
final kategori = rule['type'] ?? 'Unknown';
|
||||
final isHama = rule['id_hama'] != null;
|
||||
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text(displayIndex.toString())),
|
||||
DataCell(Text(namaKategori)),
|
||||
DataCell(Text(rule['nama_gejala'] ?? '-')),
|
||||
DataCell(Text(rule['nilai_pakar']?.toString() ?? '-')),
|
||||
DataCell(
|
||||
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: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.edit,
|
||||
color: Colors.orange,
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isHama ? Colors.green[100] : Colors.blue[100],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onPressed: () {
|
||||
child: Text(
|
||||
kategori,
|
||||
style: TextStyle(
|
||||
color: isHama ? Colors.green[800] : Colors.blue[800],
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
namaKategori,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
// Gejala
|
||||
Text(
|
||||
'Gejala: ${rule['nama_gejala'] ?? '-'}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
// Nilai pakar
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Nilai Pakar: ',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF9DC08D),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
rule['nilai_pakar']?.toString() ?? '-',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFFFFFFF),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
// Button hapus di luar card
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.white),
|
||||
onPressed: () => deleteRule(rule),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToEdit(Map<String, dynamic> rule) {
|
||||
if (rule != null &&
|
||||
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(
|
||||
|
@ -290,66 +536,104 @@ class _RulePageState extends State<RulePage> {
|
|||
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"),
|
||||
content: Text("Data rule tidak lengkap"),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
// Debug info
|
||||
print("Rule data: $rule");
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
onPressed: () {
|
||||
deleteRule(rule);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Pagination controls
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
}
|
||||
|
||||
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.center,
|
||||
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,
|
||||
),
|
||||
Text('Halaman ${currentPage + 1} dari ${(rules.length / rowsPerPage).ceil()}'),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Text('${currentPage + 1} / $totalPages'),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.chevron_right),
|
||||
onPressed: (currentPage + 1) * rowsPerPage < rules.length
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -42,32 +42,39 @@ class _TambahHamaPageState extends State<TambahHamaPage> {
|
|||
deskripsiController.text.isNotEmpty &&
|
||||
penangananController.text.isNotEmpty) {
|
||||
try {
|
||||
double? nilaipakar;
|
||||
if (nilaiPakarController.text.isNotEmpty) {
|
||||
// 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);
|
||||
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, // boleh null
|
||||
nilaiPakar,
|
||||
);
|
||||
|
||||
widget.onHamaAdded();
|
||||
Navigator.pop(context);
|
||||
_showDialog('Berhasil', 'Data hama berhasil ditambahkan.');
|
||||
} catch (e) {
|
||||
_showDialog('Gagal', 'Gagal menambahkan data hama.');
|
||||
_showDialog('Gagal', 'Gagal menambahkan data hama: $e');
|
||||
print("Error adding hama: $e");
|
||||
}
|
||||
} else {
|
||||
_showDialog('Error', 'Semua field harus diisi (kecuali nilai pakar).');
|
||||
_showDialog('Error', 'Nama, deskripsi, dan penanganan hama harus diisi.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _showDialog(String title, String message) {
|
||||
showDialog(
|
||||
|
@ -149,8 +156,11 @@ class _TambahHamaPageState extends State<TambahHamaPage> {
|
|||
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'),
|
||||
|
|
|
@ -39,11 +39,23 @@ class _TambahPenyakitPageState extends State<TambahPenyakitPage> {
|
|||
Future<void> _simpanPenyakit() async {
|
||||
if (namaController.text.isNotEmpty &&
|
||||
deskripsiController.text.isNotEmpty &&
|
||||
penangananController.text.isNotEmpty &&
|
||||
nilaiPakarController.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(',', '.');
|
||||
double nilaiPakar = double.parse(nilaiInput);
|
||||
nilaiPakar = double.parse(nilaiInput);
|
||||
}
|
||||
|
||||
print("Debug - Nilai Pakar Double: $nilaiPakar");
|
||||
|
||||
print("Debug - Nama: ${namaController.text}");
|
||||
print("Debug - Deskripsi: ${deskripsiController.text}");
|
||||
print("Debug - Penanganan: ${penangananController.text}");
|
||||
print("Debug - Nilai Pakar: $nilaiPakar");
|
||||
print("Debug - Image File: $_pickedFile");
|
||||
|
||||
await apiService.createPenyakit(
|
||||
namaController.text,
|
||||
deskripsiController.text,
|
||||
|
@ -53,13 +65,13 @@ class _TambahPenyakitPageState extends State<TambahPenyakitPage> {
|
|||
);
|
||||
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<TambahPenyakitPage> {
|
|||
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(
|
||||
|
|
|
@ -9,6 +9,7 @@ class UserListPage extends StatefulWidget {
|
|||
class _UserListPageState extends State<UserListPage> {
|
||||
final ApiService apiService = ApiService();
|
||||
List<Map<String, dynamic>> users = [];
|
||||
List<Map<String, dynamic>> filteredUsers = [];
|
||||
bool isLoading = true;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
@ -17,6 +18,7 @@ class _UserListPageState extends State<UserListPage> {
|
|||
final _passwordController = TextEditingController();
|
||||
final _alamatController = TextEditingController();
|
||||
final _nomorTeleponController = TextEditingController();
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -26,12 +28,12 @@ class _UserListPageState extends State<UserListPage> {
|
|||
|
||||
@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<UserListPage> {
|
|||
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<UserListPage> {
|
|||
|
||||
Future<void> _updateUser(Map<String, dynamic> user) async {
|
||||
try {
|
||||
// Hanya kirim password jika diisi
|
||||
String? newPassword =
|
||||
_passwordController.text.isEmpty ? null : _passwordController.text;
|
||||
|
||||
|
@ -82,22 +80,19 @@ class _UserListPageState extends State<UserListPage> {
|
|||
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,31 +113,10 @@ class _UserListPageState extends State<UserListPage> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteUser(int userId) async {
|
||||
try {
|
||||
await apiService.deleteUser(userId);
|
||||
await _loadUsers();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('User berhasil dihapus'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gagal menghapus user: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(Map<String, dynamic> user) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Konfirmasi Hapus'),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus user ${user['name']}?',
|
||||
|
@ -155,9 +129,9 @@ class _UserListPageState extends State<UserListPage> {
|
|||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
Navigator.pop(context); // Close dialog first
|
||||
Navigator.pop(context);
|
||||
await apiService.deleteUser(user['id']);
|
||||
await _loadUsers(); // Refresh the list
|
||||
await _loadUsers();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
@ -183,17 +157,15 @@ class _UserListPageState extends State<UserListPage> {
|
|||
}
|
||||
|
||||
void _showUpdateDialog(Map<String, dynamic> 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(
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Update User'),
|
||||
content: SingleChildScrollView(
|
||||
child: Form(
|
||||
|
@ -204,18 +176,14 @@ class _UserListPageState extends State<UserListPage> {
|
|||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(labelText: 'Nama'),
|
||||
validator:
|
||||
(value) =>
|
||||
value?.isEmpty ?? true
|
||||
? 'Nama tidak boleh kosong'
|
||||
: null,
|
||||
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?.isEmpty ?? true) return 'Email tidak boleh kosong';
|
||||
if (!value!.contains('@')) return 'Email tidak valid';
|
||||
return null;
|
||||
},
|
||||
|
@ -224,14 +192,12 @@ class _UserListPageState extends State<UserListPage> {
|
|||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password Baru',
|
||||
helperText:
|
||||
'Kosongkan jika tidak ingin mengubah password',
|
||||
helperText: 'Kosongkan jika tidak ingin mengubah password',
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
if (value?.isNotEmpty ?? false) {
|
||||
if (value!.length < 6)
|
||||
return 'Password minimal 6 karakter';
|
||||
if (value!.length < 6) return 'Password minimal 6 karakter';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
@ -239,21 +205,15 @@ class _UserListPageState extends State<UserListPage> {
|
|||
TextFormField(
|
||||
controller: _alamatController,
|
||||
decoration: InputDecoration(labelText: 'Alamat'),
|
||||
validator:
|
||||
(value) =>
|
||||
value?.isEmpty ?? true
|
||||
? 'Alamat tidak boleh kosong'
|
||||
: null,
|
||||
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,
|
||||
validator: (value) =>
|
||||
value?.isEmpty ?? true ? 'Nomor telepon tidak boleh kosong' : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -282,10 +242,15 @@ class _UserListPageState extends State<UserListPage> {
|
|||
}
|
||||
|
||||
void _showAddUserDialog() {
|
||||
_nameController.clear();
|
||||
_emailController.clear();
|
||||
_passwordController.clear();
|
||||
_alamatController.clear();
|
||||
_nomorTeleponController.clear();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Tambah User Baru'),
|
||||
content: SingleChildScrollView(
|
||||
child: Form(
|
||||
|
@ -382,6 +347,7 @@ class _UserListPageState extends State<UserListPage> {
|
|||
final userList = await apiService.getUsers();
|
||||
setState(() {
|
||||
users = userList;
|
||||
filteredUsers = userList;
|
||||
isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -392,6 +358,35 @@ class _UserListPageState extends State<UserListPage> {
|
|||
}
|
||||
}
|
||||
|
||||
void _filterUsers(String query) {
|
||||
setState(() {
|
||||
if (query.isEmpty) {
|
||||
filteredUsers = users;
|
||||
} else {
|
||||
filteredUsers = users.where((user) {
|
||||
final name = user['name']?.toString().toLowerCase() ?? '';
|
||||
final role = user['role']?.toString().toLowerCase() ?? '';
|
||||
final searchQuery = query.toLowerCase();
|
||||
|
||||
return name.contains(searchQuery) || role.contains(searchQuery);
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _navigateToUserDetail(Map<String, dynamic> user) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UserDetailPage(
|
||||
user: user,
|
||||
onUserUpdated: _loadUsers,
|
||||
onUserDeleted: _loadUsers,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -404,77 +399,475 @@ class _UserListPageState extends State<UserListPage> {
|
|||
backgroundColor: Color(0xFF9DC08D),
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
body:
|
||||
isLoading
|
||||
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(
|
||||
: Column(
|
||||
children: [
|
||||
// Search Bar
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
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]!),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
user['role'] == 'admin'
|
||||
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.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<String, dynamic> user;
|
||||
final VoidCallback onUserUpdated;
|
||||
final VoidCallback onUserDeleted;
|
||||
|
||||
const UserDetailPage({
|
||||
Key? key,
|
||||
required this.user,
|
||||
required this.onUserUpdated,
|
||||
required this.onUserDeleted,
|
||||
}) : super(key: key);
|
||||
|
||||
void _showDeleteConfirmation(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Konfirmasi Hapus'),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus user ${user['name']}?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
Navigator.pop(context); // Close dialog
|
||||
final apiService = ApiService();
|
||||
await apiService.deleteUser(user['id']);
|
||||
|
||||
Navigator.pop(context); // Go back to list
|
||||
onUserDeleted(); // Refresh list
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('User berhasil dihapus'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gagal menghapus user: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showUpdateDialog(BuildContext context) {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController(text: user['name'] ?? '');
|
||||
final _emailController = TextEditingController(text: user['email'] ?? '');
|
||||
final _alamatController = TextEditingController(text: user['alamat'] ?? '');
|
||||
final _nomorTeleponController = TextEditingController(text: user['nomorTelepon'] ?? '');
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Update User'),
|
||||
content: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -899,7 +899,7 @@ Future<List<Map<String, dynamic>>> getAllHistori() async {
|
|||
String deskripsi,
|
||||
String penanganan,
|
||||
XFile? pickedFile,
|
||||
double nilai_pakar
|
||||
double? nilai_pakar
|
||||
) async {
|
||||
try {
|
||||
var uri = Uri.parse(penyakitUrl);
|
||||
|
|
|
@ -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<LoginPage> {
|
|||
}
|
||||
|
||||
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<LoginPage> {
|
|||
child: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => BeforeLogin()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
Loading…
Reference in New Issue