This commit is contained in:
kleponijo 2026-05-27 16:36:43 +07:00
parent 5e854cc1e6
commit 9659dc5edf
8 changed files with 293 additions and 159 deletions

View File

@ -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

View File

@ -22,8 +22,12 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
on<GoogleSignInRequired>((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());
}

View File

@ -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<void> 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'),

View File

@ -5,15 +5,23 @@ import 'package:share_plus/share_plus.dart';
/// Mobile (Android/iOS): simpan ke temp dir lalu buka share sheet
Future<void> 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,
);
}

View File

@ -8,9 +8,23 @@ Future<void> 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);
}

View File

@ -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<MyWindSpeed> _applyFilter(
List<MyWindSpeed> 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<void> export({
required double currentSpeed,
required String alertLevel,
required String period,
required List<MyWindSpeed> 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<MyWindSpeed> history,
List<MyWindSpeed> 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<MyWindSpeed> history,
DateTime? dateFrom,
DateTime? dateTo,
List<MyWindSpeed> 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,

View File

@ -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<void> signInWithGoogle() async {
Future<bool> 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;

View File

@ -9,7 +9,7 @@ abstract class UserRepository {
Future<void> signIn(String email, String password);
Future<void> signInWithGoogle();
Future<bool> signInWithGoogle();
Future<void> logOut();
}