oke
This commit is contained in:
parent
5e854cc1e6
commit
9659dc5edf
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ abstract class UserRepository {
|
|||
|
||||
Future<void> signIn(String email, String password);
|
||||
|
||||
Future<void> signInWithGoogle();
|
||||
Future<bool> signInWithGoogle();
|
||||
|
||||
Future<void> logOut();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue