477 lines
18 KiB
Dart
477 lines
18 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:excel/excel.dart' hide Border;
|
|
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';
|
|
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
|
|
|
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 (Warna Excel Tetap Dipertahankan) ──
|
|
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(
|
|
SnackBar(
|
|
content: const Text('Belum ada data dataset untuk diexport!'),
|
|
// --- BERUBAH: Warna Notifikasi Peringatan ---
|
|
backgroundColor: AppColors.warning,
|
|
),
|
|
);
|
|
}
|
|
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 (Intake)',
|
|
'Kipas 2 (Exhaust)',
|
|
];
|
|
|
|
// Style header (Dibiarkan tetap hijau standar Excel)
|
|
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 — tanpa URL Foto
|
|
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() ?? '-');
|
|
}
|
|
|
|
// Set lebar kolom — tanpa kolom URL
|
|
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);
|
|
|
|
// 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'),
|
|
// --- BERUBAH: Warna Notifikasi Sukses ---
|
|
backgroundColor: AppColors.success,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Gagal export: $e'),
|
|
// --- BERUBAH: Warna Notifikasi Error ---
|
|
backgroundColor: AppColors.error,
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _isExporting = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final mqtt = Provider.of<MqttService>(context);
|
|
final user = SupabaseService().currentUser;
|
|
|
|
return Scaffold(
|
|
// --- BERUBAH: Background UI Utama ---
|
|
backgroundColor: AppColors.background,
|
|
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(
|
|
// --- BERUBAH: Background Kartu Profil ---
|
|
color: AppColors.cardBg,
|
|
borderRadius: BorderRadius.circular(20),
|
|
// --- BERUBAH: Garis pinggir halus ---
|
|
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const CircleAvatar(
|
|
radius: 30,
|
|
// --- BERUBAH: Warna Avatar ---
|
|
backgroundColor: AppColors.primary,
|
|
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(
|
|
// --- BERUBAH: Warna Loading ---
|
|
color: AppColors.primary,
|
|
backgroundColor: AppColors.background,
|
|
),
|
|
)
|
|
: Text(
|
|
_namaLengkap.isNotEmpty
|
|
? _namaLengkap
|
|
: 'Admin Kopi',
|
|
style: const TextStyle(
|
|
// --- BERUBAH: Warna Nama Profil ---
|
|
color: AppColors.textMain,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
user?.email ?? 'admin@coffee.io',
|
|
style: const TextStyle(
|
|
// --- BERUBAH: Warna Email Profil ---
|
|
color: AppColors.textSecondary,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const Text(
|
|
'Status: Active User',
|
|
style: TextStyle(
|
|
// --- BERUBAH: Warna Status Profil ---
|
|
color: AppColors.success,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
// ── Sistem & Koneksi ──
|
|
_buildSectionTitle('Sistem & Koneksi'),
|
|
_buildSettingsCard([
|
|
_buildConnectionStatus(
|
|
'Database Supabase', true, Icons.storage),
|
|
// --- BERUBAH: Warna Garis Pemisah Menu ---
|
|
Divider(color: AppColors.textSecondary.withValues(alpha: 0.2), 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(
|
|
// --- BERUBAH: Warna Loading ---
|
|
color: AppColors.primary, strokeWidth: 2),
|
|
),
|
|
title: Text(
|
|
'Menyiapkan file Excel...',
|
|
style: TextStyle(
|
|
// --- BERUBAH: Warna Teks Loading ---
|
|
color: AppColors.textSecondary, fontSize: 14),
|
|
),
|
|
)
|
|
: _buildSettingItem(
|
|
Icons.table_chart,
|
|
'Export Data Dataset (.xlsx)',
|
|
_exportExcel,
|
|
),
|
|
]),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// ── Keamanan ──
|
|
_buildSectionTitle('Keamanan'),
|
|
_buildSettingsCard([
|
|
_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 {
|
|
final konfirmasi = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
// --- BERUBAH: Latar Belakang Dialog ---
|
|
backgroundColor: AppColors.cardBg,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
title: const Row(
|
|
children: [
|
|
Icon(Icons.logout, color: AppColors.error),
|
|
SizedBox(width: 10),
|
|
Text('Keluar dari Akun',
|
|
// --- BERUBAH: Warna Judul Dialog ---
|
|
style: TextStyle(color: AppColors.textMain, fontSize: 18)),
|
|
],
|
|
),
|
|
content: const Text(
|
|
'Apakah kamu yakin ingin keluar dari akun ini?',
|
|
// --- BERUBAH: Warna Teks Konten Dialog ---
|
|
style: TextStyle(color: AppColors.textSecondary),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: const Text('Batal',
|
|
// --- BERUBAH: Warna Tombol Batal ---
|
|
style: TextStyle(color: AppColors.primary)),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(ctx, true),
|
|
style: ElevatedButton.styleFrom(
|
|
// --- BERUBAH: Warna Tombol Keluar di Dialog ---
|
|
backgroundColor: AppColors.error,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
child: const Text('Keluar',
|
|
style: TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (konfirmasi == true) {
|
|
await SupabaseService().signOut();
|
|
if (context.mounted) {
|
|
Navigator.pushReplacementNamed(context, '/login');
|
|
}
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
// --- BERUBAH: Warna Latar Tombol Keluar ---
|
|
backgroundColor: AppColors.error.withValues(alpha: 0.8),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.logout, color: Colors.white), // Tetap putih
|
|
label: const Text(
|
|
'Keluar dari Akun',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white), // Tetap putih
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 100),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionTitle(String title) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 10, left: 5),
|
|
child: Text(
|
|
title,
|
|
style: const TextStyle(
|
|
// --- BERUBAH: Warna Judul Seksi/Kategori ---
|
|
color: AppColors.textSecondary,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSettingsCard(List<Widget> children) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
// --- BERUBAH: Latar Belakang Kartu Pengaturan ---
|
|
color: AppColors.cardBg,
|
|
borderRadius: BorderRadius.circular(20),
|
|
// --- BERUBAH: Garis pinggir halus ---
|
|
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
|
),
|
|
child: Column(children: children),
|
|
);
|
|
}
|
|
|
|
Widget _buildSettingItem(IconData icon, String title, VoidCallback? onTap,
|
|
{String? trailing}) {
|
|
return ListTile(
|
|
// --- BERUBAH: Warna Ikon Leading ---
|
|
leading: Icon(icon, color: AppColors.primary, size: 22),
|
|
title: Text(title,
|
|
// --- BERUBAH: Warna Teks Menu ---
|
|
style: const TextStyle(color: AppColors.textMain, fontSize: 14)),
|
|
trailing: trailing != null
|
|
? Text(trailing,
|
|
// --- BERUBAH: Warna Teks Trailing ---
|
|
style: const TextStyle(color: AppColors.textSecondary, fontSize: 12))
|
|
// --- BERUBAH: Warna Ikon Panah Kanan ---
|
|
: const Icon(Icons.chevron_right, color: AppColors.textSecondary, size: 18),
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
|
|
Widget _buildConnectionStatus(
|
|
String title, bool isConnected, IconData icon) {
|
|
return ListTile(
|
|
// --- BERUBAH: Warna Ikon Status Database/MQTT ---
|
|
leading: Icon(icon,
|
|
color: isConnected ? AppColors.info : AppColors.error, size: 22),
|
|
title: Text(title,
|
|
// --- BERUBAH: Warna Teks Nama Layanan ---
|
|
style: const TextStyle(color: AppColors.textMain, fontSize: 14)),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
// --- BERUBAH: Warna Titik Indikator ---
|
|
color: isConnected ? AppColors.success : AppColors.error,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
isConnected ? 'Online' : 'Offline',
|
|
style: TextStyle(
|
|
// --- BERUBAH: Warna Teks Online/Offline ---
|
|
color: isConnected ? AppColors.success : AppColors.error,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |