This commit is contained in:
kleponijo 2026-05-23 06:55:35 +07:00
parent 6cf9d0fb70
commit aa530a5d09
3 changed files with 748 additions and 33 deletions

View File

@ -1,32 +1,44 @@
version.json,1770352057364,f7f931727903f67c4b2ec2c38d4c5c4e2ec1be0c03071b8bff0c27f5be3b9e6e
manifest.json,1768956954743,5edb32bebce763b2b7e55774ae4c5c4354a44df248387cc3c174e8710e85aaa1 manifest.json,1768956954743,5edb32bebce763b2b7e55774ae4c5c4354a44df248387cc3c174e8710e85aaa1
index.html,1770352031176,1fbdf325dceff31189cd7bcddde37e4eef1758ca2babb5cb593af432bd607799
flutter_bootstrap.js,1770352031163,ed17fea025df6287ef713e3df7b02d14f66b937811e9ae377701382e28cfd589
flutter.js,1767911992319,970356511bee637e57348b9e01b3218a3d1d6c4add478fdc306033b4b3e21ef4 flutter.js,1767911992319,970356511bee637e57348b9e01b3218a3d1d6c4add478fdc306033b4b3e21ef4
favicon.png,1768956954741,19880e004e8357a51f6a64f5d1fdc8828a6432fe9c781f8acb6b730eced2cd13 favicon.png,1768956954741,19880e004e8357a51f6a64f5d1fdc8828a6432fe9c781f8acb6b730eced2cd13
flutter_service_worker.js,1770352060714,32aece2e5d9468eccf8a5a82c52aeabcea5af4ddffb3a2401fb1bf1f302ce9e6
icons/Icon-512.png,1768956954742,1de493563deb283e6ad70f0d5a89cfd61e37343df3b919c9862c5a41699ac5bc
icons/Icon-maskable-192.png,1768956954742,f7ac56197638aec92f195c5344d15395c2cbf8a5ae74e846c5b4156b64f05e38
icons/Icon-192.png,1768956954741,5fd961c51d8eb53baacac01f8e53743a76f66a43390a11f5df042e8c618ad64a
assets/FontManifest.json,1770352057804,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
assets/shaders/stretch_effect.frag,1770352057964,586ff23669838dd33070077916f0b971a8800f67093ae8d6eed47e6cea50926f
assets/AssetManifest.bin.json,1770352057804,6763aaa55146ec9534c671aaf5a6db93265eaaa8082767ea02eb2ac9b09cbdaa
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1770352059003,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
assets/AssetManifest.bin,1770352057804,93e425eb6f3c87a588cd708f748fc5bb80fa28bf34c6d814b2bd76daa6a6ed27
assets/fonts/MaterialIcons-Regular.otf,1770352059003,400581a54bcbf9b63f6d7afd915d4713ce89ccf00ed00ac0edf5268f0de803a1
assets/shaders/ink_sparkle.frag,1770352057964,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
icons/Icon-maskable-512.png,1768956954742,ea58e5c49bc4dcdc5db0a9fe2f25b2a3152f85ef9094f0d020de0aa318451a1d icons/Icon-maskable-512.png,1768956954742,ea58e5c49bc4dcdc5db0a9fe2f25b2a3152f85ef9094f0d020de0aa318451a1d
canvaskit/skwasm_heavy.js,1767911992288,2e3aab5e61c0276a6704938362b4b3a4dc32bbb6d17d601e975e7ed03897b224 icons/Icon-maskable-192.png,1768956954742,f7ac56197638aec92f195c5344d15395c2cbf8a5ae74e846c5b4156b64f05e38
canvaskit/skwasm.js,1767911992256,6f325d22924e678a9417a777104985babcee8f99080bf90855f26bfa9829a5ea icons/Icon-512.png,1768956954742,1de493563deb283e6ad70f0d5a89cfd61e37343df3b919c9862c5a41699ac5bc
canvaskit/canvaskit.js,1767911992193,777116800268972381a4832500bb0aafd96ab2546075bd4d96a9025feb5c7555 icons/Icon-192.png,1768956954741,5fd961c51d8eb53baacac01f8e53743a76f66a43390a11f5df042e8c618ad64a
canvaskit/chromium/canvaskit.js,1767911992241,e0f423bfd63202fb53a4dd5cffe330143bfb9eabf5e4e8198bb605442a49d3e7
assets/NOTICES,1770352057804,b877416ca37cc2a29c1969535c12cb9f74e9d950b21c916d75918e99ec0da3ed
canvaskit/chromium/canvaskit.js.symbols,1767911992241,4a8d181b0622d7a4b4254deb591c43f48dd89623c621dec6f84d0a0b2b8f77a7
canvaskit/canvaskit.js.symbols,1767911992193,b1e22d35cbc9e3b4b89516a8462c9f1f73d86e6debc790833396dff78ad698a7
canvaskit/skwasm.js.symbols,1767911992272,5d3dd3374664b262880564a3e59d64b150f4c632248fb93f913b4eebdeffca83
canvaskit/skwasm_heavy.js.symbols,1767911992288,1259742821cf62bece358301f311840038fc2a0352b0c5827f8d2137d6197d08
main.dart.js,1770352050513,8c2d615d9f867aa7edc73fc0f4d4b63eaeef9f3f3cd26b583e7938bbcc1b2e72
canvaskit/skwasm.wasm,1767911992288,4776f3bfb61a7534e2260954b88b177c11a7c73644cd536c47fcaabe5d7dc79b
canvaskit/chromium/canvaskit.wasm,1767911992256,06f70f0703803c7329be30338b79530564ddc1ab2c3789d608431697a9a30ba2
canvaskit/skwasm_heavy.wasm,1767911992319,aa021afcfd815b925d5bad0debf468bef50192c458573e4570d6da4e12c2e186 canvaskit/skwasm_heavy.wasm,1767911992319,aa021afcfd815b925d5bad0debf468bef50192c458573e4570d6da4e12c2e186
canvaskit/skwasm_heavy.js.symbols,1767911992288,1259742821cf62bece358301f311840038fc2a0352b0c5827f8d2137d6197d08
canvaskit/skwasm_heavy.js,1767911992288,2e3aab5e61c0276a6704938362b4b3a4dc32bbb6d17d601e975e7ed03897b224
canvaskit/skwasm.wasm,1767911992288,4776f3bfb61a7534e2260954b88b177c11a7c73644cd536c47fcaabe5d7dc79b
canvaskit/skwasm.js.symbols,1767911992272,5d3dd3374664b262880564a3e59d64b150f4c632248fb93f913b4eebdeffca83
canvaskit/skwasm.js,1767911992256,6f325d22924e678a9417a777104985babcee8f99080bf90855f26bfa9829a5ea
canvaskit/canvaskit.wasm,1767911992241,ac942f1920618b74351bc74bf1cf4ca078bde56e11692c0bd611e906bfc2d279 canvaskit/canvaskit.wasm,1767911992241,ac942f1920618b74351bc74bf1cf4ca078bde56e11692c0bd611e906bfc2d279
canvaskit/canvaskit.js.symbols,1767911992193,b1e22d35cbc9e3b4b89516a8462c9f1f73d86e6debc790833396dff78ad698a7
canvaskit/canvaskit.js,1767911992193,777116800268972381a4832500bb0aafd96ab2546075bd4d96a9025feb5c7555
canvaskit/chromium/canvaskit.wasm,1767911992256,06f70f0703803c7329be30338b79530564ddc1ab2c3789d608431697a9a30ba2
canvaskit/chromium/canvaskit.js.symbols,1767911992241,4a8d181b0622d7a4b4254deb591c43f48dd89623c621dec6f84d0a0b2b8f77a7
canvaskit/chromium/canvaskit.js,1767911992241,e0f423bfd63202fb53a4dd5cffe330143bfb9eabf5e4e8198bb605442a49d3e7
version.json,1779493407408,f7f931727903f67c4b2ec2c38d4c5c4e2ec1be0c03071b8bff0c27f5be3b9e6e
index.html,1779493279226,3ffbcfc0c9d82b851ce84d2b1bfc85cea0a34b077bdad955e7cf52643a3cf434
flutter_bootstrap.js,1779493279204,ac46b098b9b222a709fa5ec23d1808db44a01d9b73d454c9a583d07a29cea348
flutter_service_worker.js,1779493415284,df198e5b7cc03c86b760a3ec2fe6f78f064d02cf545c4fb4b4a6a4ae142c3020
splash/img/light-1x.png,1779442242116,c4d52b9b4b1cfdd74194c0192c462e0f14934e1428ab7ff99165f08dc17f9010
splash/img/dark-1x.png,1779442242114,c4d52b9b4b1cfdd74194c0192c462e0f14934e1428ab7ff99165f08dc17f9010
assets/AssetManifest.bin.json,1779493407814,e23e10111498cc3b8b40e6549dcde13cffa6f45dc78e88194f4a7e284bb68294
assets/FontManifest.json,1779493407814,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
assets/AssetManifest.bin,1779493407814,fe7319a65158e186661b7edfcc1c2b6c0a6bc6823b923a13e1e932cd66407fa2
assets/shaders/stretch_effect.frag,1779493407940,586ff23669838dd33070077916f0b971a8800f67093ae8d6eed47e6cea50926f
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779493409456,85140ea453cd3e02bd93650fe8add6a724ae8c0ac73d0c491085b301e7bd21d2
assets/shaders/ink_sparkle.frag,1779493407944,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
assets/images/windy.png,1776157890991,b9d34ed004fff60a4fea365c23508823943f3f0793375f34ca9202069d9c9955
assets/images/atmosfer.png,1776157890990,c3a9ddfa5676b5f0e1cf833f73c0f09c1dfb9d2e202906c1afcdd7a50dcf2506
assets/fonts/MaterialIcons-Regular.otf,1779493409477,1c7547588fe9c202b730590aab269e1bae9988240caeba41da28091251a87dbe
assets/images/google-icon-logo.png,1776157890990,333cfafcd5c87738a4098359e18e02ff5848b2b1b819ec8aff29424c3f82d32b
splash/img/light-2x.png,1779442242116,e8cc8f611a58a4926632edfcd3cb7d95925b6895791632ea1c1739c4f454c8fa
splash/img/dark-2x.png,1779442242115,e8cc8f611a58a4926632edfcd3cb7d95925b6895791632ea1c1739c4f454c8fa
splash/img/light-3x.png,1779442242118,a1a4d566718037d913ad3287e9b7d1e6d0df8569c285ce3d3680db82b6a96812
splash/img/dark-3x.png,1779442242115,a1a4d566718037d913ad3287e9b7d1e6d0df8569c285ce3d3680db82b6a96812
splash/img/light-4x.png,1779442242118,09ff8858378bb70d5f4b0381eb98dc9a889e541aeb2dd7576da7d62f205de578
assets/images/logo_klimatologi.png,1777103040742,e042ff556e356926fcd5ff92555519a569a71316ab2f7074f86f4ea4a215f325
splash/img/dark-4x.png,1779442242116,09ff8858378bb70d5f4b0381eb98dc9a889e541aeb2dd7576da7d62f205de578
assets/NOTICES,1779493407816,62963000bb4f8b466d5b97db77a0f9563f047723e134974be6bf170da0f821b2
main.dart.js,1779493365556,91f95baa75b37e85885a3fc9b0fc953ed2245277abde8ca4682b0707131a2056

View File

@ -10,6 +10,8 @@ import '../../shared/widgets/export_pdf_button.dart';
import 'widgets/evaporasi_chart_widget.dart'; import 'widgets/evaporasi_chart_widget.dart';
import 'widgets/evaporasi_range_selector.dart'; import 'widgets/evaporasi_range_selector.dart';
import 'widgets/evaporasi_history_list.dart'; import 'widgets/evaporasi_history_list.dart';
// TAMBAH baris ini setelah import yang sudah ada:
import '../../shared/utils/excel/evaporasi_excel_service.dart';
class EvaporasiScreen extends StatefulWidget { class EvaporasiScreen extends StatefulWidget {
const EvaporasiScreen({super.key}); const EvaporasiScreen({super.key});
@ -19,6 +21,280 @@ class EvaporasiScreen extends StatefulWidget {
} }
class _EvaporasiScreenState extends State<EvaporasiScreen> { class _EvaporasiScreenState extends State<EvaporasiScreen> {
// Dialog export: nama file + date range
Future<void> _showExportDialog(
BuildContext context, EvaporasiState state) async {
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);
}
final nameController = TextEditingController(
text: 'evaporasi_${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) {
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);
}).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,
EvaporasiState state,
String customName,
DateTime? dateFrom,
DateTime? dateTo,
) async {
final messenger = ScaffoldMessenger.of(context);
final fileName = customName.isEmpty
? 'evaporasi_${DateFormat('ddMMyyyy').format(DateTime.now())}'
: customName;
try {
messenger.showSnackBar(
const SnackBar(
content: Row(children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white)),
SizedBox(width: 12),
Text('Membuat file Excel...'),
]),
duration: Duration(seconds: 30),
behavior: SnackBarBehavior.floating,
),
);
await EvaporasiExcelService.export(
currentValue: state.currentValue,
temperature: state.temperature,
waterLevel: state.waterLevel,
acuanPagi: state.currentData?.acuanPagi ?? 0.0,
weatherStatus: state.weatherStatus,
history: state.history,
fileName: fileName,
dateFrom: dateFrom,
dateTo: dateTo,
);
messenger.hideCurrentSnackBar();
messenger.showSnackBar(SnackBar(
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,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
duration: const Duration(seconds: 3),
));
} catch (e) {
messenger.hideCurrentSnackBar();
messenger.showSnackBar(SnackBar(
content: Text('Gagal: $e'),
backgroundColor: Colors.red.shade600,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
));
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -32,6 +308,17 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
foregroundColor: Colors.black, foregroundColor: Colors.black,
actions: [
BlocBuilder<EvaporasiBloc, EvaporasiState>(
builder: (context, state) => IconButton(
tooltip: 'Export Excel',
icon: const Icon(Icons.file_download_outlined),
onPressed: state.history.isEmpty
? null
: () => _showExportDialog(context, state),
),
),
],
), ),
body: BlocBuilder<EvaporasiBloc, EvaporasiState>( body: BlocBuilder<EvaporasiBloc, EvaporasiState>(
builder: (context, state) { builder: (context, state) {
@ -68,8 +355,7 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
// Range Selector // Range Selector
const Text( const Text(
'Tren Evaporasi & Suhu', 'Tren Evaporasi & Suhu',
style: TextStyle( style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
fontSize: 18, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
const EvaporasiRangeSelector(), const EvaporasiRangeSelector(),
@ -249,8 +535,7 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
case 'Normal': case 'Normal':
statusColor = Colors.orange; statusColor = Colors.orange;
statusIcon = Icons.warning_amber_rounded; statusIcon = Icons.warning_amber_rounded;
warningText = warningText = 'Sedang — evaporasi dalam batas normal, pantau kondisi.';
'Sedang — evaporasi dalam batas normal, pantau kondisi.';
break; break;
case 'Tinggi': case 'Tinggi':
statusColor = Colors.red; statusColor = Colors.red;
@ -282,8 +567,7 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text('Status Evaporasi', const Text('Status Evaporasi',
style: style: TextStyle(fontSize: 12, color: Colors.grey)),
TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
state.weatherStatus, state.weatherStatus,

View File

@ -0,0 +1,419 @@
// ===========================================================
// evaporasi_excel_service.dart
// Lokasi: lib/screens/monitoring/shared/utils/excel/
// ===========================================================
import 'dart:typed_data';
import 'package:excel/excel.dart';
import 'package:intl/intl.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import 'excel_saver_stub.dart'
if (dart.library.html) 'excel_saver_web.dart'
if (dart.library.io) 'excel_saver_mobile.dart';
class EvaporasiExcelService {
// Format helper
static final _dateFmt = DateFormat('dd/MM/yyyy HH:mm:ss', 'id_ID');
static final _headerFmt = DateFormat('dd MMMM yyyy, HH:mm', 'id_ID');
// Warna tema
static const _colorHeader = '1A4A8C'; // biru tua
static const _colorSubHead = '2E75B6'; // biru medium
static const _colorNormal = 'E8F4FD'; // biru sangat muda
static const _colorTinggi = 'F8D7DA'; // merah muda
static const _colorRendah = 'D1ECF1'; // biru muda
static const _colorWhite = 'FFFFFF';
static const _colorWaspada = 'FFF3CD'; // kuning muda
/// Export data evaporasi ke file Excel dan buka share sheet
static Future<void> export({
required double currentValue,
required double temperature,
required double waterLevel,
required double acuanPagi,
required String weatherStatus,
required List<Evaporasi> history,
required String fileName,
DateTime? dateFrom,
DateTime? dateTo,
}) async {
final excel = Excel.createExcel();
excel.delete('Sheet1');
_buildSummarySheet(
excel,
currentValue,
temperature,
waterLevel,
acuanPagi,
weatherStatus,
history,
dateFrom,
dateTo,
);
_buildHistorySheet(excel, history, dateFrom, dateTo);
// Simpan file
final bytes = excel.save();
if (bytes == null) throw Exception('Gagal membuat file Excel');
final safeName = fileName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
await saveAndShareExcel(Uint8List.fromList(bytes), '$safeName.xlsx');
}
//
// SHEET 1: Ringkasan
//
static void _buildSummarySheet(
Excel excel,
double currentValue,
double temperature,
double waterLevel,
double acuanPagi,
String weatherStatus,
List<Evaporasi> history,
DateTime? dateFrom,
DateTime? dateTo,
) {
final sheet = excel['Ringkasan'];
// -- Judul --
_setCell(sheet, 0, 0, 'DATA EVAPORASI WEATHER STATION',
bold: true,
fontSize: 14,
bgColor: _colorHeader,
fontColor: _colorWhite);
sheet.merge(
CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 0),
CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: 0),
);
// -- Metadata --
_setCell(sheet, 1, 0, 'Diekspor pada',
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
_setCell(sheet, 1, 1, _headerFmt.format(DateTime.now()),
bgColor: _colorNormal);
if (dateFrom != null && dateTo != null) {
_setCell(sheet, 2, 0, 'Filter tanggal',
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
_setCell(
sheet,
2,
1,
'${DateFormat('dd MMMM yyyy', 'id_ID').format(dateFrom)}${DateFormat('dd MMMM yyyy', 'id_ID').format(dateTo)}',
bgColor: _colorNormal,
);
}
// -- Nilai saat ini --
_setCell(sheet, 4, 0, 'DATA SAAT INI',
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
sheet.merge(
CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 4),
CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: 4),
);
_setCell(sheet, 5, 0, 'Evaporasi (mm)', bold: true);
_setCellDouble(sheet, 5, 1, currentValue);
_setCell(sheet, 6, 0, 'Suhu Air (°C)', bold: true);
if (temperature < 0) {
_setCell(sheet, 6, 1, '-');
} else {
_setCellDouble(sheet, 6, 1, temperature);
}
_setCell(sheet, 7, 0, 'Tinggi Air (cm)', bold: true);
_setCellDouble(sheet, 7, 1, waterLevel);
_setCell(sheet, 8, 0, 'Acuan Air Pagi (cm)', bold: true);
_setCellDouble(sheet, 8, 1, acuanPagi);
_setCell(sheet, 9, 0, 'Status', bold: true);
_setCell(sheet, 9, 1, weatherStatus,
bgColor: _statusBgColor(weatherStatus));
// -- Statistik ringkasan --
if (history.isNotEmpty) {
final evapValues = history.map((e) => e.evaporasi).toList();
final tempValues = history
.map((e) => e.suhu)
.where((s) => s >= 0)
.toList();
final evapAvg = evapValues.reduce((a, b) => a + b) / evapValues.length;
final evapMax = evapValues.reduce((a, b) => a > b ? a : b);
final evapMin = evapValues.reduce((a, b) => a < b ? a : b);
_setCell(sheet, 11, 0, 'STATISTIK HISTORY',
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
sheet.merge(
CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 11),
CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: 11),
);
_setCell(sheet, 12, 0, 'Jumlah data', bold: true);
_setCellInt(sheet, 12, 1, history.length);
_setCell(sheet, 13, 0, 'Rata-rata evaporasi (mm)', bold: true);
_setCellDouble(sheet, 13, 1, evapAvg);
_setCell(sheet, 14, 0, 'Evaporasi maksimum (mm)', bold: true);
_setCellDouble(sheet, 14, 1, evapMax);
_setCell(sheet, 15, 0, 'Evaporasi minimum (mm)', bold: true);
_setCellDouble(sheet, 15, 1, evapMin);
if (tempValues.isNotEmpty) {
final tempAvg =
tempValues.reduce((a, b) => a + b) / tempValues.length;
final tempMax = tempValues.reduce((a, b) => a > b ? a : b);
final tempMin = tempValues.reduce((a, b) => a < b ? a : b);
_setCell(sheet, 16, 0, 'Rata-rata suhu (°C)', bold: true);
_setCellDouble(sheet, 16, 1, tempAvg);
_setCell(sheet, 17, 0, 'Suhu maksimum (°C)', bold: true);
_setCellDouble(sheet, 17, 1, tempMax);
_setCell(sheet, 18, 0, 'Suhu minimum (°C)', bold: true);
_setCellDouble(sheet, 18, 1, tempMin);
}
}
// Set lebar kolom
sheet.setColumnWidth(0, 26);
sheet.setColumnWidth(1, 22);
sheet.setColumnWidth(2, 18);
sheet.setColumnWidth(3, 18);
}
//
// SHEET 2: Data History
//
static void _buildHistorySheet(
Excel excel,
List<Evaporasi> history,
DateTime? dateFrom,
DateTime? dateTo,
) {
final sheet = excel['Data History'];
// -- Header kolom --
final headers = [
'No',
'Tanggal',
'Waktu',
'Evaporasi (mm)',
'Tinggi Air (cm)',
'Suhu (°C)',
'Acuan Pagi (cm)',
'Status',
];
for (var i = 0; i < headers.length; i++) {
_setCell(sheet, 0, i, headers[i],
bold: true,
bgColor: _colorHeader,
fontColor: _colorWhite,
centered: true);
}
// 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;
// Urutkan terbaru di atas
final sorted = [...data]
..sort((a, b) => b.timestamp.compareTo(a.timestamp));
// -- Isi baris data --
for (var i = 0; i < sorted.length; i++) {
final item = sorted[i];
final rowIdx = i + 1;
final status = _getStatus(item.evaporasi);
final bgColor = i.isEven ? _colorNormal : _colorWhite;
final statusBg = _statusBgColor(status);
_setCellInt(sheet, rowIdx, 0, i + 1,
bgColor: bgColor, centered: true);
_setCell(
sheet,
rowIdx,
1,
DateFormat('dd/MM/yyyy', 'id_ID').format(item.timestamp),
bgColor: bgColor);
_setCell(
sheet,
rowIdx,
2,
DateFormat('HH:mm:ss').format(item.timestamp),
bgColor: bgColor,
centered: true);
_setCellDouble(sheet, rowIdx, 3, item.evaporasi,
bgColor: bgColor, centered: true);
_setCellDouble(sheet, rowIdx, 4, item.tinggiAir,
bgColor: bgColor, centered: true);
// Suhu: tampilkan '-' jika sensor error (< 0)
if (item.suhu < 0) {
_setCell(sheet, rowIdx, 5, '-', bgColor: bgColor, centered: true);
} else {
_setCellDouble(sheet, rowIdx, 5, item.suhu,
bgColor: bgColor, centered: true);
}
_setCellDouble(sheet, rowIdx, 6, item.acuanPagi,
bgColor: bgColor, centered: true);
_setCell(sheet, rowIdx, 7, status,
bgColor: statusBg, centered: true, bold: true);
}
// -- Footer jika kosong --
if (sorted.isEmpty) {
_setCell(sheet, 1, 0, 'Tidak ada data untuk tanggal yang dipilih',
bgColor: _colorWaspada, centered: true);
sheet.merge(
CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 1),
CellIndex.indexByColumnRow(columnIndex: 7, rowIndex: 1),
);
}
// Set lebar kolom
sheet.setColumnWidth(0, 6);
sheet.setColumnWidth(1, 14);
sheet.setColumnWidth(2, 12);
sheet.setColumnWidth(3, 18);
sheet.setColumnWidth(4, 18);
sheet.setColumnWidth(5, 14);
sheet.setColumnWidth(6, 18);
sheet.setColumnWidth(7, 12);
}
//
// HELPER CELLS
//
static void _setCell(
Sheet sheet,
int row,
int col,
String value, {
bool bold = false,
double fontSize = 11,
String bgColor = _colorWhite,
String fontColor = '000000',
bool centered = false,
}) {
final cell = sheet
.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row));
cell.value = TextCellValue(value);
cell.cellStyle = CellStyle(
bold: bold,
fontSize: fontSize.toInt(),
backgroundColorHex: ExcelColor.fromHexString('#$bgColor'),
fontColorHex: ExcelColor.fromHexString('#$fontColor'),
horizontalAlign:
centered ? HorizontalAlign.Center : HorizontalAlign.Left,
verticalAlign: VerticalAlign.Center,
textWrapping: TextWrapping.WrapText,
leftBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
rightBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
topBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
bottomBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
);
}
static void _setCellDouble(
Sheet sheet,
int row,
int col,
double value, {
String bgColor = _colorWhite,
bool centered = false,
}) {
final cell = sheet
.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row));
cell.value = DoubleCellValue(double.parse(value.toStringAsFixed(4)));
cell.cellStyle = CellStyle(
backgroundColorHex: ExcelColor.fromHexString('#$bgColor'),
horizontalAlign:
centered ? HorizontalAlign.Center : HorizontalAlign.Right,
verticalAlign: VerticalAlign.Center,
leftBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
rightBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
topBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
bottomBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
);
}
static void _setCellInt(
Sheet sheet,
int row,
int col,
int value, {
String bgColor = _colorWhite,
bool centered = false,
}) {
final cell = sheet
.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row));
cell.value = IntCellValue(value);
cell.cellStyle = CellStyle(
backgroundColorHex: ExcelColor.fromHexString('#$bgColor'),
horizontalAlign:
centered ? HorizontalAlign.Center : HorizontalAlign.Right,
verticalAlign: VerticalAlign.Center,
leftBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
rightBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
topBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
bottomBorder: Border(
borderStyle: BorderStyle.Thin,
borderColorHex: ExcelColor.fromHexString('#CCCCCC')),
);
}
static String _statusBgColor(String status) {
switch (status) {
case 'Tinggi':
return _colorTinggi;
case 'Normal':
return _colorWaspada;
default: // Rendah
return _colorRendah;
}
}
static String _getStatus(double evaporasi) {
if (evaporasi > 10.0) return 'Tinggi';
if (evaporasi >= 2.0) return 'Normal';
return 'Rendah';
}
}