From 9659dc5edfdb760982b3dc5b2fb585aca14e6263 Mon Sep 17 00:00:00 2001 From: kleponijo <158453666+kleponijo@users.noreply.github.com> Date: Wed, 27 May 2026 16:36:43 +0700 Subject: [PATCH] oke --- .firebase/hosting.YnVpbGRcd2Vi.cache | 26 +- .../auth/blocs/sign_in_bloc/sign_in_bloc.dart | 8 +- .../utils/excel/evaporasi_excel_service.dart | 68 ++-- .../utils/excel/excel_saver_mobile.dart | 18 +- .../shared/utils/excel/excel_saver_web.dart | 22 +- .../utils/excel/wind_speed_excel_service.dart | 295 ++++++++++++------ .../lib/src/firebase_user_repo.dart | 13 +- .../user_repository/lib/src/user_repo.dart | 2 +- 8 files changed, 293 insertions(+), 159 deletions(-) diff --git a/.firebase/hosting.YnVpbGRcd2Vi.cache b/.firebase/hosting.YnVpbGRcd2Vi.cache index 0d76ff6..703bf17 100644 --- a/.firebase/hosting.YnVpbGRcd2Vi.cache +++ b/.firebase/hosting.YnVpbGRcd2Vi.cache @@ -29,16 +29,16 @@ assets/images/windy.png,1776157890991,b9d34ed004fff60a4fea365c23508823943f3f0793 assets/images/logo_klimatologi.png,1777103040742,e042ff556e356926fcd5ff92555519a569a71316ab2f7074f86f4ea4a215f325 assets/images/google-icon-logo.png,1776157890990,333cfafcd5c87738a4098359e18e02ff5848b2b1b819ec8aff29424c3f82d32b assets/images/atmosfer.png,1776157890990,c3a9ddfa5676b5f0e1cf833f73c0f09c1dfb9d2e202906c1afcdd7a50dcf2506 -version.json,1779597551680,f7f931727903f67c4b2ec2c38d4c5c4e2ec1be0c03071b8bff0c27f5be3b9e6e -index.html,1779597409382,3ffbcfc0c9d82b851ce84d2b1bfc85cea0a34b077bdad955e7cf52643a3cf434 -flutter_service_worker.js,1779597560727,6a808567675f0224b1af9ad8047f11ef5a7d664aa42a4805536cda3a2f3ef4d4 -assets/FontManifest.json,1779597552223,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 -assets/AssetManifest.bin.json,1779597552223,e23e10111498cc3b8b40e6549dcde13cffa6f45dc78e88194f4a7e284bb68294 -flutter_bootstrap.js,1779597409344,5477d2c4d175fd91e3398d585c6f47786ab282fa386db0d7a1962e6111c4db7f -assets/AssetManifest.bin,1779597552222,fe7319a65158e186661b7edfcc1c2b6c0a6bc6823b923a13e1e932cd66407fa2 -assets/shaders/stretch_effect.frag,1779597552364,586ff23669838dd33070077916f0b971a8800f67093ae8d6eed47e6cea50926f -assets/shaders/ink_sparkle.frag,1779597552364,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 -assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779597553861,85140ea453cd3e02bd93650fe8add6a724ae8c0ac73d0c491085b301e7bd21d2 -assets/fonts/MaterialIcons-Regular.otf,1779597553863,1c7547588fe9c202b730590aab269e1bae9988240caeba41da28091251a87dbe -assets/NOTICES,1779597552226,62963000bb4f8b466d5b97db77a0f9563f047723e134974be6bf170da0f821b2 -main.dart.js,1779597496677,fce3d453f3b298a9672bb06cd23598463329fddd17aff0a00e85491492561209 +version.json,1779773516010,f7f931727903f67c4b2ec2c38d4c5c4e2ec1be0c03071b8bff0c27f5be3b9e6e +index.html,1779773005011,3ffbcfc0c9d82b851ce84d2b1bfc85cea0a34b077bdad955e7cf52643a3cf434 +flutter_bootstrap.js,1779773003255,e0fe03959702c0d5ddd83edfe1a7f169b837b63b5367b9ba539a30282407c7db +assets/FontManifest.json,1779773518685,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 +assets/AssetManifest.bin.json,1779773518684,e23e10111498cc3b8b40e6549dcde13cffa6f45dc78e88194f4a7e284bb68294 +assets/shaders/stretch_effect.frag,1779773518944,586ff23669838dd33070077916f0b971a8800f67093ae8d6eed47e6cea50926f +assets/AssetManifest.bin,1779773518680,fe7319a65158e186661b7edfcc1c2b6c0a6bc6823b923a13e1e932cd66407fa2 +assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779773520764,85140ea453cd3e02bd93650fe8add6a724ae8c0ac73d0c491085b301e7bd21d2 +flutter_service_worker.js,1779773528509,9a62890f30ffa44672087bab2b5c6225f9df04d908b127e60a4a4bf9103f4753 +assets/shaders/ink_sparkle.frag,1779773518950,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 +assets/fonts/MaterialIcons-Regular.otf,1779773520764,61076b4cea85b8f6d0d93ff86a36697781aaea4ca0f5f8989d3cfc342e4e786f +assets/NOTICES,1779773518685,62963000bb4f8b466d5b97db77a0f9563f047723e134974be6bf170da0f821b2 +main.dart.js,1779773387114,697202872b819f01066183694c782671a4b9447013a8c1b30d101532bcd52c15 diff --git a/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart b/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart index 4125431..a90fdb5 100644 --- a/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart +++ b/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart @@ -22,8 +22,12 @@ class SignInBloc extends Bloc { on((event, emit) async { emit(SignInProcess()); try { - await _userRepository.signInWithGoogle(); - emit(SignInSuccess()); + final didSignIn = await _userRepository.signInWithGoogle(); + if (didSignIn) { + emit(SignInSuccess()); + } else { + emit(SignInInitial()); + } } catch (e) { emit(SignInFailure()); } diff --git a/lib/screens/monitoring/shared/utils/excel/evaporasi_excel_service.dart b/lib/screens/monitoring/shared/utils/excel/evaporasi_excel_service.dart index e9393ee..7ccd62d 100644 --- a/lib/screens/monitoring/shared/utils/excel/evaporasi_excel_service.dart +++ b/lib/screens/monitoring/shared/utils/excel/evaporasi_excel_service.dart @@ -14,17 +14,17 @@ import 'excel_saver_stub.dart' class EvaporasiExcelService { // ── Format helper ────────────────────────────────────────── - static final _dateFmt = DateFormat('dd/MM/yyyy HH:mm:ss', 'id_ID'); + // 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 + 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 export({ @@ -138,10 +138,8 @@ class EvaporasiExcelService { // -- 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 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); @@ -167,8 +165,7 @@ class EvaporasiExcelService { _setCellDouble(sheet, 15, 1, evapMin); if (tempValues.isNotEmpty) { - final tempAvg = - tempValues.reduce((a, b) => a + b) / tempValues.length; + 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); @@ -223,10 +220,9 @@ class EvaporasiExcelService { // 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 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() @@ -238,27 +234,18 @@ class EvaporasiExcelService { // -- Isi baris data -- for (var i = 0; i < sorted.length; i++) { - final item = sorted[i]; + final item = sorted[i]; final rowIdx = i + 1; final status = _getStatus(item.evaporasi); - final bgColor = i.isEven ? _colorNormal : _colorWhite; + final bgColor = i.isEven ? _colorNormal : _colorWhite; final statusBg = _statusBgColor(status); - _setCellInt(sheet, rowIdx, 0, i + 1, - bgColor: bgColor, centered: true); - _setCell( - sheet, - rowIdx, - 1, + _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); + _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, @@ -311,16 +298,15 @@ class EvaporasiExcelService { String fontColor = '000000', bool centered = false, }) { - final cell = sheet - .cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row)); + 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, + horizontalAlign: centered ? HorizontalAlign.Center : HorizontalAlign.Left, verticalAlign: VerticalAlign.Center, textWrapping: TextWrapping.WrapText, leftBorder: Border( @@ -346,8 +332,8 @@ class EvaporasiExcelService { String bgColor = _colorWhite, bool centered = false, }) { - final cell = sheet - .cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row)); + 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'), @@ -377,8 +363,8 @@ class EvaporasiExcelService { String bgColor = _colorWhite, bool centered = false, }) { - final cell = sheet - .cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row)); + final cell = + sheet.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row)); cell.value = IntCellValue(value); cell.cellStyle = CellStyle( backgroundColorHex: ExcelColor.fromHexString('#$bgColor'), diff --git a/lib/screens/monitoring/shared/utils/excel/excel_saver_mobile.dart b/lib/screens/monitoring/shared/utils/excel/excel_saver_mobile.dart index e0e9b16..48ba133 100644 --- a/lib/screens/monitoring/shared/utils/excel/excel_saver_mobile.dart +++ b/lib/screens/monitoring/shared/utils/excel/excel_saver_mobile.dart @@ -5,15 +5,23 @@ import 'package:share_plus/share_plus.dart'; /// Mobile (Android/iOS): simpan ke temp dir lalu buka share sheet Future saveAndShareExcel(Uint8List bytes, String fileName) async { - final dir = await getTemporaryDirectory(); + final dir = await getTemporaryDirectory(); final file = File('${dir.path}/$fileName'); await file.writeAsBytes(bytes, flush: true); + // Wait a short moment to ensure filesystem has flushed and other apps can access file + await Future.delayed(const Duration(milliseconds: 150)); + + // Provide the name to XFile so receiving apps can preserve the filename await Share.shareXFiles( - [XFile( - file.path, - mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - )], + [ + XFile( + file.path, + name: fileName, + mimeType: + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ) + ], subject: fileName, ); } diff --git a/lib/screens/monitoring/shared/utils/excel/excel_saver_web.dart b/lib/screens/monitoring/shared/utils/excel/excel_saver_web.dart index eb0bc20..827db16 100644 --- a/lib/screens/monitoring/shared/utils/excel/excel_saver_web.dart +++ b/lib/screens/monitoring/shared/utils/excel/excel_saver_web.dart @@ -8,9 +8,23 @@ Future saveAndShareExcel(Uint8List bytes, String fileName) async { [bytes], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ); - final url = html.Url.createObjectUrlFromBlob(blob); - final anchor = html.AnchorElement(href: url) - ..setAttribute('download', fileName) - ..click(); + + final url = html.Url.createObjectUrlFromBlob(blob); + + // Create a hidden anchor, set download attr and append to document + final anchor = html.AnchorElement() + ..href = url + ..download = fileName + ..style.display = 'none'; + + // Append to DOM so some browsers respect the download attribute + html.document.body?.append(anchor); + + // Dispatch click — this should trigger a single download with correct name + anchor.click(); + + // Clean up: remove anchor and revoke URL after a short delay to ensure download starts + anchor.remove(); + await Future.delayed(const Duration(milliseconds: 500)); html.Url.revokeObjectUrl(url); } 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 45c4c76..fdaa1b2 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 @@ -10,7 +10,6 @@ import 'dart:typed_data'; import 'package:excel/excel.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:intl/intl.dart'; import 'package:monitoring_repository/monitoring_repository.dart'; @@ -20,34 +19,100 @@ import 'excel_saver_stub.dart' class WindSpeedExcelService { // ── Format helper ────────────────────────────────────────── - static final _dateFmt = DateFormat('dd/MM/yyyy HH:mm:ss', 'id_ID'); - static final _fileFmt = DateFormat('yyyyMMdd_HHmmss'); 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 _colorWaspada = 'FFF3CD'; // kuning muda - static const _colorBahaya = 'F8D7DA'; // merah muda + static const _colorHeader = '1A4A8C'; + static const _colorSubHead = '2E75B6'; + static const _colorNormal = 'E8F4FD'; + static const _colorWaspada = 'FFF3CD'; + static const _colorBahaya = 'F8D7DA'; static const _colorWhite = 'FFFFFF'; - /// Export data kecepatan angin ke file Excel dan buka share sheet + // ════════════════════════════════════════════════════════════ + // Filter helper — dipakai oleh dua sheet + // ════════════════════════════════════════════════════════════ + static List _applyFilter( + List history, { + DateTime? dateFrom, + DateTime? dateTo, + int? hourFrom, + int? minuteFrom, + int? hourTo, + int? minuteTo, + }) { + return history.where((e) { + // ── Filter tanggal ────────────────────────────────────── + 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); + if (d.isBefore(from) || d.isAfter(to)) return false; + } + + // ── Filter jam ────────────────────────────────────────── + if (hourFrom != null && hourTo != null) { + // Bandingkan dalam menit total agar menit ikut diperhitungkan + final eMin = e.timestamp.hour * 60 + e.timestamp.minute; + final fromMin = hourFrom * 60 + (minuteFrom ?? 0); + final toMin = hourTo * 60 + (minuteTo ?? 59); + + final pass = fromMin <= toMin + ? eMin >= fromMin && eMin <= toMin // normal: 08:00–17:00 + : eMin >= fromMin || eMin <= toMin; // overnight: 23:00–02:00 + if (!pass) return false; + } + + return true; + }).toList(); + } + + // ════════════════════════════════════════════════════════════ + // PUBLIC: export + // ════════════════════════════════════════════════════════════ static Future export({ required double currentSpeed, required String alertLevel, required String period, required List history, - required String fileName, // ← nama custom dari user - DateTime? dateFrom, // ← filter dari tanggal - DateTime? dateTo, // ← filter sampai tanggal + required String fileName, + DateTime? dateFrom, + DateTime? dateTo, + int? hourFrom, // ← jam mulai (0–23) + int? minuteFrom, // ← menit mulai + int? hourTo, // ← jam selesai (0–23) + int? minuteTo, // ← menit selesai }) async { + // Terapkan filter SATU KALI, hasil dipakai kedua sheet + final filtered = _applyFilter( + history, + dateFrom: dateFrom, + dateTo: dateTo, + hourFrom: hourFrom, + minuteFrom: minuteFrom, + hourTo: hourTo, + minuteTo: minuteTo, + ); + final excel = Excel.createExcel(); excel.delete('Sheet1'); _buildSummarySheet( - excel, currentSpeed, alertLevel, period, history, dateFrom, dateTo); - _buildHistorySheet(excel, history, dateFrom, dateTo); + excel, + currentSpeed, + alertLevel, + period, + filtered, + history.length, + dateFrom: dateFrom, + dateTo: dateTo, + hourFrom: hourFrom, + minuteFrom: minuteFrom, + hourTo: hourTo, + minuteTo: minuteTo, + ); + _buildHistorySheet(excel, filtered); // ── Simpan file ───────────────────────────────────────── final bytes = excel.save(); @@ -59,16 +124,22 @@ class WindSpeedExcelService { // ════════════════════════════════════════════════════════════ // SHEET 1: Ringkasan + // — statistik dihitung dari [filteredHistory] bukan all data // ════════════════════════════════════════════════════════════ static void _buildSummarySheet( Excel excel, double currentSpeed, String alertLevel, String period, - List history, + List filteredHistory, + int totalCount, { DateTime? dateFrom, DateTime? dateTo, - ) { + int? hourFrom, + int? minuteFrom, + int? hourTo, + int? minuteTo, + }) { final sheet = excel['Ringkasan']; // -- Judul -- @@ -85,79 +156,103 @@ class WindSpeedExcelService { bold: true, bgColor: _colorSubHead, fontColor: _colorWhite); _setCell(sheet, 1, 1, _headerFmt.format(DateTime.now()), bgColor: _colorNormal); + _setCell(sheet, 2, 0, 'Periode grafik', bold: true, bgColor: _colorSubHead, fontColor: _colorWhite); _setCell(sheet, 2, 1, period, bgColor: _colorNormal); + int metaRow = 3; + if (dateFrom != null && dateTo != null) { - _setCell(sheet, 3, 0, 'Filter tanggal', + _setCell(sheet, metaRow, 0, 'Filter tanggal', bold: true, bgColor: _colorSubHead, fontColor: _colorWhite); _setCell( sheet, - 3, + metaRow, 1, '${DateFormat('dd MMMM yyyy', 'id_ID').format(dateFrom)} → ${DateFormat('dd MMMM yyyy', 'id_ID').format(dateTo)}', bgColor: _colorNormal, ); + metaRow++; } - // -- Kecepatan saat ini -- - _setCell(sheet, 5, 0, 'KECEPATAN SAAT INI', + if (hourFrom != null && hourTo != null) { + final fromStr = + '${hourFrom.toString().padLeft(2, '0')}:${(minuteFrom ?? 0).toString().padLeft(2, '0')}'; + final toStr = + '${hourTo.toString().padLeft(2, '0')}:${(minuteTo ?? 59).toString().padLeft(2, '0')}'; + _setCell(sheet, metaRow, 0, 'Filter jam', + bold: true, bgColor: _colorSubHead, fontColor: _colorWhite); + _setCell(sheet, metaRow, 1, '$fromStr → $toStr', bgColor: _colorNormal); + metaRow++; + } + + // -- Kecepatan realtime saat ini -- + final secRow = metaRow + 1; + _setCell(sheet, secRow, 0, 'KECEPATAN SAAT INI', bold: true, bgColor: _colorSubHead, fontColor: _colorWhite); - sheet.merge(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 5), - CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: 5)); + sheet.merge(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: secRow), + CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: secRow)); - _setCell(sheet, 6, 0, 'Kecepatan (m/s)', bold: true); - _setCellDouble(sheet, 6, 1, currentSpeed); - _setCell(sheet, 7, 0, 'Kecepatan (km/h)', bold: true); - _setCellDouble(sheet, 7, 1, currentSpeed * 3.6); - _setCell(sheet, 8, 0, 'Status', bold: true); - final statusColor = _alertBgColor(alertLevel); - _setCell(sheet, 8, 1, alertLevel, bgColor: statusColor); + _setCell(sheet, secRow + 1, 0, 'Kecepatan (m/s)', bold: true); + _setCellDouble(sheet, secRow + 1, 1, currentSpeed); + _setCell(sheet, secRow + 2, 0, 'Kecepatan (km/h)', bold: true); + _setCellDouble(sheet, secRow + 2, 1, currentSpeed * 3.6); + _setCell(sheet, secRow + 3, 0, 'Status', bold: true); + _setCell(sheet, secRow + 3, 1, alertLevel, + bgColor: _alertBgColor(alertLevel)); - // -- Statistik ringkasan -- - if (history.isNotEmpty) { - final speeds = history.map((e) => e.speed).toList(); + // -- Statistik dari data TERFILTER (bukan all history) -- + final statRow = secRow + 5; + _setCell(sheet, statRow, 0, 'STATISTIK DATA DIEKSPOR', + bold: true, bgColor: _colorSubHead, fontColor: _colorWhite); + sheet.merge(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: statRow), + CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: statRow)); + + _setCell(sheet, statRow + 1, 0, 'Total data tersimpan', bold: true); + _setCellInt(sheet, statRow + 1, 1, totalCount); + _setCell(sheet, statRow + 2, 0, 'Data diekspor', bold: true); + _setCellInt(sheet, statRow + 2, 1, filteredHistory.length); + + if (filteredHistory.isNotEmpty) { + final speeds = filteredHistory.map((e) => e.speed).toList(); final avg = speeds.reduce((a, b) => a + b) / speeds.length; final max = speeds.reduce((a, b) => a > b ? a : b); final min = speeds.reduce((a, b) => a < b ? a : b); - _setCell(sheet, 10, 0, 'STATISTIK HISTORY', - bold: true, bgColor: _colorSubHead, fontColor: _colorWhite); - sheet.merge(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 10), - CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: 10)); - - _setCell(sheet, 11, 0, 'Jumlah data', bold: true); - _setCellInt(sheet, 11, 1, history.length); - _setCell(sheet, 12, 0, 'Rata-rata (m/s)', bold: true); - _setCellDouble(sheet, 12, 1, avg); - _setCell(sheet, 13, 0, 'Maksimum (m/s)', bold: true); - _setCellDouble(sheet, 13, 1, max); - _setCell(sheet, 14, 0, 'Minimum (m/s)', bold: true); - _setCellDouble(sheet, 14, 1, min); - _setCell(sheet, 15, 0, 'Rata-rata (km/h)', bold: true); - _setCellDouble(sheet, 15, 1, avg * 3.6); + _setCell(sheet, statRow + 3, 0, 'Rata-rata (m/s)', bold: true); + _setCellDouble(sheet, statRow + 3, 1, avg); + _setCell(sheet, statRow + 4, 0, 'Maksimum (m/s)', bold: true); + _setCellDouble(sheet, statRow + 4, 1, max); + _setCell(sheet, statRow + 5, 0, 'Minimum (m/s)', bold: true); + _setCellDouble(sheet, statRow + 5, 1, min); + _setCell(sheet, statRow + 6, 0, 'Rata-rata (km/h)', bold: true); + _setCellDouble(sheet, statRow + 6, 1, avg * 3.6); + } else { + _setCell(sheet, statRow + 3, 0, 'Tidak ada data dalam filter ini', + bgColor: _colorWaspada); + sheet.merge( + CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: statRow + 3), + CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: statRow + 3), + ); } - // Set lebar kolom - sheet.setColumnWidth(0, 22); - sheet.setColumnWidth(1, 22); + sheet.setColumnWidth(0, 24); + sheet.setColumnWidth(1, 28); sheet.setColumnWidth(2, 18); sheet.setColumnWidth(3, 18); } // ════════════════════════════════════════════════════════════ // SHEET 2: Data History + // — menerima data yang sudah difilter, tidak filter ulang // ════════════════════════════════════════════════════════════ static void _buildHistorySheet( Excel excel, - List history, - DateTime? dateFrom, - DateTime? dateTo, + List data, ) { final sheet = excel['Data History']; - // -- Header kolom -- final headers = [ 'No', 'Tanggal', @@ -174,48 +269,38 @@ class WindSpeedExcelService { 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; - - // -- Isi baris data -- - for (var i = 0; i < data.length; i++) { - final item = data[i]; - final rowIdx = i + 1; - final kmh = item.speed * 3.6; - final status = _getAlertLevel(item.speed); - final bgColor = i.isEven ? _colorNormal : _colorWhite; - final statusBg = _alertBgColor(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.speed, - bgColor: bgColor, centered: true); - _setCellDouble(sheet, rowIdx, 4, kmh, bgColor: bgColor, centered: true); - _setCell(sheet, rowIdx, 5, status, - bgColor: statusBg, centered: true, bold: true); - } - - // -- Footer jika kosong -- if (data.isEmpty) { - _setCell(sheet, 1, 0, 'Tidak ada data untuk tanggal yang dipilih', + _setCell(sheet, 1, 0, 'Tidak ada data untuk filter yang dipilih', bgColor: _colorWaspada, centered: true); sheet.merge(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 1), CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: 1)); + } else { + // Urutkan ascending (terlama → terbaru) di Excel + final sorted = [...data] + ..sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + for (var i = 0; i < sorted.length; i++) { + final item = sorted[i]; + final rowIdx = i + 1; + final kmh = item.speed * 3.6; + final status = _getAlertLevel(item.speed); + final bgColor = i.isEven ? _colorNormal : _colorWhite; + + _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.speed, + bgColor: bgColor, centered: true); + _setCellDouble(sheet, rowIdx, 4, kmh, bgColor: bgColor, centered: true); + _setCell(sheet, rowIdx, 5, status, + bgColor: _alertBgColor(status), centered: true, bold: true); + } } - // Set lebar kolom sheet.setColumnWidth(0, 6); sheet.setColumnWidth(1, 14); sheet.setColumnWidth(2, 12); @@ -227,6 +312,36 @@ class WindSpeedExcelService { // ════════════════════════════════════════════════════════════ // HELPER CELLS // ════════════════════════════════════════════════════════════ + static CellStyle _baseStyle({ + String bgColor = _colorWhite, + String fontColor = '000000', + bool bold = false, + int fontSize = 11, + bool centered = false, + }) => + CellStyle( + bold: bold, + fontSize: fontSize, + 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 _setCell( Sheet sheet, int row, diff --git a/packages/user_repository/lib/src/firebase_user_repo.dart b/packages/user_repository/lib/src/firebase_user_repo.dart index c5a14ab..a711594 100644 --- a/packages/user_repository/lib/src/firebase_user_repo.dart +++ b/packages/user_repository/lib/src/firebase_user_repo.dart @@ -35,7 +35,13 @@ class FirebaseUserRepo implements UserRepository { } } catch (e) { log('Error fetching user data: $e'); - yield MyUser.empty; + // Firestore sementara tidak tersedia; jangan anggap user logout. + yield MyUser( + userId: firebaseUser.uid, + email: firebaseUser.email ?? '', + name: firebaseUser.displayName ?? '', + hasActiveCart: false, + ); } } }); @@ -91,10 +97,10 @@ class FirebaseUserRepo implements UserRepository { } @override - Future signInWithGoogle() async { + Future signInWithGoogle() async { try { final googleUser = await _googleSignIn.signIn(); - if (googleUser == null) return; // user cancel + if (googleUser == null) return false; // user cancel final googleAuth = await googleUser.authentication; final credential = GoogleAuthProvider.credential( @@ -117,6 +123,7 @@ class FirebaseUserRepo implements UserRepository { ); await setUserData(myUser); } + return true; } catch (e) { log(e.toString()); rethrow; diff --git a/packages/user_repository/lib/src/user_repo.dart b/packages/user_repository/lib/src/user_repo.dart index f90adfa..7a978eb 100644 --- a/packages/user_repository/lib/src/user_repo.dart +++ b/packages/user_repository/lib/src/user_repo.dart @@ -9,7 +9,7 @@ abstract class UserRepository { Future signIn(String email, String password); - Future signInWithGoogle(); + Future signInWithGoogle(); Future logOut(); }