diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 247de97..8e39290 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,7 +3,7 @@
+#include
+#include
#include
#include
+#include
+// ================= SUPABASE =================
+const char* supabase_url = "https://hpmicdhjyboyeofphgae.supabase.co";
+const char* supabase_key = "sb_publishable_OB8e9y1OO0z3Y5xQ828YvA_T3jD16zT";
// ================= WIFI =================
-const char* ssid = "Shanum_4G";
-const char* password = "12345678";
+const char* ssid = "Ozie 1";
+const char* password = "followkami";
+
+// ================= TIME (NTP) =================
+const char* ntpServer = "pool.ntp.org";
+const long gmtOffset_sec = 7 * 3600; // WIB (GMT+7)
+const int daylightOffset_sec = 0;
// ================= MQTT =================
const char* mqtt_server = "broker.hivemq.com";
@@ -36,6 +47,59 @@ bool isAutoMode = true;
bool manualPump = false;
bool manualLight = false;
+// State Tracking for Upload
+bool prevPump = false;
+bool prevLight = false;
+unsigned long lastPeriodicUpload = 0;
+
+// ================= SUPABASE UPLOAD =================
+void uploadToSupabase(float hum, String status, String lamp, String pump) {
+ if (WiFi.status() == WL_CONNECTED) {
+ // Ambil waktu saat ini
+ struct tm timeinfo;
+ String timeStr = "";
+ if (getLocalTime(&timeinfo)) {
+ char timeStringBuff[50];
+ strftime(timeStringBuff, sizeof(timeStringBuff), "%H:%M:%S", &timeinfo);
+ timeStr = String(timeStringBuff);
+ }
+
+ WiFiClientSecure clientSecure;
+ clientSecure.setInsecure();
+ HTTPClient http;
+
+ String url = String(supabase_url) + "/rest/v1/humidity";
+ http.begin(clientSecure, url);
+ http.addHeader("Content-Type", "application/json");
+ http.addHeader("apikey", supabase_key);
+ http.addHeader("Authorization", "Bearer " + String(supabase_key));
+ http.addHeader("Prefer", "return=minimal");
+
+ String json = "{\"kelembapan\":" + String(hum) +
+ (timeStr != "" ? ",\"tanggal_upload\":\"" + timeStr + "\"" : "") +
+ ",\"status\":\"" + status + "\"" +
+ ",\"status_lampu\":\"" + lamp + "\"" +
+ ",\"status_pompa\":\"" + pump + "\"}";
+
+ Serial.println("[Supabase] Uploading data...");
+ int httpCode = http.POST(json);
+
+ if (httpCode >= 200 && httpCode < 300) {
+ Serial.printf("[Supabase] Berhasil Terkirim! Status: %d\n", httpCode);
+ } else if (httpCode > 0) {
+ String response = http.getString();
+ Serial.printf("[Supabase] Gagal Mengirim. HTTP Code: %d\n", httpCode);
+ Serial.printf("[Supabase] Alasan: %s\n", response.c_str());
+ } else {
+ Serial.printf("[Supabase] Gagal Koneksi. Error: %s\n", http.errorToString(httpCode).c_str());
+ }
+
+ http.end();
+ } else {
+ Serial.println("[Supabase] Gagal: WiFi tidak terhubung!");
+ }
+}
+
// ================= WIFI =================
void setup_wifi() {
delay(10);
@@ -63,15 +127,15 @@ void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("] ");
Serial.println(message);
- if (String(topic) == "esp32/control/mode") {
+ if (String(topic) == "esp32/statusiqbal/mode") {
isAutoMode = (message == "auto");
Serial.println(isAutoMode ? "Mode: AUTO" : "Mode: MANUAL");
}
- else if (String(topic) == "esp32/control/pump") {
+ else if (String(topic) == "esp32/statusiqbal/pump") {
manualPump = (message == "on");
Serial.println(manualPump ? "Manual Pump: ON" : "Manual Pump: OFF");
}
- else if (String(topic) == "esp32/control/light") {
+ else if (String(topic) == "esp32/statusiqbal/light") {
manualLight = (message == "on");
Serial.println(manualLight ? "Manual Light: ON" : "Manual Light: OFF");
}
@@ -82,19 +146,22 @@ void reconnect() {
while (!client.connected()) {
Serial.print("Connecting MQTT...");
- if (client.connect("ESP32Client", "esp32/status", 0, true, "offline")) {
- Serial.println("connected");
- client.publish("esp32/status", "online", true);
+ // Create a unique client ID using MAC address
+ String clientId = "ESP32MJ-" + WiFi.macAddress();
+
+ if (client.connect(clientId.c_str(), "esp32/statusiqbal", 0, true, "offline")) {
+ Serial.println("CONNECTED to MQTT ✅");
+ client.publish("esp32/statusiqbal", "online", true);
// Subscribe to control topics
- client.subscribe("esp32/control/mode");
- client.subscribe("esp32/control/pump");
- client.subscribe("esp32/control/light");
+ client.subscribe("esp32/statusiqbal/mode");
+ client.subscribe("esp32/statusiqbal/pump");
+ client.subscribe("esp32/statusiqbal/light");
} else {
- Serial.print("failed, rc=");
+ Serial.print("FAILED, rc=");
Serial.print(client.state());
- Serial.println(" coba lagi...");
- delay(2000);
+ Serial.println(" trying again in 5s...");
+ delay(5000);
}
}
}
@@ -111,6 +178,10 @@ void setup() {
dht.begin();
setup_wifi();
+
+ // Konfigurasi waktu dari NTP
+ configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
+
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
@@ -126,6 +197,7 @@ void loop() {
if (now - lastMsg > 2000) {
lastMsg = now;
+
float suhu = dht.readTemperature();
float kelembapan = dht.readHumidity();
@@ -134,19 +206,25 @@ void loop() {
return;
}
+
// ================= CONTROL LOGIC =================
if (isAutoMode) {
- // Automatic Mode (Sensor Based)
- bool kondisiLembab = (kelembapan > batasLembab);
- bool kondisiPanas = (suhu > batasPanas);
-
- digitalWrite(RELAY1, kondisiLembab ? RELAY_ON : RELAY_OFF);
- digitalWrite(RELAY2, kondisiPanas ? RELAY_ON : RELAY_OFF);
-
- if (kondisiPanas && kondisiLembab) statusRelay = "PANAS+LEMBAB";
- else if (kondisiPanas) statusRelay = "PANAS";
- else if (kondisiLembab) statusRelay = "LEMBAB";
- else statusRelay = "NORMAL";
+ // Logic: <70% Pump ON, Lamp OFF | >90% Lamp ON, Pump OFF | 70-90% NORMAL (OFF/OFF)
+ if (kelembapan < 70.0) {
+ digitalWrite(RELAY1, RELAY_ON); // Pompa Hidup
+ digitalWrite(RELAY2, RELAY_OFF); // Lampu Mati
+ statusRelay = "KERING / PENYIRAMAN";
+ }
+ else if (kelembapan > 90.0) {
+ digitalWrite(RELAY1, RELAY_OFF); // Pompa Mati
+ digitalWrite(RELAY2, RELAY_ON); // Lampu Nyala
+ statusRelay = "TERLALU LEMBAB";
+ }
+ else {
+ digitalWrite(RELAY1, RELAY_OFF); // Pompa Mati
+ digitalWrite(RELAY2, RELAY_OFF); // Lampu Mati
+ statusRelay = "NORMAL";
+ }
} else {
// Manual Mode (App Based)
digitalWrite(RELAY1, manualPump ? RELAY_ON : RELAY_OFF);
@@ -158,17 +236,42 @@ void loop() {
bool currentPump = (digitalRead(RELAY1) == RELAY_ON);
bool currentLight = (digitalRead(RELAY2) == RELAY_ON);
- client.publish("esp32/dht/suhu", String(suhu).c_str());
+ // Upload logic: Action transition OR Periodic (30 mins) if Normal
+ unsigned long nowMillis = millis();
+ bool actionDetected = (currentPump != prevPump || currentLight != prevLight);
+
+ // Interval 30 menit = 1800000 ms
+ bool isPeriodicTime = (nowMillis - lastPeriodicUpload >= 1800000);
+
+ if (actionDetected) {
+ Serial.println("[Reporting] Deteksi Perubahan Status!");
+ }
+
+ if (actionDetected || (statusRelay == "NORMAL" && isPeriodicTime)) {
+ String statusLampuUpload = currentLight ? (isAutoMode ? "MENYALA - OTOMATIS" : "MENYALA - MANUAL") : "MATI";
+ String statusPompaUpload = currentPump ? (isAutoMode ? "MENYALA - OTOMATIS" : "MENYALA - MANUAL") : "MATI";
+
+ Serial.println("[Reporting] Memulai Upload ke Supabase...");
+ uploadToSupabase(kelembapan, statusRelay, statusLampuUpload, statusPompaUpload);
+ lastPeriodicUpload = nowMillis;
+ }
+
+ // MQTT Reporting
client.publish("esp32/dht/kelembapan", String(kelembapan).c_str());
client.publish("esp32/relay/status", statusRelay.c_str());
- client.publish("esp32/status/pump", currentPump ? "on" : "off", true);
- client.publish("esp32/status/light", currentLight ? "on" : "off", true);
- client.publish("esp32/status/mode", isAutoMode ? "auto" : "manual", true);
+ client.publish("esp32/statusiqbal/pump", currentPump ? "on" : "off", true);
+ client.publish("esp32/statusiqbal/light", currentLight ? "on" : "off", true);
+ client.publish("esp32/statusiqbal/mode", isAutoMode ? "auto" : "manual", true);
+
+ // Save states for next comparison
+ prevPump = currentPump;
+ prevLight = currentLight;
// Serial Debug
Serial.print("Mode: "); Serial.print(isAutoMode ? "AUTO" : "MANUAL");
Serial.print(" | Hum: "); Serial.print(kelembapan);
Serial.print("% | Pump: "); Serial.print(currentPump ? "ON" : "OFF");
- Serial.print(" | Light: "); Serial.println(currentLight ? "ON" : "OFF");
+ Serial.print(" | Light: "); Serial.print(currentLight ? "ON" : "OFF");
+ Serial.print(" | Status: "); Serial.println(statusRelay);
}
}
\ No newline at end of file
diff --git a/iotb.cpp b/iotb.cpp
new file mode 100644
index 0000000..b7cc7bf
--- /dev/null
+++ b/iotb.cpp
@@ -0,0 +1,240 @@
+#include
+#include
+#include
+#include
+#include
+
+// ================= SUPABASE =================
+const char* supabase_url = "https://hpmicdhjyboyeofphgae.supabase.co";
+const char* supabase_key = "sb_publishable_OB8e9y1OO0z3Y5xQ828YvA_T3jD16zT";
+
+// ================= WIFI =================
+const char* ssid = "Shanum_4G";
+const char* password = "12345678";
+
+// ================= MQTT =================
+const char* mqtt_server = "broker.emqx.io";
+
+WiFiClient espClient;
+PubSubClient client(espClient);
+
+// ================= DHT =================
+#define DHTPIN 4
+#define DHTTYPE DHT22
+DHT dht(DHTPIN, DHTTYPE);
+
+// ================= RELAY =================
+#define RELAY1 27
+#define RELAY2 26
+
+// Relay aktif LOW
+#define RELAY_ON LOW
+#define RELAY_OFF HIGH
+
+// ================= THRESHOLD =================
+float batasPanas = 70.0;
+float batasLembab = 90.0;
+
+// ================= GLOBAL STATE =================
+unsigned long lastMsg = 0;
+String statusRelay = "INIT";
+bool isAutoMode = true;
+bool manualPump = false;
+bool manualLight = false;
+
+// State Tracking for Upload
+bool prevPump = false;
+bool prevLight = false;
+unsigned long lastPeriodicUpload = 0;
+
+// ================= SUPABASE UPLOAD =================
+void uploadToSupabase(float hum, String status, String lamp, String pump) {
+ if (WiFi.status() == WL_CONNECTED) {
+ WiFiClientSecure clientSecure;
+ clientSecure.setInsecure();
+ HTTPClient http;
+
+ String url = String(supabase_url) + "/rest/v1/humidity";
+ http.begin(clientSecure, url);
+ http.addHeader("Content-Type", "application/json");
+ http.addHeader("apikey", supabase_key);
+ http.addHeader("Authorization", "Bearer " + String(supabase_key));
+ http.addHeader("Prefer", "return=minimal");
+
+ String json = "{\"kelembapan\":" + String(hum) +
+ ",\"status\":\"" + status + "\"" +
+ ",\"status_lampu\":\"" + lamp + "\"" +
+ ",\"status_pompa\":\"" + pump + "\"}";
+
+ Serial.println("[Supabase] Uploading data...");
+ int httpCode = http.POST(json);
+ if (httpCode > 0) {
+ Serial.printf("[Supabase] POST success, code: %d\n", httpCode);
+ } else {
+ Serial.printf("[Supabase] POST failed, error: %s\n", http.errorToString(httpCode).c_str());
+ }
+ http.end();
+ }
+}
+
+// ================= WIFI =================
+void setup_wifi() {
+ delay(10);
+ Serial.println("Connecting to WiFi...");
+
+ WiFi.begin(ssid, password);
+
+ while (WiFi.status() != WL_CONNECTED) {
+ delay(500);
+ Serial.print(".");
+ }
+
+ Serial.println("\nWiFi connected!");
+}
+
+// ================= MQTT CALLBACK =================
+void callback(char* topic, byte* payload, unsigned int length) {
+ String message = "";
+ for (int i = 0; i < length; i++) {
+ message += (char)payload[i];
+ }
+
+ Serial.print("Message arrived [");
+ Serial.print(topic);
+ Serial.print("] ");
+ Serial.println(message);
+
+ if (String(topic) == "esp32/statusiqbal/mode") {
+ isAutoMode = (message == "auto");
+ Serial.println(isAutoMode ? "Mode: AUTO" : "Mode: MANUAL");
+ }
+ else if (String(topic) == "esp32/statusiqbal/pump") {
+ manualPump = (message == "on");
+ Serial.println(manualPump ? "Manual Pump: ON" : "Manual Pump: OFF");
+ }
+ else if (String(topic) == "esp32/statusiqbal/light") {
+ manualLight = (message == "on");
+ Serial.println(manualLight ? "Manual Light: ON" : "Manual Light: OFF");
+ }
+}
+
+// ================= MQTT RECONNECT =================
+void reconnect() {
+ while (!client.connected()) {
+ Serial.print("Connecting MQTT...");
+
+ if (client.connect("ESP32Client", "esp32/statusiqbal", 0, true, "offline")) {
+ Serial.println("connected");
+ client.publish("esp32/statusiqbal", "online", true);
+
+ // Subscribe to control topics
+ client.subscribe("esp32/statusiqbal/mode");
+ client.subscribe("esp32/statusiqbal/pump");
+ client.subscribe("esp32/statusiqbal/light");
+ } else {
+ Serial.print("failed, rc=");
+ Serial.print(client.state());
+ Serial.println(" coba lagi...");
+ delay(2000);
+ }
+ }
+}
+
+void setup() {
+ Serial.begin(115200);
+
+ pinMode(RELAY1, OUTPUT);
+ pinMode(RELAY2, OUTPUT);
+
+ digitalWrite(RELAY1, RELAY_OFF);
+ digitalWrite(RELAY2, RELAY_OFF);
+
+ dht.begin();
+
+ setup_wifi();
+ client.setServer(mqtt_server, 1883);
+ client.setCallback(callback);
+}
+
+void loop() {
+ if (!client.connected()) {
+ reconnect();
+ }
+ client.loop();
+
+ unsigned long now = millis();
+
+ if (now - lastMsg > 2000) {
+ lastMsg = now;
+
+ float suhu = dht.readTemperature();
+ float kelembapan = dht.readHumidity();
+
+ if (isnan(suhu) || isnan(kelembapan)) {
+ Serial.println("Gagal baca DHT!");
+ return;
+ }
+
+ // ================= CONTROL LOGIC =================
+ if (isAutoMode) {
+ // Logic: <70% Pump ON, Lamp OFF | >90% Lamp ON, Pump OFF | 70-90% NORMAL (OFF/OFF)
+ if (kelembapan < 70.0) {
+ digitalWrite(RELAY1, RELAY_ON); // Pompa Hidup
+ digitalWrite(RELAY2, RELAY_OFF); // Lampu Mati
+ statusRelay = "KERING / PENYIRAMAN";
+ }
+ else if (kelembapan > 90.0) {
+ digitalWrite(RELAY1, RELAY_OFF); // Pompa Mati
+ digitalWrite(RELAY2, RELAY_ON); // Lampu Nyala
+ statusRelay = "TERLALU LEMBAB";
+ }
+ else {
+ digitalWrite(RELAY1, RELAY_OFF); // Pompa Mati
+ digitalWrite(RELAY2, RELAY_OFF); // Lampu Mati
+ statusRelay = "NORMAL";
+ }
+ } else {
+ // Manual Mode (App Based)
+ digitalWrite(RELAY1, manualPump ? RELAY_ON : RELAY_OFF);
+ digitalWrite(RELAY2, manualLight ? RELAY_ON : RELAY_OFF);
+ statusRelay = "MANUAL";
+ }
+
+ // ================= STATUS REPORTING =================
+ bool currentPump = (digitalRead(RELAY1) == RELAY_ON);
+ bool currentLight = (digitalRead(RELAY2) == RELAY_ON);
+
+ // Upload logic: Action transition OR Periodic (30 mins) if Normal
+ unsigned long nowMillis = millis();
+ bool actionDetected = (currentPump != prevPump || currentLight != prevLight);
+ // Interval 30 menit = 1800000 ms
+ bool isPeriodicTime = (nowMillis - lastPeriodicUpload >= 1800000);
+
+ if (actionDetected || (statusRelay == "NORMAL" && isPeriodicTime)) {
+ String statusLampuUpload = currentLight ? (isAutoMode ? "MENYALA - OTOMATIS" : "MENYALA - MANUAL") : "MATI";
+ String statusPompaUpload = currentPump ? (isAutoMode ? "MENYALA - OTOMATIS" : "MENYALA - MANUAL") : "MATI";
+
+ uploadToSupabase(kelembapan, statusRelay, statusLampuUpload, statusPompaUpload);
+ lastPeriodicUpload = nowMillis;
+ }
+
+ // MQTT Reporting
+ client.publish("esp32/dht/suhu", String(suhu).c_str());
+ client.publish("esp32/dht/kelembapan", String(kelembapan).c_str());
+ client.publish("esp32/relay/status", statusRelay.c_str());
+ client.publish("esp32/statusiqbal/pump", currentPump ? "on" : "off", true);
+ client.publish("esp32/statusiqbal/light", currentLight ? "on" : "off", true);
+ client.publish("esp32/statusiqbal/mode", isAutoMode ? "auto" : "manual", true);
+
+ // Save states
+ prevPump = currentPump;
+ prevLight = currentLight;
+
+ // Serial Debug
+ Serial.print("Mode: "); Serial.print(isAutoMode ? "AUTO" : "MANUAL");
+ Serial.print(" | Hum: "); Serial.print(kelembapan);
+ Serial.print("% | Pump: "); Serial.print(currentPump ? "ON" : "OFF");
+ Serial.print(" | Light: "); Serial.println(currentLight ? "ON" : "OFF");
+ Serial.print(" | Status: "); Serial.println(statusRelay);
+ }
+}
\ No newline at end of file
diff --git a/lib/core/services/mqtt_service.dart b/lib/core/services/mqtt_service.dart
index d39a30d..c4047d7 100644
--- a/lib/core/services/mqtt_service.dart
+++ b/lib/core/services/mqtt_service.dart
@@ -34,7 +34,7 @@ class MqttService {
final clientId = 'mj_debug_${Random().nextInt(999999)}';
// FINAL ROBUST CONFIG: Back to EMQX Port 1883
- client = MqttServerClient('broker.emqx.io', clientId);
+ client = MqttServerClient('broker.hivemq.com', clientId);
client!.port = 1883;
client!.setProtocolV311();
client!.keepAlivePeriod = 60;
@@ -101,10 +101,10 @@ class MqttService {
const topics = [
"esp32/dht/kelembapan",
"esp32/relay/status",
- "esp32/status",
- "esp32/status/mode",
- "esp32/status/pump",
- "esp32/status/light",
+ "esp32/statusiqbal",
+ "esp32/statusiqbal/mode",
+ "esp32/statusiqbal/pump",
+ "esp32/statusiqbal/light",
];
for (final t in topics) {
@@ -127,20 +127,20 @@ class MqttService {
relayStatus.value = payload.toUpperCase();
break;
- case "esp32/status":
+ case "esp32/statusiqbal":
isHardwareOnline.value = (pt == "online");
debugPrint('MQTT: Hardware -> ${isHardwareOnline.value}');
break;
- case "esp32/status/mode":
+ case "esp32/statusiqbal/mode":
isAutoMode.value = (pt == "auto");
break;
- case "esp32/status/pump":
+ case "esp32/statusiqbal/pump":
isPumpOn.value = (pt == "on");
break;
- case "esp32/status/light":
+ case "esp32/statusiqbal/light":
isLightOn.value = (pt == "on");
break;
}
@@ -154,7 +154,7 @@ class MqttService {
}
final builder = MqttClientPayloadBuilder()..addString(value);
- final topic = "esp32/status/$subTopic";
+ final topic = "esp32/statusiqbal/$subTopic";
client!.publishMessage(
topic,
diff --git a/lib/features/account/presentation/pages/account_page.dart b/lib/features/account/presentation/pages/account_page.dart
index e0dff02..ac8fac5 100644
--- a/lib/features/account/presentation/pages/account_page.dart
+++ b/lib/features/account/presentation/pages/account_page.dart
@@ -2,10 +2,16 @@ import 'package:flutter/material.dart';
import 'package:monitoring_jamur/core/theme/app_theme.dart';
import 'package:monitoring_jamur/core/session/user_session.dart';
import 'package:monitoring_jamur/features/auth/presentation/pages/login_page.dart';
+import 'package:monitoring_jamur/features/account/presentation/pages/manage_account_page.dart';
-class AccountPage extends StatelessWidget {
+class AccountPage extends StatefulWidget {
const AccountPage({super.key});
+ @override
+ State createState() => _AccountPageState();
+}
+
+class _AccountPageState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -68,10 +74,12 @@ class AccountPage extends StatelessWidget {
_buildMenuTile(
icon: Icons.settings_rounded,
title: 'Kelola Akun',
- onTap: () {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Fitur Kelola Akun segera hadir')),
+ onTap: () async {
+ await Navigator.push(
+ context,
+ MaterialPageRoute(builder: (context) => const ManageAccountPage()),
);
+ if (mounted) setState(() {});
},
),
const SizedBox(height: 12),
@@ -81,7 +89,7 @@ class AccountPage extends StatelessWidget {
isDestructive: true,
onTap: () async {
await UserSession.logout();
- if (context.mounted) {
+ if (mounted) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
@@ -180,3 +188,4 @@ class AccountPage extends StatelessWidget {
);
}
}
+
diff --git a/lib/features/account/presentation/pages/manage_account_page.dart b/lib/features/account/presentation/pages/manage_account_page.dart
new file mode 100644
index 0000000..9bb03d4
--- /dev/null
+++ b/lib/features/account/presentation/pages/manage_account_page.dart
@@ -0,0 +1,326 @@
+import 'package:flutter/material.dart';
+import 'package:monitoring_jamur/core/theme/app_theme.dart';
+import 'package:monitoring_jamur/core/session/user_session.dart';
+import 'package:monitoring_jamur/features/auth/data/user_repository.dart';
+import 'package:monitoring_jamur/features/auth/presentation/pages/login_page.dart';
+
+class ManageAccountPage extends StatefulWidget {
+ const ManageAccountPage({super.key});
+
+ @override
+ State createState() => _ManageAccountPageState();
+}
+
+class _ManageAccountPageState extends State {
+ final TextEditingController _usernameController = TextEditingController();
+ final TextEditingController _passwordController = TextEditingController();
+ final UserRepository _userRepository = UserRepository();
+ bool _isLoading = false;
+ bool _obscurePassword = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _usernameController.text = UserSession.username ?? '';
+ }
+
+ Future _updateUsername() async {
+ final newUsername = _usernameController.text.trim();
+ if (newUsername.isEmpty) return;
+ if (newUsername == UserSession.username) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Username baru harus berbeda dari yang lama')),
+ );
+ return;
+ }
+
+ setState(() => _isLoading = true);
+ final success = await _userRepository.updateUsername(UserSession.username!, newUsername);
+ setState(() => _isLoading = false);
+
+ if (success) {
+ await UserSession.saveSession(newUsername);
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Username berhasil diperbarui')),
+ );
+ }
+ } else {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Gagal memperbarui username')),
+ );
+ }
+ }
+ }
+
+ Future _updatePassword() async {
+ final newPassword = _passwordController.text.trim();
+ if (newPassword.isEmpty) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Password tidak boleh kosong')),
+ );
+ return;
+ }
+
+ setState(() => _isLoading = true);
+ final success = await _userRepository.updatePassword(UserSession.username!, newPassword);
+ setState(() => _isLoading = false);
+
+ if (success) {
+ _passwordController.clear();
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Password berhasil diperbarui')),
+ );
+ }
+ } else {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Gagal memperbarui password')),
+ );
+ }
+ }
+ }
+
+ Future _deleteAccount() async {
+ final confirm = await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text('Hapus Akun'),
+ content: const Text('Apakah Anda yakin ingin menghapus akun ini? Tindakan ini tidak dapat dibatalkan.'),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context, false),
+ child: const Text('Batal'),
+ ),
+ TextButton(
+ onPressed: () => Navigator.pop(context, true),
+ style: TextButton.styleFrom(foregroundColor: Colors.red),
+ child: const Text('Hapus'),
+ ),
+ ],
+ ),
+ );
+
+ if (confirm != true) return;
+
+ setState(() => _isLoading = true);
+ final success = await _userRepository.deleteUser(UserSession.username!);
+ setState(() => _isLoading = false);
+
+ if (success) {
+ await UserSession.logout();
+ if (mounted) {
+ Navigator.pushAndRemoveUntil(
+ context,
+ MaterialPageRoute(builder: (context) => const LoginPage()),
+ (route) => false,
+ );
+ }
+ } else {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Gagal menghapus akun')),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: AppTheme.backgroundBeige,
+ appBar: AppBar(
+ title: const Text('Kelola Akun'),
+ backgroundColor: Colors.transparent,
+ elevation: 0,
+ foregroundColor: AppTheme.textDark,
+ ),
+ body: Stack(
+ children: [
+ SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ children: [
+ // Username Update Section
+ _buildSection(
+ title: 'Ganti Username',
+ child: Column(
+ children: [
+ _buildTextField(
+ controller: _usernameController,
+ label: 'Username baru',
+ icon: Icons.person_outline,
+ ),
+ const SizedBox(height: 16),
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: _isLoading ? null : _updateUsername,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppTheme.primaryGreen,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ ),
+ child: const Text('Simpan Username'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+ // Password Update Section
+ _buildSection(
+ title: 'Ganti Password',
+ child: Column(
+ children: [
+ _buildTextField(
+ controller: _passwordController,
+ label: 'Password baru',
+ icon: Icons.lock_outline,
+ isPassword: true,
+ ),
+ const SizedBox(height: 16),
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: _isLoading ? null : _updatePassword,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppTheme.primaryGreen,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ ),
+ child: const Text('Simpan Password'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 48),
+ // Delete Account Section
+ _buildSection(
+ title: 'Zona Berbahaya',
+ isDestructive: true,
+ child: Column(
+ children: [
+ const Text(
+ 'Menghapus akun akan menghapus semua data Anda secara permanen.',
+ style: TextStyle(color: AppTheme.textLight, fontSize: 13),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 16),
+ SizedBox(
+ width: double.infinity,
+ child: OutlinedButton(
+ onPressed: _isLoading ? null : _deleteAccount,
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Colors.red,
+ side: const BorderSide(color: Colors.red),
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ ),
+ child: const Text('Hapus Akun Saya'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ if (_isLoading)
+ Container(
+ color: Colors.black26,
+ child: const Center(
+ child: CircularProgressIndicator(color: AppTheme.primaryGreen),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSection({required String title, required Widget child, bool isDestructive = false}) {
+ return Container(
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: AppTheme.surfaceWhite,
+ borderRadius: BorderRadius.circular(24),
+ border: isDestructive ? Border.all(color: Colors.red.withOpacity(0.3)) : null,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.03),
+ blurRadius: 10,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: isDestructive ? Colors.red : AppTheme.textDark,
+ ),
+ ),
+ const SizedBox(height: 16),
+ child,
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTextField({
+ required TextEditingController controller,
+ required String label,
+ required IconData icon,
+ bool isPassword = false,
+ }) {
+ return TextField(
+ controller: controller,
+ obscureText: isPassword ? _obscurePassword : false,
+ decoration: InputDecoration(
+ labelText: label,
+ prefixIcon: Icon(icon, color: AppTheme.primaryGreen),
+ suffixIcon: isPassword
+ ? IconButton(
+ icon: Icon(
+ _obscurePassword ? Icons.visibility_off : Icons.visibility,
+ color: AppTheme.primaryGreen.withOpacity(0.7),
+ ),
+ onPressed: () {
+ setState(() {
+ _obscurePassword = !_obscurePassword;
+ });
+ },
+ )
+ : null,
+ filled: true,
+ fillColor: AppTheme.backgroundBeige.withOpacity(0.5),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(16),
+ borderSide: BorderSide.none,
+ ),
+ ),
+ );
+ }
+
+ @override
+ void dispose() {
+ _usernameController.dispose();
+ _passwordController.dispose();
+ super.dispose();
+ }
+}
diff --git a/lib/features/auth/data/user_repository.dart b/lib/features/auth/data/user_repository.dart
index 42f5877..7df9df6 100644
--- a/lib/features/auth/data/user_repository.dart
+++ b/lib/features/auth/data/user_repository.dart
@@ -35,4 +35,48 @@ class UserRepository {
return null;
}
}
+
+ /// Update username in the custom `users` table.
+ Future updateUsername(String oldUsername, String newUsername) async {
+ try {
+ final response = await _supabase
+ .from('users')
+ .update({'username': newUsername})
+ .eq('username', oldUsername)
+ .select();
+
+ return response.isNotEmpty;
+ } catch (e) {
+ print('Update username error: $e');
+ return false;
+ }
+ }
+
+ /// Update password in the custom `users` table.
+ Future updatePassword(String username, String newPassword) async {
+ try {
+ final response = await _supabase
+ .from('users')
+ .update({'password': newPassword})
+ .eq('username', username)
+ .select();
+
+ return response.isNotEmpty;
+ } catch (e) {
+ print('Update password error: $e');
+ return false;
+ }
+ }
+
+ /// Delete a user from the custom `users` table.
+ Future deleteUser(String username) async {
+ try {
+ await _supabase.from('users').delete().eq('username', username);
+ return true;
+ } catch (e) {
+ print('Delete user error: $e');
+ return false;
+ }
+ }
}
+
diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart
index d01c8c2..cb17e88 100644
--- a/lib/features/auth/presentation/pages/login_page.dart
+++ b/lib/features/auth/presentation/pages/login_page.dart
@@ -17,6 +17,7 @@ class _LoginPageState extends State {
final _passwordController = TextEditingController();
final _userRepo = UserRepository();
bool _isLoading = false;
+ bool _obscurePassword = true;
void _login() async {
if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) {
@@ -87,10 +88,21 @@ class _LoginPageState extends State {
const SizedBox(height: 16),
TextField(
controller: _passwordController,
- obscureText: true,
- decoration: const InputDecoration(
+ obscureText: _obscurePassword,
+ decoration: InputDecoration(
hintText: 'Password',
- prefixIcon: Icon(Icons.lock_outline),
+ prefixIcon: const Icon(Icons.lock_outline),
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscurePassword ? Icons.visibility_off : Icons.visibility,
+ color: AppTheme.primaryGreen.withOpacity(0.7),
+ ),
+ onPressed: () {
+ setState(() {
+ _obscurePassword = !_obscurePassword;
+ });
+ },
+ ),
),
),
const SizedBox(height: 24),
diff --git a/lib/features/auth/presentation/pages/register_page.dart b/lib/features/auth/presentation/pages/register_page.dart
index 58de9bd..4d328aa 100644
--- a/lib/features/auth/presentation/pages/register_page.dart
+++ b/lib/features/auth/presentation/pages/register_page.dart
@@ -14,6 +14,7 @@ class _RegisterPageState extends State {
final _passwordController = TextEditingController();
final _userRepo = UserRepository();
bool _isLoading = false;
+ bool _obscurePassword = true;
void _register() async {
if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) {
@@ -87,10 +88,21 @@ class _RegisterPageState extends State {
const SizedBox(height: 16),
TextField(
controller: _passwordController,
- obscureText: true,
- decoration: const InputDecoration(
+ obscureText: _obscurePassword,
+ decoration: InputDecoration(
hintText: 'Create Password',
- prefixIcon: Icon(Icons.lock_outline),
+ prefixIcon: const Icon(Icons.lock_outline),
+ suffixIcon: IconButton(
+ icon: Icon(
+ _obscurePassword ? Icons.visibility_off : Icons.visibility,
+ color: AppTheme.primaryGreen.withOpacity(0.7),
+ ),
+ onPressed: () {
+ setState(() {
+ _obscurePassword = !_obscurePassword;
+ });
+ },
+ ),
),
),
const SizedBox(height: 40),
diff --git a/lib/features/history/data/history_repository.dart b/lib/features/history/data/history_repository.dart
index e75e5b6..3f22563 100644
--- a/lib/features/history/data/history_repository.dart
+++ b/lib/features/history/data/history_repository.dart
@@ -43,7 +43,7 @@ class HistoryRepository {
'status': status,
'status_lampu': lightStatus,
'status_pompa': pumpStatus,
- 'tanggal_upload': DateFormat('HH:mm:ss').format(DateTime.now()),
+ 'tanggal_upload': DateTime.now().toIso8601String(),
});
return true;
} catch (e) {
diff --git a/lib/features/home/presentation/pages/dashboard_page.dart b/lib/features/home/presentation/pages/dashboard_page.dart
index 2f8d64b..0527f28 100644
--- a/lib/features/home/presentation/pages/dashboard_page.dart
+++ b/lib/features/home/presentation/pages/dashboard_page.dart
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:monitoring_jamur/core/theme/app_theme.dart';
-import 'package:monitoring_jamur/features/home/presentation/pages/statistics_page.dart';
import 'package:monitoring_jamur/core/services/mqtt_service.dart';
import 'package:monitoring_jamur/features/history/data/history_repository.dart';
import 'dart:math' as math;
@@ -137,9 +136,6 @@ class _DashboardPageState extends State {
const SizedBox(height: 24),
// Save to History Button
_buildSaveToHistoryButton(),
- const SizedBox(height: 16),
- // Statistics Button
- _buildStatisticsButton(context),
const SizedBox(height: 32),
],
),
@@ -233,7 +229,7 @@ class _DashboardPageState extends State {
_mqttService.publishControl(
'pump', val ? 'on' : 'off');
if (val) {
- _saveManualAction('Pompa Air', true);
+ _showActionFeedback('Pompa Air');
}
},
);
@@ -252,7 +248,7 @@ class _DashboardPageState extends State {
_mqttService.publishControl(
'light', val ? 'on' : 'off');
if (val) {
- _saveManualAction('Lampu Pemanas', true);
+ _showActionFeedback('Lampu Pemanas');
}
},
);
@@ -326,45 +322,6 @@ class _DashboardPageState extends State {
);
}
- Widget _buildStatisticsButton(BuildContext context) {
- return GestureDetector(
- onTap: () {
- Navigator.push(
- context,
- MaterialPageRoute(builder: (context) => const StatisticsPage()),
- );
- },
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 20),
- decoration: BoxDecoration(
- color: AppTheme.surfaceWhite,
- borderRadius: BorderRadius.circular(24),
- boxShadow: [
- BoxShadow(
- color: AppTheme.primaryGreen.withAlpha(20),
- blurRadius: 15,
- offset: const Offset(0, 8),
- ),
- ],
- ),
- child: const Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.bar_chart_rounded, color: AppTheme.primaryGreen, size: 28),
- SizedBox(width: 12),
- Text(
- 'Lihat Statistik',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- color: AppTheme.primaryGreen,
- ),
- ),
- ],
- ),
- ),
- );
- }
String _getHumidityStatus(double humidity) {
if (humidity < 70) return 'Sangat Kering';
@@ -412,31 +369,11 @@ class _DashboardPageState extends State {
}
}
- Future _saveManualAction(String device, bool isOn) async {
- final status = _getHumidityStatus(_humidity);
-
- // Prepare current statuses
- String pumpStatusText = _mqttService.isPumpOn.value ? 'MENYALA - MANUAL' : 'MATI';
- String lightStatusText = _mqttService.isLightOn.value ? 'MENYALA - MANUAL' : 'MATI';
-
- // Override with the new action state
- if (device.contains('Pompa')) {
- pumpStatusText = 'MENYALA - MANUAL';
- } else {
- lightStatusText = 'MENYALA - MANUAL';
- }
-
- await _historyRepository.saveHumidityData(
- humidity: _humidity,
- status: status,
- lightStatus: lightStatusText,
- pumpStatus: pumpStatusText,
- );
-
+ void _showActionFeedback(String device) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: Text('$device dinyalakan (Data tersimpan!)'),
+ content: Text('$device dinyalakan (Perintah terkirim ke alat!)'),
backgroundColor: AppTheme.primaryGreen,
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
@@ -754,19 +691,14 @@ class _InstructionPanel extends StatelessWidget {
subMessage = 'Tidak optimal untuk pertumbuhan aktif. Pompa menyala (penyiraman aktif)';
icon = Icons.warning_amber_rounded;
color = Colors.orange.shade800;
- } else if (humidity >= 70 && humidity < 80) {
- message = 'Kering - Lembab';
- subMessage = 'Mulai mendekati kondisi ideal. Pompa menyala (penyiraman terbatas)';
- icon = Icons.info_outline_rounded;
- color = Colors.blueGrey;
- } else if (humidity >= 80 && humidity <= 90) {
+ }else if (humidity >= 70 && humidity <= 90) {
message = 'Lembab (Ideal)';
- subMessage = 'Kondisi optimal pertumbuhan jamur tiram. Pompa mati';
+ subMessage = 'Kondisi optimal pertumbuhan jamur tiram.';
icon = Icons.check_circle_outline_rounded;
color = AppTheme.primaryGreen;
} else {
message = 'Sangat Lembab';
- subMessage = 'Menyebabkan kontaminasi. Pompa mati';
+ subMessage = 'Menyebabkan kontaminasi.Lampu Menyala';
icon = Icons.error_outline_rounded;
color = Colors.red.shade700;
}
diff --git a/lib/features/home/presentation/pages/statistics_page.dart b/lib/features/home/presentation/pages/statistics_page.dart
deleted file mode 100644
index 067c808..0000000
--- a/lib/features/home/presentation/pages/statistics_page.dart
+++ /dev/null
@@ -1,143 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:monitoring_jamur/core/theme/app_theme.dart';
-
-class StatisticsPage extends StatelessWidget {
- const StatisticsPage({super.key});
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- backgroundColor: AppTheme.backgroundBeige,
- appBar: AppBar(
- title: const Text('Statistik Monitoring'),
- backgroundColor: Colors.transparent,
- elevation: 0,
- foregroundColor: AppTheme.textDark,
- centerTitle: true,
- ),
- body: SingleChildScrollView(
- padding: const EdgeInsets.all(24.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _buildSummaryRow(),
- const SizedBox(height: 32),
- _buildChartSection('Tren Kelembapan', [85, 82, 88, 84, 86, 85, 87], AppTheme.primaryGreen),
- const SizedBox(height: 32),
- _buildChartSection('Tren Suhu', [24, 25, 24, 26, 25, 24, 25], Colors.orange),
- const SizedBox(height: 40),
- _buildAnalysisCard(),
- ],
- ),
- ),
- );
- }
-
- Widget _buildSummaryRow() {
- return Row(
- children: [
- _buildStatCard('Rata-rata', '85%', Icons.water_drop_rounded, AppTheme.primaryGreen),
- const SizedBox(width: 16),
- _buildStatCard('Tertinggi', '89%', Icons.trending_up_rounded, Colors.blue),
- ],
- );
- }
-
- Widget _buildStatCard(String label, String value, IconData icon, Color color) {
- return Expanded(
- child: Container(
- padding: const EdgeInsets.all(20),
- decoration: BoxDecoration(
- color: AppTheme.surfaceWhite,
- borderRadius: BorderRadius.circular(24),
- boxShadow: [
- BoxShadow(
- color: Colors.black.withAlpha(5),
- blurRadius: 10,
- offset: const Offset(0, 4),
- ),
- ],
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Icon(icon, color: color, size: 28),
- const SizedBox(height: 12),
- Text(value, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
- Text(label, style: const TextStyle(fontSize: 12, color: AppTheme.textLight)),
- ],
- ),
- ),
- );
- }
-
- Widget _buildChartSection(String title, List values, Color color) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- title,
- style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppTheme.textDark),
- ),
- const SizedBox(height: 16),
- Container(
- height: 180,
- padding: const EdgeInsets.all(20),
- decoration: BoxDecoration(
- color: AppTheme.surfaceWhite,
- borderRadius: BorderRadius.circular(24),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: values.map((v) {
- double height = (v / 100) * 120;
- if (title.contains('Suhu')) height = (v / 40) * 120; // Scale for temperature
- return Container(
- width: 24,
- height: height,
- decoration: BoxDecoration(
- color: color.withAlpha(50),
- borderRadius: BorderRadius.circular(8),
- border: Border.all(color: color, width: 2),
- ),
- );
- }).toList(),
- ),
- ),
- ],
- );
- }
-
- Widget _buildAnalysisCard() {
- return Container(
- width: double.infinity,
- padding: const EdgeInsets.all(24),
- decoration: BoxDecoration(
- color: AppTheme.primaryGreen.withAlpha(20),
- borderRadius: BorderRadius.circular(24),
- border: Border.all(color: AppTheme.primaryGreen.withAlpha(50), width: 2),
- ),
- child: const Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Icon(Icons.auto_awesome_rounded, color: AppTheme.primaryGreen),
- SizedBox(width: 12),
- Text(
- 'Analisis Sistem',
- style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppTheme.primaryGreen),
- ),
- ],
- ),
- SizedBox(height: 12),
- Text(
- 'Berdasarkan data 24 jam terakhir, kelembapan stabil di rentang ideal (80-90%). Pertumbuhan jamur terpantau optimal.',
- style: TextStyle(fontSize: 14, color: AppTheme.textDark, height: 1.5),
- ),
- ],
- ),
- );
- }
-}