TKK_E32231105/lib/screens/riwayat_page.dart

654 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../models/panen_model.dart';
import '../providers/panen_provider.dart';
import '../services/excel_export_service.dart';
class RiwayatPage extends StatefulWidget {
const RiwayatPage({super.key});
@override
State<RiwayatPage> createState() => _RiwayatPageState();
}
class _RiwayatPageState extends State<RiwayatPage> {
DateTime? _selectedStartDate;
DateTime? _selectedEndDate;
String _selectedKandang = 'semua';
DateTime? _lastRefreshAt;
@override
void initState() {
super.initState();
_selectedStartDate = DateTime.now().subtract(const Duration(days: 7));
_selectedEndDate = DateTime.now();
_lastRefreshAt = DateTime.now();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.orange.shade100,
Colors.amber.shade100,
Colors.greenAccent.shade100,
],
),
),
child: SafeArea(
child: Column(
children: [
// Filter Section
Container(
color: Colors.white,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filter Tanggal',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: InkWell(
onTap: _selectStartDate,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.calendar_today, size: 18),
const SizedBox(width: 8),
Text(
_selectedStartDate != null
? DateFormat(
'dd/MM/yyyy',
).format(_selectedStartDate!)
: 'Mulai',
style: const TextStyle(fontSize: 12),
),
],
),
),
),
),
const SizedBox(width: 8),
Text(
'sd',
style: TextStyle(color: Colors.grey.shade600),
),
const SizedBox(width: 8),
Expanded(
child: InkWell(
onTap: _selectEndDate,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.calendar_today, size: 18),
const SizedBox(width: 8),
Text(
_selectedEndDate != null
? DateFormat(
'dd/MM/yyyy',
).format(_selectedEndDate!)
: 'Akhir',
style: const TextStyle(fontSize: 12),
),
],
),
),
),
),
],
),
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),
LayoutBuilder(
builder: (context, constraints) {
final compact = constraints.maxWidth < 520;
final updateText = _lastRefreshAt != null
? 'Update: ${DateFormat('dd/MM/yyyy HH:mm').format(_lastRefreshAt!)}'
: 'Belum pernah refresh';
final actionButtons = Wrap(
spacing: 8,
runSpacing: 8,
children: [
TextButton.icon(
onPressed: _handleRefresh,
icon: const Icon(Icons.refresh, size: 16),
label: const Text('Refresh'),
),
ElevatedButton.icon(
onPressed: _downloadExcelReport,
icon: const Icon(Icons.download, size: 16),
label: const Text('Download Excel'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
),
),
],
);
if (compact) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
updateText,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
actionButtons,
],
);
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
updateText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
),
const SizedBox(width: 8),
actionButtons,
],
);
},
),
],
),
),
// Content
Expanded(child: _buildRiwayatList()),
],
),
),
),
);
}
Widget _buildRiwayatList() {
final panenProvider = context.watch<PanenProvider>();
final panens = _getFilteredPanens(panenProvider);
final groupedPanens = <DateTime, List<Panen>>{};
for (final panen in panens) {
final key = DateTime(
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));
}
final totalTelur = panens.fold<int>(0, (sum, p) => sum + p.jumlahTelur);
final listChildren = <Widget>[
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.orange.shade300, Colors.orange.shade600],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.orange.shade300.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Produksi',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$totalTelur',
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'telur',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.8),
),
),
],
),
const Text('🥚', style: TextStyle(fontSize: 48)),
],
),
const SizedBox(height: 12),
Text(
'${panens.length} pencatatan panen',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
const SizedBox(height: 24),
Text(
'Detail Panen Per Hari',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 12),
];
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);
listChildren.add(
Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('dd MMMM yyyy').format(date),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
...items.map(
(panen) => Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(10),
),
child: Row(
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(
fontSize: 11,
color: Colors.grey.shade600,
),
),
],
),
),
Text(
'${panen.jumlahTelur} 🥚',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
],
),
),
),
const SizedBox(height: 6),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Total telur hari ini: $dailyTotal',
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.orange.shade800,
),
),
),
],
),
),
),
);
}
}
return RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
children: listChildren,
),
);
}
List<Panen> _getFilteredPanens(PanenProvider panenProvider) {
final allPanens = List<Panen>.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 {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedStartDate ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
_selectedStartDate = picked;
});
}
}
Future<void> _selectEndDate() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedEndDate ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
_selectedEndDate = picked;
});
}
}
Future<void> _downloadExcelReport() async {
if (_selectedStartDate == null || _selectedEndDate == null) {
_showErrorMessage('Pilih tanggal mulai dan akhir terlebih dahulu');
return;
}
if (_selectedStartDate!.isAfter(_selectedEndDate!)) {
_showErrorMessage(
'Tanggal mulai tidak boleh lebih besar dari tanggal akhir');
return;
}
try {
_showLoadingDialog('Membuat laporan Excel...');
final panenProvider = context.read<PanenProvider>();
final panens = _getFilteredPanens(panenProvider);
final filePath = await ExcelExportService.exportWeeklyReport(
panens: panens,
startDate: _selectedStartDate!,
endDate: _selectedEndDate!,
kandangFilter: _selectedKandang,
);
if (!mounted) return;
Navigator.of(context).pop(); // Close loading dialog
_showSuccessMessage('Laporan berhasil dibuat:\n$filePath');
} catch (e) {
if (!mounted) return;
Navigator.of(context).pop(); // Close loading dialog
_showErrorMessage('Gagal membuat laporan: $e');
}
}
void _showLoadingDialog(String message) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(message),
],
),
);
},
);
}
void _showSuccessMessage(String message) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('✓ Sukses'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
);
},
);
}
void _showErrorMessage(String message) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('✗ Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
);
},
);
}
}