feat: improve riwayat filters, refresh, and daily grouping
This commit is contained in:
parent
f51a0ef11f
commit
692e74ad6c
|
|
@ -13,12 +13,15 @@ class RiwayatPage extends StatefulWidget {
|
||||||
class _RiwayatPageState extends State<RiwayatPage> {
|
class _RiwayatPageState extends State<RiwayatPage> {
|
||||||
DateTime? _selectedStartDate;
|
DateTime? _selectedStartDate;
|
||||||
DateTime? _selectedEndDate;
|
DateTime? _selectedEndDate;
|
||||||
|
String _selectedKandang = 'semua';
|
||||||
|
DateTime? _lastRefreshAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedStartDate = DateTime.now().subtract(const Duration(days: 7));
|
_selectedStartDate = DateTime.now().subtract(const Duration(days: 7));
|
||||||
_selectedEndDate = DateTime.now();
|
_selectedEndDate = DateTime.now();
|
||||||
|
_lastRefreshAt = DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -123,6 +126,68 @@ class _RiwayatPageState extends State<RiwayatPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Filter Kandang',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedKandang,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'semua',
|
||||||
|
child: Text('Semua Kandang'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'kandang1',
|
||||||
|
child: Text('Kandang 1'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'kandang2',
|
||||||
|
child: Text('Kandang 2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() {
|
||||||
|
_selectedKandang = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_lastRefreshAt != null
|
||||||
|
? 'Update: ${DateFormat('dd/MM/yyyy HH:mm').format(_lastRefreshAt!)}'
|
||||||
|
: 'Belum pernah refresh',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: _handleRefresh,
|
||||||
|
icon: const Icon(Icons.refresh, size: 16),
|
||||||
|
label: const Text('Refresh'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -138,40 +203,28 @@ class _RiwayatPageState extends State<RiwayatPage> {
|
||||||
Widget _buildRiwayatList() {
|
Widget _buildRiwayatList() {
|
||||||
final panenProvider = context.watch<PanenProvider>();
|
final panenProvider = context.watch<PanenProvider>();
|
||||||
|
|
||||||
List<dynamic> panens = [];
|
final panens = _getFilteredPanens(panenProvider);
|
||||||
if (_selectedStartDate != null && _selectedEndDate != null) {
|
final groupedPanens = <DateTime, List<dynamic>>{};
|
||||||
panens = panenProvider.getPanenByDateRange(
|
for (final panen in panens) {
|
||||||
_selectedStartDate!,
|
final key = DateTime(
|
||||||
_selectedEndDate!,
|
panen.tanggalPanen.year,
|
||||||
|
panen.tanggalPanen.month,
|
||||||
|
panen.tanggalPanen.day,
|
||||||
);
|
);
|
||||||
|
groupedPanens.putIfAbsent(key, () => []).add(panen);
|
||||||
|
}
|
||||||
|
final sortedDates = groupedPanens.keys.toList()
|
||||||
|
..sort((a, b) => b.compareTo(a));
|
||||||
|
|
||||||
|
for (final key in sortedDates) {
|
||||||
|
groupedPanens[key]!
|
||||||
|
.sort((a, b) => b.tanggalPanen.compareTo(a.tanggalPanen));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panens.isEmpty) {
|
final totalTelur =
|
||||||
return Center(
|
panens.fold<int>(0, (sum, p) => sum + (p.jumlahTelur as int));
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text('📭', style: TextStyle(fontSize: 64)),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Tidak ada data panen',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
final listChildren = <Widget>[
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Total Harian Summary
|
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
|
|
@ -206,7 +259,7 @@ class _RiwayatPageState extends State<RiwayatPage> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${panens.isEmpty ? 0 : panens.map<int>((p) => p.jumlahTelur).reduce((a, b) => a + b)}',
|
'$totalTelur',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 36,
|
fontSize: 36,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -223,7 +276,7 @@ class _RiwayatPageState extends State<RiwayatPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text('🥚', style: TextStyle(fontSize: 48)),
|
const Text('🥚', style: TextStyle(fontSize: 48)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
@ -238,8 +291,6 @@ class _RiwayatPageState extends State<RiwayatPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Breakdown per hari
|
|
||||||
Text(
|
Text(
|
||||||
'Detail Panen Per Hari',
|
'Detail Panen Per Hari',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
@ -249,17 +300,39 @@ class _RiwayatPageState extends State<RiwayatPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
ListView.builder(
|
];
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: panens.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final panen = panens[index];
|
|
||||||
final tanggal = DateFormat(
|
|
||||||
'dd MMMM yyyy',
|
|
||||||
).format(panen.tanggalPanen);
|
|
||||||
|
|
||||||
return Container(
|
if (panens.isEmpty) {
|
||||||
|
listChildren.add(
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 48),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text('📭', style: TextStyle(fontSize: 64)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Tidak ada data panen',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for (final date in sortedDates) {
|
||||||
|
final items = groupedPanens[date]!;
|
||||||
|
final dailyTotal =
|
||||||
|
items.fold<int>(0, (sum, p) => sum + (p.jumlahTelur as int));
|
||||||
|
|
||||||
|
listChildren.add(
|
||||||
|
Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|
@ -279,104 +352,129 @@ class _RiwayatPageState extends State<RiwayatPage> {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
panen.kandangNama,
|
DateFormat('dd MMMM yyyy').format(date),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
Text(
|
||||||
'$tanggal • ${panen.jam}',
|
'$dailyTotal telur',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.orange.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'${panen.jumlahTelur}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.orange.shade700,
|
color: Colors.orange.shade700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('🥚', style: TextStyle(fontSize: 16)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Row(
|
...items.map(
|
||||||
children: [
|
(panen) => Container(
|
||||||
Container(
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.all(10),
|
||||||
horizontal: 10,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: (panen.jenisPanen ?? '').toLowerCase() ==
|
color: Colors.grey.shade50,
|
||||||
'sore'
|
borderRadius: BorderRadius.circular(10),
|
||||||
? Colors.blue.shade50
|
|
||||||
: Colors.green.shade50,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Row(
|
||||||
(panen.jenisPanen ?? 'manual').toUpperCase(),
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
panen.kandangNama,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'${panen.jam} • ${(panen.jenisPanen ?? 'manual').toUpperCase()}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.bold,
|
color: Colors.grey.shade600,
|
||||||
color: (panen.jenisPanen ?? '').toLowerCase() ==
|
|
||||||
'sore'
|
|
||||||
? Colors.blue.shade700
|
|
||||||
: Colors.green.shade700,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
Expanded(
|
Text(
|
||||||
child: Text(
|
'${panen.jumlahTelur} 🥚',
|
||||||
panen.catatan,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 14,
|
||||||
color: Colors.grey.shade700,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
),
|
}
|
||||||
],
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: _handleRefresh,
|
||||||
|
child: ListView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: listChildren,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<dynamic> _getFilteredPanens(PanenProvider panenProvider) {
|
||||||
|
final allPanens = List<dynamic>.from(panenProvider.panens);
|
||||||
|
final startDate = _selectedStartDate;
|
||||||
|
final endDate = _selectedEndDate;
|
||||||
|
|
||||||
|
final startBoundary = startDate != null
|
||||||
|
? DateTime(startDate.year, startDate.month, startDate.day)
|
||||||
|
: null;
|
||||||
|
final endBoundary = endDate != null
|
||||||
|
? DateTime(endDate.year, endDate.month, endDate.day, 23, 59, 59, 999)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final filtered = allPanens.where((panen) {
|
||||||
|
final inDateRange = (startBoundary == null ||
|
||||||
|
!panen.tanggalPanen.isBefore(startBoundary)) &&
|
||||||
|
(endBoundary == null || !panen.tanggalPanen.isAfter(endBoundary));
|
||||||
|
|
||||||
|
if (!inDateRange) return false;
|
||||||
|
|
||||||
|
if (_selectedKandang == 'semua') return true;
|
||||||
|
|
||||||
|
final kandangIdLower = panen.kandangId.toString().toLowerCase();
|
||||||
|
final kandangNamaLower = panen.kandangNama.toString().toLowerCase();
|
||||||
|
final normalized =
|
||||||
|
'$kandangIdLower $kandangNamaLower'.replaceAll('_', '');
|
||||||
|
return normalized.contains(_selectedKandang);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
filtered.sort((a, b) => b.tanggalPanen.compareTo(a.tanggalPanen));
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleRefresh() async {
|
||||||
|
final panenProvider = context.read<PanenProvider>();
|
||||||
|
await panenProvider.loadTodaySnapshots();
|
||||||
|
await panenProvider.restorePanenHistoryFromFirebase();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_lastRefreshAt = DateTime.now();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _selectStartDate() async {
|
Future<void> _selectStartDate() async {
|
||||||
final DateTime? picked = await showDatePicker(
|
final DateTime? picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue