oke
This commit is contained in:
parent
6cf9d0fb70
commit
aa530a5d09
|
|
@ -1,32 +1,44 @@
|
|||
version.json,1770352057364,f7f931727903f67c4b2ec2c38d4c5c4e2ec1be0c03071b8bff0c27f5be3b9e6e
|
||||
manifest.json,1768956954743,5edb32bebce763b2b7e55774ae4c5c4354a44df248387cc3c174e8710e85aaa1
|
||||
index.html,1770352031176,1fbdf325dceff31189cd7bcddde37e4eef1758ca2babb5cb593af432bd607799
|
||||
flutter_bootstrap.js,1770352031163,ed17fea025df6287ef713e3df7b02d14f66b937811e9ae377701382e28cfd589
|
||||
flutter.js,1767911992319,970356511bee637e57348b9e01b3218a3d1d6c4add478fdc306033b4b3e21ef4
|
||||
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
|
||||
canvaskit/skwasm_heavy.js,1767911992288,2e3aab5e61c0276a6704938362b4b3a4dc32bbb6d17d601e975e7ed03897b224
|
||||
canvaskit/skwasm.js,1767911992256,6f325d22924e678a9417a777104985babcee8f99080bf90855f26bfa9829a5ea
|
||||
canvaskit/canvaskit.js,1767911992193,777116800268972381a4832500bb0aafd96ab2546075bd4d96a9025feb5c7555
|
||||
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
|
||||
icons/Icon-maskable-192.png,1768956954742,f7ac56197638aec92f195c5344d15395c2cbf8a5ae74e846c5b4156b64f05e38
|
||||
icons/Icon-512.png,1768956954742,1de493563deb283e6ad70f0d5a89cfd61e37343df3b919c9862c5a41699ac5bc
|
||||
icons/Icon-192.png,1768956954741,5fd961c51d8eb53baacac01f8e53743a76f66a43390a11f5df042e8c618ad64a
|
||||
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.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
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import '../../shared/widgets/export_pdf_button.dart';
|
|||
import 'widgets/evaporasi_chart_widget.dart';
|
||||
import 'widgets/evaporasi_range_selector.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 {
|
||||
const EvaporasiScreen({super.key});
|
||||
|
|
@ -19,6 +21,280 @@ class EvaporasiScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -32,6 +308,17 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
|||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
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>(
|
||||
builder: (context, state) {
|
||||
|
|
@ -68,8 +355,7 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
|||
// ── Range Selector ──────────────────────────
|
||||
const Text(
|
||||
'Tren Evaporasi & Suhu',
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const EvaporasiRangeSelector(),
|
||||
|
|
@ -249,8 +535,7 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
|||
case 'Normal':
|
||||
statusColor = Colors.orange;
|
||||
statusIcon = Icons.warning_amber_rounded;
|
||||
warningText =
|
||||
'Sedang — evaporasi dalam batas normal, pantau kondisi.';
|
||||
warningText = 'Sedang — evaporasi dalam batas normal, pantau kondisi.';
|
||||
break;
|
||||
case 'Tinggi':
|
||||
statusColor = Colors.red;
|
||||
|
|
@ -282,8 +567,7 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Status Evaporasi',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
state.weatherStatus,
|
||||
|
|
@ -307,4 +591,4 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue