This commit is contained in:
kleponijo 2026-05-20 19:42:30 +07:00
parent 5602e488be
commit 21e471d0ea
2 changed files with 245 additions and 27 deletions

View File

@ -14,7 +14,6 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:intl/intl.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
// Import kondisional: web pakai dart:html, mobile pakai io + share_plus
import 'excel_saver_stub.dart'
if (dart.library.html) 'excel_saver_web.dart'
if (dart.library.io) 'excel_saver_mobile.dart';
@ -39,21 +38,23 @@ class WindSpeedExcelService {
required String alertLevel,
required String period,
required List<MyWindSpeed> history,
DateTime? filterDate,
required String fileName, // nama custom dari user
DateTime? dateFrom, // filter dari tanggal
DateTime? dateTo, // filter sampai tanggal
}) async {
final excel = Excel.createExcel();
excel.delete('Sheet1');
_buildSummarySheet(
excel, currentSpeed, alertLevel, period, history, filterDate);
_buildHistorySheet(excel, history, filterDate);
excel, currentSpeed, alertLevel, period, history, dateFrom, dateTo);
_buildHistorySheet(excel, history, dateFrom, dateTo);
// Simpan file
final bytes = excel.save();
if (bytes == null) throw Exception('Gagal membuat file Excel');
final name = 'wind_speed_${_fileFmt.format(DateTime.now())}.xlsx';
await saveAndShareExcel(Uint8List.fromList(bytes), name);
final safeName = fileName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
await saveAndShareExcel(Uint8List.fromList(bytes), '$safeName.xlsx');
}
//
@ -65,7 +66,8 @@ class WindSpeedExcelService {
String alertLevel,
String period,
List<MyWindSpeed> history,
DateTime? filterDate,
DateTime? dateFrom,
DateTime? dateTo,
) {
final sheet = excel['Ringkasan'];
@ -87,12 +89,16 @@ class WindSpeedExcelService {
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
_setCell(sheet, 2, 1, period, bgColor: _colorNormal);
if (filterDate != null) {
if (dateFrom != null && dateTo != null) {
_setCell(sheet, 3, 0, 'Filter tanggal',
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
_setCell(
sheet, 3, 1, DateFormat('dd MMMM yyyy', 'id_ID').format(filterDate),
bgColor: _colorNormal);
sheet,
3,
1,
'${DateFormat('dd MMMM yyyy', 'id_ID').format(dateFrom)}${DateFormat('dd MMMM yyyy', 'id_ID').format(dateTo)}',
bgColor: _colorNormal,
);
}
// -- Kecepatan saat ini --
@ -146,7 +152,8 @@ class WindSpeedExcelService {
static void _buildHistorySheet(
Excel excel,
List<MyWindSpeed> history,
DateTime? filterDate,
DateTime? dateFrom,
DateTime? dateTo,
) {
final sheet = excel['Data History'];
@ -167,14 +174,15 @@ class WindSpeedExcelService {
centered: true);
}
// -- Filter jika ada tanggal dipilih --
final data = filterDate != null
? history
.where((e) =>
e.timestamp.year == filterDate.year &&
e.timestamp.month == filterDate.month &&
e.timestamp.day == filterDate.day)
.toList()
// Filter berdasarkan rentang tanggal (inklusif kedua ujung)
final data = (dateFrom != null && dateTo != null)
? history.where((e) {
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);
return !d.isBefore(from) && !d.isAfter(to);
}).toList()
: history;
// -- Isi baris data --

View File

@ -58,9 +58,217 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
}
}
// Export Excel
Future<void> _exportExcel(BuildContext context, WindSpeedState state) async {
// Dialog export: nama file + date range
Future<void> _showExportDialog(
BuildContext context, WindSpeedState state) async {
// Tentukan batas tanggal dari history
DateTime firstDate = DateTime.now().subtract(const Duration(days: 365));
DateTime lastDate = DateTime.now();
if (state.history.isNotEmpty) {
final sorted = [...state.history]
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
firstDate = DateTime(sorted.first.timestamp.year,
sorted.first.timestamp.month, sorted.first.timestamp.day);
lastDate = DateTime(sorted.last.timestamp.year,
sorted.last.timestamp.month, sorted.last.timestamp.day);
}
// State dialog pakai ValueNotifier supaya tidak perlu StatefulWidget terpisah
final nameController = TextEditingController(
text: 'kecepatan_angin_${DateFormat('ddMMyyyy').format(DateTime.now())}',
);
DateTime? dateFrom;
DateTime? dateTo;
await showDialog(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) {
final fmt = DateFormat('dd MMM yyyy', 'id_ID');
Future<void> pickRange() async {
final range = await showDateRangePicker(
context: ctx,
firstDate: firstDate,
lastDate: lastDate,
initialDateRange: dateFrom != null && dateTo != null
? DateTimeRange(start: dateFrom!, end: dateTo!)
: null,
locale: const Locale('id', 'ID'),
builder: (ctx, child) => Theme(
data: Theme.of(ctx).copyWith(
colorScheme: ColorScheme.light(
primary: Colors.blue.shade700,
onPrimary: Colors.white,
surface: Colors.white,
),
),
child: child!,
),
);
if (range != null) {
setDialogState(() {
dateFrom = range.start;
dateTo = range.end;
});
}
}
return AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row(
children: [
Icon(Icons.file_download_outlined, color: Colors.blue.shade700),
const SizedBox(width: 10),
const Text('Export Excel',
style:
TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
],
),
content: SizedBox(
width: 340,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Nama file
const Text('Nama file',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
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: 20),
// Range tanggal
const Text('Filter rentang tanggal',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black54)),
const SizedBox(height: 6),
InkWell(
onTap: pickRange,
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
? '${fmt.format(dateFrom!)}${fmt.format(dateTo!)}'
: 'Semua data (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),
),
],
),
),
),
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: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('Batal',
style: TextStyle(color: Colors.grey.shade600)),
),
ElevatedButton.icon(
onPressed: () {
Navigator.pop(ctx);
_doExport(context, state, nameController.text.trim(),
dateFrom, dateTo);
},
icon: const Icon(Icons.download_rounded, size: 16),
label: const Text('Export'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
],
);
},
),
);
nameController.dispose();
}
// Proses export setelah dialog ditutup
Future<void> _doExport(
BuildContext context,
WindSpeedState state,
String customName,
DateTime? dateFrom,
DateTime? dateTo,
) async {
final messenger = ScaffoldMessenger.of(context);
final fileName = customName.isEmpty
? 'kecepatan_angin_${DateFormat('ddMMyyyy').format(DateTime.now())}'
: customName;
try {
messenger.showSnackBar(
const SnackBar(
@ -83,15 +291,17 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
alertLevel: state.alertLevel,
period: state.selectedPeriod,
history: state.history,
filterDate: state.selectedDate,
fileName: fileName,
dateFrom: dateFrom,
dateTo: dateTo,
);
messenger.hideCurrentSnackBar();
messenger.showSnackBar(SnackBar(
content: const Row(children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 8),
Text('File Excel berhasil dibuat!'),
content: Row(children: [
const Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 8),
Expanded(child: Text('$fileName.xlsx berhasil dibuat!')),
]),
backgroundColor: Colors.green.shade600,
behavior: SnackBarBehavior.floating,
@ -129,7 +339,7 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
icon: const Icon(Icons.file_download_outlined),
onPressed: state.history.isEmpty
? null
: () => _exportExcel(context, state),
: () => _showExportDialog(context, state),
),
),
IconButton(