420 lines
14 KiB
Dart
420 lines
14 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:excel/excel.dart';
|
|
import 'package:open_file/open_file.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import '../services/supabase_service.dart';
|
|
import '../services/mqtt_service.dart';
|
|
import 'package:mqtt_client/mqtt_client.dart';
|
|
|
|
class SettingsScreen extends StatefulWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
@override
|
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
|
}
|
|
|
|
class _SettingsScreenState extends State<SettingsScreen> {
|
|
String _namaLengkap = '';
|
|
bool _isLoadingProfile = true;
|
|
bool _isExporting = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadProfile();
|
|
}
|
|
|
|
Future<void> _loadProfile() async {
|
|
try {
|
|
final profile = await SupabaseService().getProfile();
|
|
if (mounted) {
|
|
setState(() {
|
|
_namaLengkap = profile?['nama_lengkap'] ?? '';
|
|
_isLoadingProfile = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) setState(() => _isLoadingProfile = false);
|
|
}
|
|
}
|
|
|
|
// ── Export foto_dataset ke Excel ──
|
|
Future<void> _exportExcel() async {
|
|
setState(() => _isExporting = true);
|
|
|
|
try {
|
|
// Ambil semua data dari foto_dataset
|
|
final data = await SupabaseService().getFotoDataset();
|
|
|
|
if (data.isEmpty) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Belum ada data dataset untuk diexport!'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Buat file Excel
|
|
final excel = Excel.createExcel();
|
|
final sheet = excel['Dataset Kopi'];
|
|
|
|
// Header kolom
|
|
final headers = [
|
|
'No',
|
|
'Timestamp',
|
|
'Suhu (°C)',
|
|
'Kelembapan (%)',
|
|
'Intensitas Cahaya (Lux)',
|
|
'Kipas 1 (Exhaust)',
|
|
'Kipas 2 (Intake)',
|
|
'URL Foto',
|
|
];
|
|
|
|
// Style header
|
|
for (int i = 0; i < headers.length; i++) {
|
|
final cell = sheet.cell(
|
|
CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0));
|
|
cell.value = TextCellValue(headers[i]);
|
|
cell.cellStyle = CellStyle(
|
|
bold: true,
|
|
backgroundColorHex: ExcelColor.fromHexString('#16A34A'),
|
|
fontColorHex: ExcelColor.fromHexString('#FFFFFF'),
|
|
);
|
|
}
|
|
|
|
// Isi data
|
|
for (int i = 0; i < data.length; i++) {
|
|
final row = data[i];
|
|
final rowIndex = i + 1;
|
|
|
|
sheet
|
|
.cell(CellIndex.indexByColumnRow(
|
|
columnIndex: 0, rowIndex: rowIndex))
|
|
.value = IntCellValue(i + 1);
|
|
|
|
sheet
|
|
.cell(CellIndex.indexByColumnRow(
|
|
columnIndex: 1, rowIndex: rowIndex))
|
|
.value = TextCellValue(row['timestamp']?.toString() ?? '-');
|
|
|
|
sheet
|
|
.cell(CellIndex.indexByColumnRow(
|
|
columnIndex: 2, rowIndex: rowIndex))
|
|
.value = DoubleCellValue(
|
|
double.tryParse(row['suhu'].toString()) ?? 0);
|
|
|
|
sheet
|
|
.cell(CellIndex.indexByColumnRow(
|
|
columnIndex: 3, rowIndex: rowIndex))
|
|
.value = DoubleCellValue(
|
|
double.tryParse(row['kelembapan'].toString()) ?? 0);
|
|
|
|
sheet
|
|
.cell(CellIndex.indexByColumnRow(
|
|
columnIndex: 4, rowIndex: rowIndex))
|
|
.value = DoubleCellValue(
|
|
double.tryParse(row['intensitas'].toString()) ?? 0);
|
|
|
|
sheet
|
|
.cell(CellIndex.indexByColumnRow(
|
|
columnIndex: 5, rowIndex: rowIndex))
|
|
.value = TextCellValue(row['kipas1']?.toString() ?? '-');
|
|
|
|
sheet
|
|
.cell(CellIndex.indexByColumnRow(
|
|
columnIndex: 6, rowIndex: rowIndex))
|
|
.value = TextCellValue(row['kipas2']?.toString() ?? '-');
|
|
|
|
sheet
|
|
.cell(CellIndex.indexByColumnRow(
|
|
columnIndex: 7, rowIndex: rowIndex))
|
|
.value = TextCellValue(row['url_foto']?.toString() ?? '-');
|
|
}
|
|
|
|
// Set lebar kolom
|
|
sheet.setColumnWidth(0, 5);
|
|
sheet.setColumnWidth(1, 22);
|
|
sheet.setColumnWidth(2, 12);
|
|
sheet.setColumnWidth(3, 16);
|
|
sheet.setColumnWidth(4, 24);
|
|
sheet.setColumnWidth(5, 18);
|
|
sheet.setColumnWidth(6, 18);
|
|
sheet.setColumnWidth(7, 50);
|
|
|
|
// Simpan file
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
final tanggal = DateTime.now()
|
|
.toString()
|
|
.substring(0, 10)
|
|
.replaceAll('-', '');
|
|
final fileName = 'dataset_kopi_$tanggal.xlsx';
|
|
final filePath = '${dir.path}/$fileName';
|
|
final fileBytes = excel.save();
|
|
|
|
if (fileBytes == null) throw Exception('Gagal generate file Excel');
|
|
|
|
final file = File(filePath);
|
|
await file.writeAsBytes(fileBytes);
|
|
|
|
// Share file
|
|
await OpenFile.open(filePath);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'✓ Export berhasil — ${data.length} data tersimpan'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Gagal export: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _isExporting = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final mqtt = Provider.of<MqttService>(context);
|
|
final user = SupabaseService().currentUser;
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
body: SafeArea(
|
|
child: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// ── Profile Section ──
|
|
Container(
|
|
padding: const EdgeInsets.all(15),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1A1A1A),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const CircleAvatar(
|
|
radius: 30,
|
|
backgroundColor: Colors.green,
|
|
child: Icon(Icons.person,
|
|
color: Colors.white, size: 40),
|
|
),
|
|
const SizedBox(width: 15),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_isLoadingProfile
|
|
? const SizedBox(
|
|
width: 100,
|
|
height: 16,
|
|
child: LinearProgressIndicator(
|
|
color: Colors.green,
|
|
backgroundColor: Colors.grey,
|
|
),
|
|
)
|
|
: Text(
|
|
_namaLengkap.isNotEmpty
|
|
? _namaLengkap
|
|
: 'Admin Kopi',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
user?.email ?? 'admin@coffee.io',
|
|
style: const TextStyle(
|
|
color: Colors.grey, fontSize: 12),
|
|
),
|
|
const Text(
|
|
'Status: Active User',
|
|
style: TextStyle(
|
|
color: Colors.green, fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
// ── Sistem & Koneksi ──
|
|
_buildSectionTitle('Sistem & Koneksi'),
|
|
_buildSettingsCard([
|
|
_buildConnectionStatus(
|
|
'Database Supabase', true, Icons.storage),
|
|
const Divider(color: Colors.grey, height: 1),
|
|
_buildConnectionStatus(
|
|
'MQTT Broker (EMQX)',
|
|
mqtt.client?.connectionStatus?.state ==
|
|
MqttConnectionState.connected,
|
|
Icons.cloud_sync,
|
|
),
|
|
]),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// ── Manajemen Data ──
|
|
_buildSectionTitle('Manajemen Data'),
|
|
_buildSettingsCard([
|
|
_isExporting
|
|
? const ListTile(
|
|
leading: SizedBox(
|
|
width: 22,
|
|
height: 22,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.green, strokeWidth: 2),
|
|
),
|
|
title: Text(
|
|
'Menyiapkan file Excel...',
|
|
style: TextStyle(
|
|
color: Colors.grey, fontSize: 14),
|
|
),
|
|
)
|
|
: _buildSettingItem(
|
|
Icons.table_chart,
|
|
'Export Data Dataset (.xlsx)',
|
|
_exportExcel,
|
|
),
|
|
]),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// ── Keamanan ──
|
|
_buildSectionTitle('Keamanan'),
|
|
_buildSettingItem(
|
|
Icons.lock_reset, 'Ganti Password Akun', () {
|
|
Navigator.pushNamed(
|
|
context,
|
|
'/change_password',
|
|
arguments: {'dari_reset': false},
|
|
);
|
|
}),
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
// ── Tombol Keluar ──
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 55,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () async {
|
|
await SupabaseService().signOut();
|
|
if (context.mounted) {
|
|
Navigator.pushReplacementNamed(context, '/login');
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.redAccent.withOpacity(0.8),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.logout, color: Colors.white),
|
|
label: const Text(
|
|
'Keluar dari Akun',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 100),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionTitle(String title) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 10, left: 5),
|
|
child: Text(
|
|
title,
|
|
style: const TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSettingsCard(List<Widget> children) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1A1A1A),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Column(children: children),
|
|
);
|
|
}
|
|
|
|
Widget _buildSettingItem(IconData icon, String title, VoidCallback? onTap,
|
|
{String? trailing}) {
|
|
return ListTile(
|
|
leading: Icon(icon, color: Colors.green, size: 22),
|
|
title: Text(title,
|
|
style: const TextStyle(color: Colors.white, fontSize: 14)),
|
|
trailing: trailing != null
|
|
? Text(trailing,
|
|
style: const TextStyle(color: Colors.grey, fontSize: 12))
|
|
: const Icon(Icons.chevron_right, color: Colors.grey, size: 18),
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
|
|
Widget _buildConnectionStatus(
|
|
String title, bool isConnected, IconData icon) {
|
|
return ListTile(
|
|
leading: Icon(icon,
|
|
color: isConnected ? Colors.blue : Colors.red, size: 22),
|
|
title: Text(title,
|
|
style: const TextStyle(color: Colors.white, fontSize: 14)),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: isConnected ? Colors.green : Colors.red,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
isConnected ? 'Online' : 'Offline',
|
|
style: TextStyle(
|
|
color: isConnected ? Colors.green : Colors.red,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |