This commit is contained in:
kleponijo 2026-05-27 17:18:13 +07:00
parent 9659dc5edf
commit 9b6362a61d
1 changed files with 381 additions and 99 deletions

View File

@ -21,7 +21,7 @@ class WindSpeedScreen extends StatefulWidget {
} }
class _WindSpeedScreenState extends State<WindSpeedScreen> { class _WindSpeedScreenState extends State<WindSpeedScreen> {
// Date picker // Date picker (untuk filter riwayat di layar utama)
Future<void> _pickDate(BuildContext context, WindSpeedState state) async { Future<void> _pickDate(BuildContext context, WindSpeedState state) async {
DateTime firstDate = DateTime.now().subtract(const Duration(days: 365)); DateTime firstDate = DateTime.now().subtract(const Duration(days: 365));
DateTime lastDate = DateTime.now(); DateTime lastDate = DateTime.now();
@ -58,7 +58,9 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
} }
} }
// Dialog export: nama file + date range //
// Dialog export: nama file + filter tanggal + filter jam
//
Future<void> _showExportDialog( Future<void> _showExportDialog(
BuildContext context, WindSpeedState state) async { BuildContext context, WindSpeedState state) async {
// Tentukan batas tanggal dari history // Tentukan batas tanggal dari history
@ -79,14 +81,48 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
); );
DateTime? dateFrom; DateTime? dateFrom;
DateTime? dateTo; DateTime? dateTo;
TimeOfDay? timeFrom;
TimeOfDay? timeTo;
await showDialog( await showDialog(
context: context, context: context,
builder: (ctx) => StatefulBuilder( builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) { builder: (ctx, setDialogState) {
final fmt = DateFormat('dd MMM yyyy', 'id_ID'); final dateFmt = DateFormat('dd MMM yyyy', 'id_ID');
Future<void> pickRange() async { // Hitung jumlah data sesuai filter aktif
int countFiltered() {
return state.history.where((e) {
bool passDate = true;
bool passTime = true;
if (dateFrom != null && dateTo != null) {
final d = DateTime(
e.timestamp.year,
e.timestamp.month,
e.timestamp.day,
);
final from = DateTime(
dateFrom!.year,
dateFrom!.month,
dateFrom!.day,
);
final to = DateTime(dateTo!.year, dateTo!.month, dateTo!.day);
passDate = !d.isBefore(from) && !d.isAfter(to);
}
if (timeFrom != null && timeTo != null) {
final h = e.timestamp.hour;
final fromH = timeFrom!.hour;
final toH = timeTo!.hour;
// Support overnight range (mis. 2302)
passTime = fromH <= toH
? h >= fromH && h <= toH
: h >= fromH || h <= toH;
}
return passDate && passTime;
}).length;
}
Future<void> pickDateRange() async {
final range = await showDateRangePicker( final range = await showDateRangePicker(
context: ctx, context: ctx,
firstDate: firstDate, firstDate: firstDate,
@ -114,129 +150,368 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
} }
} }
Future<void> pickTime(bool isFrom) async {
final picked = await showTimePicker(
context: ctx,
initialTime: isFrom
? (timeFrom ?? const TimeOfDay(hour: 0, minute: 0))
: (timeTo ?? const TimeOfDay(hour: 23, minute: 59)),
builder: (ctx, child) => MediaQuery(
data: MediaQuery.of(ctx).copyWith(alwaysUse24HourFormat: true),
child: Theme(
data: Theme.of(ctx).copyWith(
colorScheme: ColorScheme.light(
primary: Colors.blue.shade700,
onPrimary: Colors.white,
),
),
child: child!,
),
),
);
if (picked != null) {
setDialogState(() {
if (isFrom)
timeFrom = picked;
else
timeTo = picked;
});
}
}
String fmtTime(TimeOfDay t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
final filtered = countFiltered();
final hasAnyFilter = dateFrom != null || timeFrom != null;
return AlertDialog( return AlertDialog(
shape: shape: RoundedRectangleBorder(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), borderRadius: BorderRadius.circular(20),
),
title: Row( title: Row(
children: [ children: [
Icon(Icons.file_download_outlined, color: Colors.blue.shade700), Icon(Icons.file_download_outlined, color: Colors.blue.shade700),
const SizedBox(width: 10), const SizedBox(width: 10),
const Text('Export Excel', const Text(
style: 'Export Excel',
TextStyle(fontSize: 17, fontWeight: FontWeight.bold)), style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
),
], ],
), ),
content: SizedBox( content: SingleChildScrollView(
width: 340, child: SizedBox(
child: Column( width: 340,
mainAxisSize: MainAxisSize.min, child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
// Nama file children: [
const Text('Nama file', // Nama file
const Text(
'Nama file',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.black54)), color: Colors.black54,
const SizedBox(height: 6),
TextField(
controller: nameController,
decoration: InputDecoration(
hintText: 'nama_file',
suffixText: '.xlsx',
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
BorderSide(color: Colors.blue.shade600, width: 1.5),
), ),
), ),
), const SizedBox(height: 6),
const SizedBox(height: 20), TextField(
controller: nameController,
// Range tanggal decoration: InputDecoration(
const Text('Filter rentang tanggal', hintText: 'nama_file',
style: TextStyle( suffixText: '.xlsx',
fontSize: 12, filled: true,
fontWeight: FontWeight.w600, fillColor: Colors.grey.shade100,
color: Colors.black54)), contentPadding: const EdgeInsets.symmetric(
const SizedBox(height: 6), horizontal: 14,
InkWell( vertical: 10,
onTap: pickRange, ),
borderRadius: BorderRadius.circular(10), border: OutlineInputBorder(
child: Container( borderRadius: BorderRadius.circular(10),
width: double.infinity, borderSide: BorderSide(color: Colors.grey.shade300),
padding: const EdgeInsets.symmetric( ),
horizontal: 14, vertical: 12), enabledBorder: OutlineInputBorder(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(10),
color: Colors.grey.shade100, borderSide: BorderSide(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(10), ),
border: Border.all(color: Colors.grey.shade300), focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: Colors.blue.shade600,
width: 1.5,
),
),
), ),
child: Row( ),
children: [ const SizedBox(height: 20),
Icon(Icons.date_range_rounded,
size: 18, color: Colors.blue.shade600), // Filter tanggal
const SizedBox(width: 10), const Text(
Expanded( 'Filter rentang tanggal',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black54,
),
),
const SizedBox(height: 6),
InkWell(
onTap: pickDateRange,
borderRadius: BorderRadius.circular(10),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
),
child: Row(
children: [
Icon(
Icons.date_range_rounded,
size: 18,
color: Colors.blue.shade600,
),
const SizedBox(width: 10),
Expanded(
child: Text(
dateFrom != null && dateTo != null
? '${dateFmt.format(dateFrom!)}${dateFmt.format(dateTo!)}'
: 'Semua tanggal (tanpa filter)',
style: TextStyle(
fontSize: 13,
color: dateFrom != null
? Colors.black87
: Colors.grey.shade500,
),
),
),
if (dateFrom != null)
GestureDetector(
onTap: () => setDialogState(() {
dateFrom = null;
dateTo = null;
}),
child: Icon(
Icons.close_rounded,
size: 16,
color: Colors.grey.shade500,
),
),
],
),
),
),
const SizedBox(height: 16),
// Filter jam
Row(
children: [
const Text(
'Filter rentang jam',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black54,
),
),
const Spacer(),
if (timeFrom != null || timeTo != null)
GestureDetector(
onTap: () => setDialogState(() {
timeFrom = null;
timeTo = null;
}),
child: Text( child: Text(
dateFrom != null && dateTo != null 'Reset',
? '${fmt.format(dateFrom!)}${fmt.format(dateTo!)}'
: 'Semua data (tanpa filter)',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 11,
color: dateFrom != null color: Colors.blue.shade600,
? Colors.black87 fontWeight: FontWeight.w600,
: Colors.grey.shade500,
), ),
), ),
), ),
if (dateFrom != null) ],
GestureDetector( ),
onTap: () => setDialogState(() { const SizedBox(height: 6),
dateFrom = null; Row(
dateTo = null; children: [
}), // Dari jam
child: Icon(Icons.close_rounded, Expanded(
size: 16, color: Colors.grey.shade500), child: InkWell(
onTap: () => pickTime(true),
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
decoration: BoxDecoration(
color: timeFrom != null
? Colors.blue.shade50
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: timeFrom != null
? Colors.blue.shade300
: Colors.grey.shade300,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dari jam',
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 3),
Row(
children: [
Icon(
Icons.schedule_rounded,
size: 14,
color: timeFrom != null
? Colors.blue.shade600
: Colors.grey.shade400,
),
const SizedBox(width: 4),
Text(
timeFrom != null
? fmtTime(timeFrom!)
: '00:00',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: timeFrom != null
? Colors.blue.shade700
: Colors.grey.shade400,
),
),
],
),
],
),
), ),
], ),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Icon(
Icons.arrow_forward_rounded,
size: 16,
color: Colors.grey.shade400,
),
),
// Sampai jam
Expanded(
child: InkWell(
onTap: () => pickTime(false),
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
decoration: BoxDecoration(
color: timeTo != null
? Colors.blue.shade50
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: timeTo != null
? Colors.blue.shade300
: Colors.grey.shade300,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sampai jam',
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade500,
),
),
const SizedBox(height: 3),
Row(
children: [
Icon(
Icons.schedule_rounded,
size: 14,
color: timeTo != null
? Colors.blue.shade600
: Colors.grey.shade400,
),
const SizedBox(width: 4),
Text(
timeTo != null
? fmtTime(timeTo!)
: '23:59',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: timeTo != null
? Colors.blue.shade700
: Colors.grey.shade400,
),
),
],
),
],
),
),
),
),
],
),
// Counter data hasil filter
const SizedBox(height: 10),
Text(
hasAnyFilter
? '$filtered data akan diekspor'
: '${state.history.length} data (semua, tanpa filter)',
style: TextStyle(
fontSize: 11,
color: (filtered == 0 && hasAnyFilter)
? Colors.red.shade400
: Colors.blue.shade600,
fontWeight: FontWeight.w500,
), ),
), ),
),
if (dateFrom != null) ...[
const SizedBox(height: 6),
Text(
'${state.history.where((e) => !e.timestamp.isBefore(dateFrom!) && !e.timestamp.isAfter(dateTo!.add(const Duration(days: 1)))).length} data dalam rentang ini',
style:
TextStyle(fontSize: 11, color: Colors.blue.shade600),
),
], ],
], ),
), ),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
child: Text('Batal', child: Text(
style: TextStyle(color: Colors.grey.shade600)), 'Batal',
style: TextStyle(color: Colors.grey.shade600),
),
), ),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () {
Navigator.pop(ctx); Navigator.pop(ctx);
_doExport(context, state, nameController.text.trim(), _doExport(
dateFrom, dateTo); context,
state,
nameController.text.trim(),
dateFrom,
dateTo,
timeFrom,
timeTo,
);
}, },
icon: const Icon(Icons.download_rounded, size: 16), icon: const Icon(Icons.download_rounded, size: 16),
label: const Text('Export'), label: const Text('Export'),
@ -244,7 +519,8 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
backgroundColor: Colors.blue.shade700, backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)), borderRadius: BorderRadius.circular(10),
),
), ),
), ),
], ],
@ -263,6 +539,8 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
String customName, String customName,
DateTime? dateFrom, DateTime? dateFrom,
DateTime? dateTo, DateTime? dateTo,
TimeOfDay? timeFrom, // baru
TimeOfDay? timeTo, // baru
) async { ) async {
final messenger = ScaffoldMessenger.of(context); final messenger = ScaffoldMessenger.of(context);
final fileName = customName.isEmpty final fileName = customName.isEmpty
@ -294,6 +572,10 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
fileName: fileName, fileName: fileName,
dateFrom: dateFrom, dateFrom: dateFrom,
dateTo: dateTo, dateTo: dateTo,
hourFrom: timeFrom?.hour,
minuteFrom: timeFrom?.minute,
hourTo: timeTo?.hour,
minuteTo: timeTo?.minute,
); );
messenger.hideCurrentSnackBar(); messenger.hideCurrentSnackBar();