oke
This commit is contained in:
parent
5602e488be
commit
21e471d0ea
|
|
@ -14,7 +14,6 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
|
|
||||||
// Import kondisional: web pakai dart:html, mobile pakai io + share_plus
|
|
||||||
import 'excel_saver_stub.dart'
|
import 'excel_saver_stub.dart'
|
||||||
if (dart.library.html) 'excel_saver_web.dart'
|
if (dart.library.html) 'excel_saver_web.dart'
|
||||||
if (dart.library.io) 'excel_saver_mobile.dart';
|
if (dart.library.io) 'excel_saver_mobile.dart';
|
||||||
|
|
@ -39,21 +38,23 @@ class WindSpeedExcelService {
|
||||||
required String alertLevel,
|
required String alertLevel,
|
||||||
required String period,
|
required String period,
|
||||||
required List<MyWindSpeed> history,
|
required List<MyWindSpeed> history,
|
||||||
DateTime? filterDate,
|
required String fileName, // ← nama custom dari user
|
||||||
|
DateTime? dateFrom, // ← filter dari tanggal
|
||||||
|
DateTime? dateTo, // ← filter sampai tanggal
|
||||||
}) async {
|
}) async {
|
||||||
final excel = Excel.createExcel();
|
final excel = Excel.createExcel();
|
||||||
excel.delete('Sheet1');
|
excel.delete('Sheet1');
|
||||||
|
|
||||||
_buildSummarySheet(
|
_buildSummarySheet(
|
||||||
excel, currentSpeed, alertLevel, period, history, filterDate);
|
excel, currentSpeed, alertLevel, period, history, dateFrom, dateTo);
|
||||||
_buildHistorySheet(excel, history, filterDate);
|
_buildHistorySheet(excel, history, dateFrom, dateTo);
|
||||||
|
|
||||||
// ── Simpan file ─────────────────────────────────────────
|
// ── Simpan file ─────────────────────────────────────────
|
||||||
final bytes = excel.save();
|
final bytes = excel.save();
|
||||||
if (bytes == null) throw Exception('Gagal membuat file Excel');
|
if (bytes == null) throw Exception('Gagal membuat file Excel');
|
||||||
final name = 'wind_speed_${_fileFmt.format(DateTime.now())}.xlsx';
|
|
||||||
|
|
||||||
await saveAndShareExcel(Uint8List.fromList(bytes), name);
|
final safeName = fileName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
|
||||||
|
await saveAndShareExcel(Uint8List.fromList(bytes), '$safeName.xlsx');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
@ -65,7 +66,8 @@ class WindSpeedExcelService {
|
||||||
String alertLevel,
|
String alertLevel,
|
||||||
String period,
|
String period,
|
||||||
List<MyWindSpeed> history,
|
List<MyWindSpeed> history,
|
||||||
DateTime? filterDate,
|
DateTime? dateFrom,
|
||||||
|
DateTime? dateTo,
|
||||||
) {
|
) {
|
||||||
final sheet = excel['Ringkasan'];
|
final sheet = excel['Ringkasan'];
|
||||||
|
|
||||||
|
|
@ -87,12 +89,16 @@ class WindSpeedExcelService {
|
||||||
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
|
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
|
||||||
_setCell(sheet, 2, 1, period, bgColor: _colorNormal);
|
_setCell(sheet, 2, 1, period, bgColor: _colorNormal);
|
||||||
|
|
||||||
if (filterDate != null) {
|
if (dateFrom != null && dateTo != null) {
|
||||||
_setCell(sheet, 3, 0, 'Filter tanggal',
|
_setCell(sheet, 3, 0, 'Filter tanggal',
|
||||||
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
|
bold: true, bgColor: _colorSubHead, fontColor: _colorWhite);
|
||||||
_setCell(
|
_setCell(
|
||||||
sheet, 3, 1, DateFormat('dd MMMM yyyy', 'id_ID').format(filterDate),
|
sheet,
|
||||||
bgColor: _colorNormal);
|
3,
|
||||||
|
1,
|
||||||
|
'${DateFormat('dd MMMM yyyy', 'id_ID').format(dateFrom)} → ${DateFormat('dd MMMM yyyy', 'id_ID').format(dateTo)}',
|
||||||
|
bgColor: _colorNormal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Kecepatan saat ini --
|
// -- Kecepatan saat ini --
|
||||||
|
|
@ -146,7 +152,8 @@ class WindSpeedExcelService {
|
||||||
static void _buildHistorySheet(
|
static void _buildHistorySheet(
|
||||||
Excel excel,
|
Excel excel,
|
||||||
List<MyWindSpeed> history,
|
List<MyWindSpeed> history,
|
||||||
DateTime? filterDate,
|
DateTime? dateFrom,
|
||||||
|
DateTime? dateTo,
|
||||||
) {
|
) {
|
||||||
final sheet = excel['Data History'];
|
final sheet = excel['Data History'];
|
||||||
|
|
||||||
|
|
@ -167,14 +174,15 @@ class WindSpeedExcelService {
|
||||||
centered: true);
|
centered: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Filter jika ada tanggal dipilih --
|
// Filter berdasarkan rentang tanggal (inklusif kedua ujung)
|
||||||
final data = filterDate != null
|
final data = (dateFrom != null && dateTo != null)
|
||||||
? history
|
? history.where((e) {
|
||||||
.where((e) =>
|
final d =
|
||||||
e.timestamp.year == filterDate.year &&
|
DateTime(e.timestamp.year, e.timestamp.month, e.timestamp.day);
|
||||||
e.timestamp.month == filterDate.month &&
|
final from = DateTime(dateFrom.year, dateFrom.month, dateFrom.day);
|
||||||
e.timestamp.day == filterDate.day)
|
final to = DateTime(dateTo.year, dateTo.month, dateTo.day);
|
||||||
.toList()
|
return !d.isBefore(from) && !d.isAfter(to);
|
||||||
|
}).toList()
|
||||||
: history;
|
: history;
|
||||||
|
|
||||||
// -- Isi baris data --
|
// -- Isi baris data --
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,217 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Export Excel ─────────────────────────────────────────────
|
// ── Dialog export: nama file + date range ───────────────────
|
||||||
Future<void> _exportExcel(BuildContext context, WindSpeedState state) async {
|
Future<void> _showExportDialog(
|
||||||
|
BuildContext context, WindSpeedState state) async {
|
||||||
|
// Tentukan batas tanggal dari history
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// State dialog — pakai ValueNotifier supaya tidak perlu StatefulWidget terpisah
|
||||||
|
final nameController = TextEditingController(
|
||||||
|
text: 'kecepatan_angin_${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) => !e.timestamp.isBefore(dateFrom!) && !e.timestamp.isAfter(dateTo!.add(const Duration(days: 1)))).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,
|
||||||
|
WindSpeedState state,
|
||||||
|
String customName,
|
||||||
|
DateTime? dateFrom,
|
||||||
|
DateTime? dateTo,
|
||||||
|
) async {
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
final fileName = customName.isEmpty
|
||||||
|
? 'kecepatan_angin_${DateFormat('ddMMyyyy').format(DateTime.now())}'
|
||||||
|
: customName;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
|
|
@ -83,15 +291,17 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
||||||
alertLevel: state.alertLevel,
|
alertLevel: state.alertLevel,
|
||||||
period: state.selectedPeriod,
|
period: state.selectedPeriod,
|
||||||
history: state.history,
|
history: state.history,
|
||||||
filterDate: state.selectedDate,
|
fileName: fileName,
|
||||||
|
dateFrom: dateFrom,
|
||||||
|
dateTo: dateTo,
|
||||||
);
|
);
|
||||||
|
|
||||||
messenger.hideCurrentSnackBar();
|
messenger.hideCurrentSnackBar();
|
||||||
messenger.showSnackBar(SnackBar(
|
messenger.showSnackBar(SnackBar(
|
||||||
content: const Row(children: [
|
content: Row(children: [
|
||||||
Icon(Icons.check_circle, color: Colors.white),
|
const Icon(Icons.check_circle, color: Colors.white),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('File Excel berhasil dibuat!'),
|
Expanded(child: Text('$fileName.xlsx berhasil dibuat!')),
|
||||||
]),
|
]),
|
||||||
backgroundColor: Colors.green.shade600,
|
backgroundColor: Colors.green.shade600,
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
|
|
@ -129,7 +339,7 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
||||||
icon: const Icon(Icons.file_download_outlined),
|
icon: const Icon(Icons.file_download_outlined),
|
||||||
onPressed: state.history.isEmpty
|
onPressed: state.history.isEmpty
|
||||||
? null
|
? null
|
||||||
: () => _exportExcel(context, state),
|
: () => _showExportDialog(context, state),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue