feat: improve riwayat filters, refresh, and daily grouping

This commit is contained in:
Wizznu 2026-03-27 23:51:13 +07:00
parent f51a0ef11f
commit 692e74ad6c
1 changed files with 308 additions and 210 deletions

View File

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