From 21e471d0ea77bcde17fa96a025c3d4ab0b7a9c3f Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Wed, 20 May 2026 19:42:30 +0700 Subject: [PATCH] oke --- .../utils/excel/wind_speed_excel_service.dart | 46 ++-- .../wind_speed/views/wind_speed_screen.dart | 226 +++++++++++++++++- 2 files changed, 245 insertions(+), 27 deletions(-) diff --git a/lib/screens/monitoring/shared/utils/excel/wind_speed_excel_service.dart b/lib/screens/monitoring/shared/utils/excel/wind_speed_excel_service.dart index 9d65060..45c4c76 100644 --- a/lib/screens/monitoring/shared/utils/excel/wind_speed_excel_service.dart +++ b/lib/screens/monitoring/shared/utils/excel/wind_speed_excel_service.dart @@ -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 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 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 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 -- diff --git a/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart b/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart index bdd1518..52abbe9 100644 --- a/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart +++ b/lib/screens/monitoring/wind_speed/views/wind_speed_screen.dart @@ -58,9 +58,217 @@ class _WindSpeedScreenState extends State { } } - // ── Export Excel ───────────────────────────────────────────── - Future _exportExcel(BuildContext context, WindSpeedState state) async { + // ── Dialog export: nama file + date range ─────────────────── + Future _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 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 _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 { 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 { icon: const Icon(Icons.file_download_outlined), onPressed: state.history.isEmpty ? null - : () => _exportExcel(context, state), + : () => _showExportDialog(context, state), ), ), IconButton(