commit 71c0e8a53887607e564b6853b8705b8e81e24a99 Author: amrimd Date: Tue Aug 12 12:31:32 2025 +0700 first commit diff --git a/ALAT_IOT/Admin_Pendaftaran.ino b/ALAT_IOT/Admin_Pendaftaran.ino new file mode 100644 index 0000000..7eee170 --- /dev/null +++ b/ALAT_IOT/Admin_Pendaftaran.ino @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include + +const char* ssid = "amriiiii.02"; +const char* password = "123sampai10"; + +const char* serverName = "http://192.168.92.176/playground_api/receive_rfid_scan.php"; + +#define SS_PIN 5 +#define RST_PIN 15 + +MFRC522 rfid(SS_PIN, RST_PIN); +LiquidCrystal_I2C lcd(0x27, 16, 2); + +void setup() { + Serial.begin(115200); + + lcd.init(); + lcd.backlight(); + + SPI.begin(); + rfid.PCD_Init(); + + lcd.setCursor(0, 0); + lcd.print("Menghubungkan..."); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + lcd.setCursor(0, 1); + lcd.print("WiFi Connecting.."); + } + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("WiFi Tersambung"); + Serial.println("WiFi Tersambung!"); + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + delay(1000); + lcd.clear(); + + lcd.setCursor(0, 0); + lcd.print("SISTEM PEMANTAU"); + lcd.setCursor(0, 1); + lcd.print("ANAK PLAYGROUND"); + delay(2000); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("TEMPELKAN GELANG"); + lcd.setCursor(0, 1); + lcd.print("UNTUK MENDAFTAR"); + Serial.println("Sistem siap. Tempelkan gelang RFID."); +} + +void loop() { + if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) { + delay(50); + return; + } + + String uidStr = ""; + for (byte i = 0; i < rfid.uid.size; i++) { + if (rfid.uid.uidByte[i] < 0x10) uidStr += "0"; + uidStr += String(rfid.uid.uidByte[i], HEX); + } + uidStr.toUpperCase(); + + Serial.print("UID Gelang Terbaca: "); + Serial.println(uidStr); + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("GELANG TERBACA"); + lcd.setCursor(0, 1); + lcd.print(uidStr); + delay(1000); + + if (WiFi.status() == WL_CONNECTED) { + HTTPClient http; + http.begin(serverName); + http.addHeader("Content-Type", "application/x-www-form-urlencoded"); + + // Hanya kirim kode_gelang + String httpRequestData = "kode_gelang=" + uidStr; + + Serial.print("Mengirim data: "); + Serial.println(httpRequestData); + + int httpResponseCode = http.POST(httpRequestData); + + lcd.clear(); + lcd.setCursor(0, 0); + + if (httpResponseCode > 0) { + String response = http.getString(); + Serial.print("HTTP Response Code: "); + Serial.println(httpResponseCode); + Serial.print("Server Response: "); + Serial.println(response); + + lcd.print("Data Dikirim OK"); + lcd.setCursor(0, 1); + lcd.print("Status: "); + lcd.print(httpResponseCode); + } else { + Serial.print("Error Mengirim Data. HTTP Response Code: "); + Serial.println(httpResponseCode); + + lcd.print("Gagal Kirim"); + lcd.setCursor(0, 1); + lcd.print("Kode: "); + lcd.print(httpResponseCode); + } + + http.end(); + } else { + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("WiFi Terputus!"); + lcd.setCursor(0, 1); + lcd.print("Reconnecting..."); + Serial.println("WiFi Terputus, mencoba menyambungkan ulang..."); + WiFi.reconnect(); + delay(2000); + } + + delay(2000); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("TEMPELKAN GELANG"); + lcd.setCursor(0, 1); + lcd.print("UNTUK MENDAFTAR"); +} \ No newline at end of file diff --git a/ALAT_IOT/Interaktif.ino b/ALAT_IOT/Interaktif.ino new file mode 100644 index 0000000..8da18a3 --- /dev/null +++ b/ALAT_IOT/Interaktif.ino @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include + +const char* ssid = "amriiiii.02"; +const char* password = "123sampai10"; + +const char* serverName = "http://192.168.92.176/playground_api/process_wahana_scan.php"; + +const char* namaWahana = "Interaktif"; + +#define SS_PIN 5 +#define RST_PIN 15 + +MFRC522 rfid(SS_PIN, RST_PIN); +LiquidCrystal_I2C lcd(0x27, 16, 2); + +void setup() { + Serial.begin(115200); + + lcd.init(); + lcd.backlight(); + + SPI.begin(); + rfid.PCD_Init(); + + lcd.setCursor(0, 0); + lcd.print("Menghubungkan..."); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + lcd.setCursor(0, 1); + lcd.print("WiFi Connecting.."); + } + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("WiFi Tersambung"); + Serial.println("WiFi Tersambung!"); + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + delay(1000); + lcd.clear(); + + lcd.setCursor(0, 0); + lcd.print("SISTEM PEMANTAU"); + lcd.setCursor(0, 1); + lcd.print("ANAK PLAYGROUND"); + delay(2000); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("TEMPELKAN GELANG"); + lcd.setCursor(0, 1); + lcd.print(" UNTUK MASUK "); + Serial.println("Sistem siap. Tempelkan gelang RFID."); +} + +void loop() { + if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) { + delay(50); + return; + } + + String uidStr = ""; + for (byte i = 0; i < rfid.uid.size; i++) { + if (rfid.uid.uidByte[i] < 0x10) uidStr += "0"; + uidStr += String(rfid.uid.uidByte[i], HEX); + } + uidStr.toUpperCase(); + + Serial.print("UID Gelang Terbaca: "); + Serial.println(uidStr); + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("GELANG TERBACA"); + lcd.setCursor(0, 1); + lcd.print(uidStr); + delay(1000); + + if (WiFi.status() == WL_CONNECTED) { + HTTPClient http; + http.begin(serverName); + http.addHeader("Content-Type", "application/x-www-form-urlencoded"); + + String httpRequestData = "kode_gelang=" + uidStr + "&wahana=" + String(namaWahana); + + Serial.print("Mengirim data: "); + Serial.println(httpRequestData); + + int httpResponseCode = http.POST(httpRequestData); + + lcd.clear(); + lcd.setCursor(0, 0); + + if (httpResponseCode > 0) { + String response = http.getString(); + Serial.print("HTTP Response Code: "); + Serial.println(httpResponseCode); + Serial.print("Server Response: "); + Serial.println(response); + + lcd.print("Data Dikirim OK"); + lcd.setCursor(0, 1); + lcd.print("Status: "); + lcd.print(httpResponseCode); + } else { + Serial.print("Error Mengirim Data. HTTP Response Code: "); + Serial.println(httpResponseCode); + + lcd.print("Gagal Kirim"); + lcd.setCursor(0, 1); + lcd.print("Kode: "); + lcd.print(httpResponseCode); + } + + http.end(); + } else { + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("WiFi Terputus!"); + lcd.setCursor(0, 1); + lcd.print("Reconnecting..."); + Serial.println("WiFi Terputus, mencoba menyambungkan ulang..."); + WiFi.reconnect(); + delay(2000); + } + + delay(2000); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("TEMPELKAN GELANG"); + lcd.setCursor(0, 1); + lcd.print(" UNTUK MASUK "); +} \ No newline at end of file diff --git a/ALAT_IOT/Simulasi.ino b/ALAT_IOT/Simulasi.ino new file mode 100644 index 0000000..b84bffb --- /dev/null +++ b/ALAT_IOT/Simulasi.ino @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include + +const char* ssid = "amriiiii.02"; +const char* password = "123sampai10"; + +const char* serverName = "http://192.168.92.176/playground_api/process_wahana_scan.php"; + +const char* namaWahana = "Simulasi"; + +#define SS_PIN 5 +#define RST_PIN 15 + +MFRC522 rfid(SS_PIN, RST_PIN); +LiquidCrystal_I2C lcd(0x27, 16, 2); + +void setup() { + Serial.begin(115200); + + lcd.init(); + lcd.backlight(); + + SPI.begin(); + rfid.PCD_Init(); + + lcd.setCursor(0, 0); + lcd.print("Menghubungkan..."); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + lcd.setCursor(0, 1); + lcd.print("WiFi Connecting.."); + } + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("WiFi Tersambung"); + Serial.println("WiFi Tersambung!"); + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + delay(1000); + lcd.clear(); + + lcd.setCursor(0, 0); + lcd.print("SISTEM PEMANTAU"); + lcd.setCursor(0, 1); + lcd.print("ANAK PLAYGROUND"); + delay(2000); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("TEMPELKAN GELANG"); + lcd.setCursor(0, 1); + lcd.print(" UNTUK MASUK "); + Serial.println("Sistem siap. Tempelkan gelang RFID."); +} + +void loop() { + if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) { + delay(50); + return; + } + + String uidStr = ""; + for (byte i = 0; i < rfid.uid.size; i++) { + if (rfid.uid.uidByte[i] < 0x10) uidStr += "0"; + uidStr += String(rfid.uid.uidByte[i], HEX); + } + uidStr.toUpperCase(); + + Serial.print("UID Gelang Terbaca: "); + Serial.println(uidStr); + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("GELANG TERBACA"); + lcd.setCursor(0, 1); + lcd.print(uidStr); + delay(1000); + + if (WiFi.status() == WL_CONNECTED) { + HTTPClient http; + http.begin(serverName); + http.addHeader("Content-Type", "application/x-www-form-urlencoded"); + + String httpRequestData = "kode_gelang=" + uidStr + "&wahana=" + String(namaWahana); + + Serial.print("Mengirim data: "); + Serial.println(httpRequestData); + + int httpResponseCode = http.POST(httpRequestData); + + lcd.clear(); + lcd.setCursor(0, 0); + + if (httpResponseCode > 0) { + String response = http.getString(); + Serial.print("HTTP Response Code: "); + Serial.println(httpResponseCode); + Serial.print("Server Response: "); + Serial.println(response); + + lcd.print("Data Dikirim OK"); + lcd.setCursor(0, 1); + lcd.print("Status: "); + lcd.print(httpResponseCode); + } else { + Serial.print("Error Mengirim Data. HTTP Response Code: "); + Serial.println(httpResponseCode); + + lcd.print("Gagal Kirim"); + lcd.setCursor(0, 1); + lcd.print("Kode: "); + lcd.print(httpResponseCode); + } + + http.end(); + } else { + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("WiFi Terputus!"); + lcd.setCursor(0, 1); + lcd.print("Reconnecting..."); + Serial.println("WiFi Terputus, mencoba menyambungkan ulang..."); + WiFi.reconnect(); + delay(2000); + } + + delay(2000); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("TEMPELKAN GELANG"); + lcd.setCursor(0, 1); + lcd.print(" UNTUK MASUK "); +} \ No newline at end of file diff --git a/ALAT_IOT/Virtual.ino b/ALAT_IOT/Virtual.ino new file mode 100644 index 0000000..1a55280 --- /dev/null +++ b/ALAT_IOT/Virtual.ino @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include + +const char* ssid = "amriiiii.02"; +const char* password = "123sampai10"; + +const char* serverName = "http://192.168.92.176/playground_api/process_wahana_scan.php"; + +const char* namaWahana = "Virtual"; + +#define SS_PIN 5 +#define RST_PIN 15 + +MFRC522 rfid(SS_PIN, RST_PIN); +LiquidCrystal_I2C lcd(0x27, 16, 2); + +void setup() { + Serial.begin(115200); + + lcd.init(); + lcd.backlight(); + + SPI.begin(); + rfid.PCD_Init(); + + lcd.setCursor(0, 0); + lcd.print("Menghubungkan..."); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + lcd.setCursor(0, 1); + lcd.print("WiFi Connecting.."); + } + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("WiFi Tersambung"); + Serial.println("WiFi Tersambung!"); + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + delay(1000); + lcd.clear(); + + lcd.setCursor(0, 0); + lcd.print("SISTEM PEMANTAU"); + lcd.setCursor(0, 1); + lcd.print("ANAK PLAYGROUND"); + delay(2000); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("TEMPELKAN GELANG"); + lcd.setCursor(0, 1); + lcd.print(" UNTUK MASUK "); + Serial.println("Sistem siap. Tempelkan gelang RFID."); +} + +void loop() { + if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) { + delay(50); + return; + } + + String uidStr = ""; + for (byte i = 0; i < rfid.uid.size; i++) { + if (rfid.uid.uidByte[i] < 0x10) uidStr += "0"; + uidStr += String(rfid.uid.uidByte[i], HEX); + } + uidStr.toUpperCase(); + + Serial.print("UID Gelang Terbaca: "); + Serial.println(uidStr); + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("GELANG TERBACA"); + lcd.setCursor(0, 1); + lcd.print(uidStr); + delay(1000); + + if (WiFi.status() == WL_CONNECTED) { + HTTPClient http; + http.begin(serverName); + http.addHeader("Content-Type", "application/x-www-form-urlencoded"); + + String httpRequestData = "kode_gelang=" + uidStr + "&wahana=" + String(namaWahana); + + Serial.print("Mengirim data: "); + Serial.println(httpRequestData); + + int httpResponseCode = http.POST(httpRequestData); + + lcd.clear(); + lcd.setCursor(0, 0); + + if (httpResponseCode > 0) { + String response = http.getString(); + Serial.print("HTTP Response Code: "); + Serial.println(httpResponseCode); + Serial.print("Server Response: "); + Serial.println(response); + + lcd.print("Data Dikirim OK"); + lcd.setCursor(0, 1); + lcd.print("Status: "); + lcd.print(httpResponseCode); + } else { + Serial.print("Error Mengirim Data. HTTP Response Code: "); + Serial.println(httpResponseCode); + + lcd.print("Gagal Kirim"); + lcd.setCursor(0, 1); + lcd.print("Kode: "); + lcd.print(httpResponseCode); + } + + http.end(); + } else { + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("WiFi Terputus!"); + lcd.setCursor(0, 1); + lcd.print("Reconnecting..."); + Serial.println("WiFi Terputus, mencoba menyambungkan ulang..."); + WiFi.reconnect(); + delay(2000); + } + + delay(2000); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("TEMPELKAN GELANG"); + lcd.setCursor(0, 1); + lcd.print(" UNTUK MASUK "); +} \ No newline at end of file diff --git a/APLIKASI/.gitignore b/APLIKASI/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/APLIKASI/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/APLIKASI/.idea/.gitignore b/APLIKASI/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/APLIKASI/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/APLIKASI/.idea/.name b/APLIKASI/.idea/.name new file mode 100644 index 0000000..4e30d2b --- /dev/null +++ b/APLIKASI/.idea/.name @@ -0,0 +1 @@ +MoniKids \ No newline at end of file diff --git a/APLIKASI/.idea/AndroidProjectSystem.xml b/APLIKASI/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/APLIKASI/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/.idea/compiler.xml b/APLIKASI/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/APLIKASI/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/.idea/deploymentTargetSelector.xml b/APLIKASI/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/APLIKASI/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/.idea/gradle.xml b/APLIKASI/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/APLIKASI/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/APLIKASI/.idea/migrations.xml b/APLIKASI/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/APLIKASI/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/.idea/misc.xml b/APLIKASI/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/APLIKASI/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/.idea/runConfigurations.xml b/APLIKASI/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/APLIKASI/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/.gitignore b/APLIKASI/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/APLIKASI/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/APLIKASI/app/build.gradle b/APLIKASI/app/build.gradle new file mode 100644 index 0000000..0f590a1 --- /dev/null +++ b/APLIKASI/app/build.gradle @@ -0,0 +1,41 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace 'com.tugasakhir.monikids' + compileSdk 35 + + defaultConfig { + applicationId "com.tugasakhir.monikids" + minSdk 24 + targetSdk 35 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } +} + +dependencies { + + implementation libs.appcompat + implementation libs.material + implementation libs.activity + implementation libs.constraintlayout + testImplementation libs.junit + androidTestImplementation libs.ext.junit + androidTestImplementation libs.espresso.core + implementation 'com.android.volley:volley:1.2.1' +} \ No newline at end of file diff --git a/APLIKASI/app/proguard-rules.pro b/APLIKASI/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/APLIKASI/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/APLIKASI/app/src/androidTest/java/com/tugasakhir/monikids/ExampleInstrumentedTest.java b/APLIKASI/app/src/androidTest/java/com/tugasakhir/monikids/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f7493c2 --- /dev/null +++ b/APLIKASI/app/src/androidTest/java/com/tugasakhir/monikids/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.tugasakhir.monikids; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.tugasakhir.monikids", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/APLIKASI/app/src/main/AndroidManifest.xml b/APLIKASI/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0019e1e --- /dev/null +++ b/APLIKASI/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/java/com/tugasakhir/monikids/MainActivity.java b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/MainActivity.java new file mode 100644 index 0000000..e8cd410 --- /dev/null +++ b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/MainActivity.java @@ -0,0 +1,24 @@ +package com.tugasakhir.monikids; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + } +} \ No newline at end of file diff --git a/APLIKASI/app/src/main/java/com/tugasakhir/monikids/utils/CustomAlertDialog.java b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/utils/CustomAlertDialog.java new file mode 100644 index 0000000..cfde838 --- /dev/null +++ b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/utils/CustomAlertDialog.java @@ -0,0 +1,281 @@ +package com.tugasakhir.monikids.utils; + +import android.animation.ObjectAnimator; +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.util.DisplayMetrics; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.core.content.ContextCompat; + +import com.tugasakhir.monikids.R; + +public class CustomAlertDialog { + + public enum AlertType { + SUCCESS, ERROR, WARNING, INFO + } + + public interface OnAlertClickListener { + void onPositiveClick(); + void onNegativeClick(); + } + + private Dialog dialog; + private Context context; + private View dialogView; + private TextView tvTitle, tvMessage; + private ImageView ivIcon; + private Button btnPositive, btnNegative; + private ViewGroup layoutButtons; + + public CustomAlertDialog(Context context) { + this.context = context; + initDialog(); + } + + private void initDialog() { + dialog = new Dialog(context); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + dialog.setCancelable(false); + + LayoutInflater inflater = LayoutInflater.from(context); + dialogView = inflater.inflate(R.layout.dialog_custom_alert, null); + dialog.setContentView(dialogView); + + // Initialize views + tvTitle = dialogView.findViewById(R.id.tvAlertTitle); + tvMessage = dialogView.findViewById(R.id.tvAlertMessage); + ivIcon = dialogView.findViewById(R.id.ivAlertIcon); + btnPositive = dialogView.findViewById(R.id.btnPositive); + btnNegative = dialogView.findViewById(R.id.btnNegative); + layoutButtons = dialogView.findViewById(R.id.layoutButtons); + + // Set dialog size dengan proporsi yang lebih baik + setupDialogSize(); + } + + private void setupDialogSize() { + Window window = dialog.getWindow(); + if (window != null) { + WindowManager.LayoutParams layoutParams = window.getAttributes(); + + // Dapatkan ukuran layar + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + int screenWidth = displayMetrics.widthPixels; + int screenHeight = displayMetrics.heightPixels; + + // Set lebar dialog: 85% dari lebar layar (atau maksimal 400dp) + int maxWidthDp = 400; // maksimal 400dp + int maxWidthPx = (int) (maxWidthDp * displayMetrics.density); + int desiredWidth = (int) (screenWidth * 0.85f); // 85% dari lebar layar + + layoutParams.width = Math.min(desiredWidth, maxWidthPx); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + + // Center dialog di layar + layoutParams.gravity = android.view.Gravity.CENTER; + + window.setAttributes(layoutParams); + } + } + + public CustomAlertDialog setTitle(String title) { + tvTitle.setText(title); + tvTitle.setVisibility(View.VISIBLE); + return this; + } + + public CustomAlertDialog setMessage(String message) { + tvMessage.setText(message); + return this; + } + + public CustomAlertDialog setAlertType(AlertType type) { + switch (type) { + case SUCCESS: + ivIcon.setImageResource(R.drawable.ic_check_circle); + ivIcon.setColorFilter(ContextCompat.getColor(context, R.color.success_color)); + tvTitle.setTextColor(ContextCompat.getColor(context, R.color.success_color)); + break; + case ERROR: + ivIcon.setImageResource(R.drawable.ic_error_circle); + ivIcon.setColorFilter(ContextCompat.getColor(context, R.color.error_color)); + tvTitle.setTextColor(ContextCompat.getColor(context, R.color.error_color)); + break; + case WARNING: + ivIcon.setImageResource(R.drawable.ic_warning_circle); + ivIcon.setColorFilter(ContextCompat.getColor(context, R.color.warning_color)); + tvTitle.setTextColor(ContextCompat.getColor(context, R.color.warning_color)); + break; + case INFO: + ivIcon.setImageResource(R.drawable.ic_info_circle); + ivIcon.setColorFilter(ContextCompat.getColor(context, R.color.purple_primary)); + tvTitle.setTextColor(ContextCompat.getColor(context, R.color.purple_primary)); + break; + } + return this; + } + + public CustomAlertDialog setPositiveButton(String text, OnAlertClickListener listener) { + btnPositive.setText(text); + btnPositive.setVisibility(View.VISIBLE); + btnPositive.setOnClickListener(v -> { + animateButtonPress(v); + if (listener != null) { + listener.onPositiveClick(); + } + dismiss(); + }); + return this; + } + + public CustomAlertDialog setNegativeButton(String text, OnAlertClickListener listener) { + btnNegative.setText(text); + btnNegative.setVisibility(View.VISIBLE); + btnNegative.setOnClickListener(v -> { + animateButtonPress(v); + if (listener != null) { + listener.onNegativeClick(); + } + dismiss(); + }); + return this; + } + + public CustomAlertDialog setCancelable(boolean cancelable) { + dialog.setCancelable(cancelable); + return this; + } + + public void show() { + if (!dialog.isShowing()) { + dialog.show(); + animateShow(); + } + } + + public void dismiss() { + if (dialog.isShowing()) { + animateHide(() -> dialog.dismiss()); + } + } + + private void animateShow() { + // Scale and fade in animation dengan efek lebih smooth + ObjectAnimator scaleX = ObjectAnimator.ofFloat(dialogView, "scaleX", 0.8f, 1.05f, 1f); + ObjectAnimator scaleY = ObjectAnimator.ofFloat(dialogView, "scaleY", 0.8f, 1.05f, 1f); + ObjectAnimator alpha = ObjectAnimator.ofFloat(dialogView, "alpha", 0f, 1f); + + scaleX.setDuration(400); + scaleY.setDuration(400); + alpha.setDuration(300); + + scaleX.setInterpolator(new AccelerateDecelerateInterpolator()); + scaleY.setInterpolator(new AccelerateDecelerateInterpolator()); + alpha.setInterpolator(new AccelerateDecelerateInterpolator()); + + scaleX.start(); + scaleY.start(); + alpha.start(); + + // Icon bounce animation yang lebih halus + ObjectAnimator iconBounce = ObjectAnimator.ofFloat(ivIcon, "scaleX", 0f, 1.3f, 1f); + ObjectAnimator iconBounceY = ObjectAnimator.ofFloat(ivIcon, "scaleY", 0f, 1.3f, 1f); + iconBounce.setDuration(500); + iconBounceY.setDuration(500); + iconBounce.setStartDelay(200); + iconBounceY.setStartDelay(200); + iconBounce.setInterpolator(new AccelerateDecelerateInterpolator()); + iconBounceY.setInterpolator(new AccelerateDecelerateInterpolator()); + iconBounce.start(); + iconBounceY.start(); + } + + private void animateHide(Runnable onComplete) { + ObjectAnimator scaleX = ObjectAnimator.ofFloat(dialogView, "scaleX", 1f, 0.9f); + ObjectAnimator scaleY = ObjectAnimator.ofFloat(dialogView, "scaleY", 1f, 0.9f); + ObjectAnimator alpha = ObjectAnimator.ofFloat(dialogView, "alpha", 1f, 0f); + + scaleX.setDuration(250); + scaleY.setDuration(250); + alpha.setDuration(250); + + alpha.addListener(new android.animation.AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(android.animation.Animator animation) { + onComplete.run(); + } + }); + + scaleX.start(); + scaleY.start(); + alpha.start(); + } + + private void animateButtonPress(View button) { + ObjectAnimator scaleX = ObjectAnimator.ofFloat(button, "scaleX", 1f, 0.95f, 1f); + ObjectAnimator scaleY = ObjectAnimator.ofFloat(button, "scaleY", 1f, 0.95f, 1f); + scaleX.setDuration(150); + scaleY.setDuration(150); + scaleX.start(); + scaleY.start(); + } + + // Builder pattern for easy creation + public static class Builder { + private CustomAlertDialog dialog; + + public Builder(Context context) { + dialog = new CustomAlertDialog(context); + } + + public Builder setTitle(String title) { + dialog.setTitle(title); + return this; + } + + public Builder setMessage(String message) { + dialog.setMessage(message); + return this; + } + + public Builder setAlertType(AlertType type) { + dialog.setAlertType(type); + return this; + } + + public Builder setPositiveButton(String text, OnAlertClickListener listener) { + dialog.setPositiveButton(text, listener); + return this; + } + + public Builder setNegativeButton(String text, OnAlertClickListener listener) { + dialog.setNegativeButton(text, listener); + return this; + } + + public Builder setCancelable(boolean cancelable) { + dialog.setCancelable(cancelable); + return this; + } + + public CustomAlertDialog create() { + return dialog; + } + + public void show() { + dialog.show(); + } + } +} \ No newline at end of file diff --git a/APLIKASI/app/src/main/java/com/tugasakhir/monikids/utils/IPManager.java b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/utils/IPManager.java new file mode 100644 index 0000000..e36169c --- /dev/null +++ b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/utils/IPManager.java @@ -0,0 +1,120 @@ +package com.tugasakhir.monikids.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +public class IPManager { + private static final String PREF_NAME = "IP_SETTINGS"; + private static final String KEY_SERVER_IP = "SERVER_IP"; + private static final String DEFAULT_IP = "192.168.18.120"; + + private static IPManager instance; + private SharedPreferences preferences; + private String currentIP; + + private IPManager(Context context) { + preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + currentIP = preferences.getString(KEY_SERVER_IP, DEFAULT_IP); + } + + public static synchronized IPManager getInstance(Context context) { + if (instance == null) { + instance = new IPManager(context.getApplicationContext()); + } + return instance; + } + + /** + * Mendapatkan IP server saat ini + */ + public String getCurrentIP() { + return currentIP; + } + + /** + * Mengubah IP server dan menyimpannya secara persisten + */ + public boolean setServerIP(String newIP) { + if (isValidIP(newIP)) { + currentIP = newIP; + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(KEY_SERVER_IP, newIP); + return editor.commit(); + } + return false; + } + + /** + * Mendapatkan base URL untuk API + */ + public String getBaseURL() { + return "http://" + currentIP + "/playground_api/"; + } + + /** + * Mendapatkan home URL + */ + public String getHomeURL() { + return "http://" + currentIP + "/playground_api/home.php"; + } + + /** + * Mendapatkan URL lengkap untuk endpoint tertentu + */ + public String getURL(String endpoint) { + return getBaseURL() + endpoint; + } + + /** + * Validasi format IP address + */ + private boolean isValidIP(String ip) { + if (ip == null || ip.trim().isEmpty()) { + return false; + } + + // Cek untuk localhost + if (ip.equals("localhost") || ip.equals("127.0.0.1")) { + return true; + } + + // Cek untuk emulator + if (ip.equals("10.0.2.2")) { + return true; + } + + // Validasi IP address format + String[] parts = ip.split("\\."); + if (parts.length != 4) { + return false; + } + + try { + for (String part : parts) { + int num = Integer.parseInt(part); + if (num < 0 || num > 255) { + return false; + } + } + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Reset ke IP default + */ + public void resetToDefault() { + setServerIP(DEFAULT_IP); + } + + /** + * Mendapatkan daftar IP yang umum digunakan + */ + public static class QuickIPs { + public static final String LOCALHOST = "127.0.0.1"; + public static final String EMULATOR = "10.0.2.2"; + public static final String DEFAULT = "192.168.18.120"; + } +} \ No newline at end of file diff --git a/APLIKASI/app/src/main/java/com/tugasakhir/monikids/view/HomeActivity.java b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/view/HomeActivity.java new file mode 100644 index 0000000..161e4a0 --- /dev/null +++ b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/view/HomeActivity.java @@ -0,0 +1,635 @@ +package com.tugasakhir.monikids.view; + +import android.Manifest; +import android.animation.ObjectAnimator; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.media.AudioAttributes; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.cardview.widget.CardView; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import com.tugasakhir.monikids.R; +import com.tugasakhir.monikids.utils.CustomAlertDialog; +import com.tugasakhir.monikids.utils.IPManager; // Import IPManager + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +public class HomeActivity extends AppCompatActivity { + + private static final String TAG = "HomeActivity"; + // Hapus hardcoded HOME_URL, ini akan diset dari IPManager + // private static final String HOME_URL = "http://192.168.18.120/playground_api/home.php"; + private String HOME_URL; // Deklarasikan sebagai non-static dan tanpa inisialisasi awal + + private SharedPreferences sharedPreferences; + private RequestQueue requestQueue; + private IPManager ipManager; // Deklarasi instance IPManager + + // UI Components + private TextView tvUserName, tvBandId, tvPhoneNumber, tvEntryTime; + private TextView tvLastAttraction, tvPlayDuration; + private LinearLayout llTitle, llStatistik, llLogout; + private LinearLayout timelineContainer; // Timeline container + private CardView cardLogout; + + // User data + private int idAnak; + private String nama, kodeGelang; + private String lastKnownExitTime = null; // Untuk menyimpan waktu keluar terakhir yang diketahui + + // Polling mechanism + private Handler handler; + private Runnable runnable; + private static final long POLLING_INTERVAL_MS = 10000; // Poll setiap 10 detik + + // Notification specific + private static final String CHANNEL_ID = "monikids_exit_channel"; + private static final int NOTIFICATION_ID = 101; // ID unik untuk notifikasi keluar + private Vibrator vibrator; // Untuk kontrol getaran manual + private CustomAlertDialog activeExitDialog = null; // Referensi ke dialog yang sedang aktif + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_home); + + initComponents(); + setupAnimations(); + getUserData(); + createNotificationChannel(); // Buat channel notifikasi + // HOME_URL akan diinisialisasi di onResume untuk memastikan IP terbaru + // startPollingHomeData(); // Mulai polling data akan dipanggil di onResume + setupLogout(); + } + + private void initComponents() { + sharedPreferences = getSharedPreferences("MoniKidsPrefs", MODE_PRIVATE); + requestQueue = Volley.newRequestQueue(this); + ipManager = IPManager.getInstance(this); // Inisialisasi IPManager di sini + vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); // Inisialisasi vibrator + + tvUserName = findViewById(R.id.tvUserName); + tvBandId = findViewById(R.id.tvBandId); + tvPhoneNumber = findViewById(R.id.tvPhoneNumber); + tvEntryTime = findViewById(R.id.tvEntryTime); + tvLastAttraction = findViewById(R.id.tvLastAttraction); + tvPlayDuration = findViewById(R.id.tvPlayDuration); + cardLogout = findViewById(R.id.cardLogout); + llTitle = findViewById(R.id.llTitle); + llStatistik = findViewById(R.id.llStatistik); + llLogout = findViewById(R.id.llLogout); + + // Initialize timeline container + timelineContainer = findViewById(R.id.timelineContainer); + } + + private void getUserData() { + idAnak = sharedPreferences.getInt("id_anak", 0); + nama = sharedPreferences.getString("nama", ""); + kodeGelang = sharedPreferences.getString("kode_gelang", ""); + + tvUserName.setText(nama); + tvBandId.setText(kodeGelang); + + // Ambil waktu keluar terakhir yang disimpan jika ada + lastKnownExitTime = sharedPreferences.getString("last_known_exit_time_" + idAnak, null); + Log.d(TAG, "Initial lastKnownExitTime: " + lastKnownExitTime); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = "Notifikasi Anak Keluar Playground"; + String description = "Notifikasi penting ketika anak keluar dari area bermain"; + int importance = NotificationManager.IMPORTANCE_HIGH; // Prioritas tinggi + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); + channel.setDescription(description); + + // Pengaturan getar untuk channel (pola berulang) + long[] vibrationPattern = {0, 1000, 500, 1000, 500, 1000, 500, 1000}; + channel.enableVibration(true); + channel.setVibrationPattern(vibrationPattern); + + // Menggunakan USAGE_NOTIFICATION karena USAGE_NOTIFICATION_COMMUNICATION membutuhkan API 29+ + channel.setSound(null, new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build()); + + NotificationManager notificationManager = getSystemService(NotificationManager.class); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + } + } + } + + private void startPollingHomeData() { + handler = new Handler(Looper.getMainLooper()); + runnable = new Runnable() { + @Override + public void run() { + loadHomeData(); + handler.postDelayed(this, POLLING_INTERVAL_MS); + } + }; + handler.post(runnable); + } + + private void stopPollingHomeData() { + if (handler != null && runnable != null) { + handler.removeCallbacks(runnable); + } + } + + private void loadHomeData() { + if (idAnak == 0) { + showError("Data user tidak valid"); + stopPollingHomeData(); + return; + } + + JSONObject requestData = new JSONObject(); + try { + requestData.put("id_anak", idAnak); + } catch (JSONException e) { + Log.e(TAG, "Error creating request data", e); + return; + } + + JsonObjectRequest request = new JsonObjectRequest( + Request.Method.POST, + HOME_URL, // Menggunakan HOME_URL yang sudah diset dari IPManager + requestData, + this::handleHomeResponse, + this::handleErrorResponse + ) { + @Override + public java.util.Map getHeaders() { + java.util.Map headers = new java.util.HashMap<>(); + headers.put("Content-Type", "application/json"); + headers.put("Accept", "application/json"); + return headers; + } + }; + + request.setRetryPolicy(new com.android.volley.DefaultRetryPolicy( + 10000, // 10 seconds timeout + 1, // no retry + 1.0f + )); + + requestQueue.add(request); + } + + private void handleHomeResponse(JSONObject response) { + try { + if (response.getBoolean("success")) { + JSONObject data = response.optJSONObject("data"); + if (data != null) { + JSONObject userData = data.optJSONObject("user_data"); + if (userData != null) { + updateUserData(userData); + String currentExitTime = userData.optString("exit_time", null); + // Cek apakah currentExitTime benar-benar string "null" dari PHP + if ("null".equals(currentExitTime)) { + currentExitTime = null; + } + checkAndNotifyChildExit(currentExitTime); + } + + JSONArray timelineArray = data.optJSONArray("timeline"); + if (timelineArray != null) { + populateTimeline(timelineArray); + } + } + } else { + Log.e(TAG, "API Success false: " + response.optString("message")); + } + } catch (JSONException e) { + Log.e(TAG, "Error parsing home response", e); + } + } + + private void handleErrorResponse(VolleyError error) { + if (error.networkResponse != null && error.networkResponse.data != null) { + String errorBody = new String(error.networkResponse.data); + Log.e(TAG, "Server Error: " + errorBody); + } else { + Log.e(TAG, "Network Error", error); + } + } + + private void updateUserData(JSONObject userData) throws JSONException { + String phoneNumber = userData.optString("no_hp", "-"); + String entryTime = userData.optString("entry_time", "-"); + String playDuration = userData.optString("play_duration", "-"); + String lastAttraction = userData.optString("last_attraction", "-"); + + tvPhoneNumber.setText(phoneNumber); + tvEntryTime.setText(entryTime); + tvPlayDuration.setText(playDuration); + tvLastAttraction.setText(lastAttraction); + } + + private void checkAndNotifyChildExit(String currentExitTime) { + Log.d(TAG, "Checking child exit. Current Exit Time: " + currentExitTime + ", Last Known Exit Time: " + lastKnownExitTime); + + // Jika currentExitTime tidak null/kosong DAN ada perubahan status + if (currentExitTime != null && !currentExitTime.isEmpty() && + (lastKnownExitTime == null || !lastKnownExitTime.equals(currentExitTime))) { + + lastKnownExitTime = currentExitTime; + sharedPreferences.edit().putString("last_known_exit_time_" + idAnak, currentExitTime).apply(); + Log.d(TAG, "Anak terdeteksi keluar! Notifikasi akan dikirim."); + + // Hentikan polling sementara untuk mencegah pemicuan berulang + stopPollingHomeData(); + + sendChildExitNotification(); + showChildExitDialog(); + + } else if (currentExitTime == null || currentExitTime.isEmpty()) { + // Jika anak masih bermain (currentExitTime null/kosong) + // Pastikan lastKnownExitTime juga null agar deteksi berjalan lagi jika anak keluar setelah masuk kembali + if (lastKnownExitTime != null) { + Log.d(TAG, "Anak masuk kembali atau status reset. Resetting lastKnownExitTime."); + lastKnownExitTime = null; + sharedPreferences.edit().remove("last_known_exit_time_" + idAnak).apply(); + } + } + } + + private void sendChildExitNotification() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && // Hanya berlaku untuk Android 13 (API 33) ke atas + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "Notifikasi tidak dapat dikirim: Izin POST_NOTIFICATIONS tidak diberikan."); + return; // Hentikan jika izin tidak diberikan + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_warning_circle) + .setContentTitle("Anak Anda Keluar Playground!") + .setContentText("Anak Anda (" + nama + ") telah keluar dari area bermain.") + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .setAutoCancel(false); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + builder.setVibrate(new long[]{0, 1000, 500, 1000, 500, 1000, 500, 1000}); + } + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.notify(NOTIFICATION_ID, builder.build()); + + startContinuousVibration(); + } + + private void showChildExitDialog() { + // Karena CustomAlertDialog Anda tidak mengekspos isShowing() secara publik, + // kita akan menggunakan referensi activeExitDialog itu sendiri. + // Jika activeExitDialog tidak null, asumsi dia sedang ditampilkan. + // Anda mungkin perlu menambahkan method `isShowing()` ke CustomAlertDialog Anda + // untuk penanganan yang lebih robust (seperti yang disarankan di respons sebelumnya). + if (activeExitDialog != null) { + Log.d(TAG, "Dialog keluar anak sudah ditampilkan, tidak perlu menampilkan lagi."); + return; + } + + activeExitDialog = new CustomAlertDialog.Builder(this) + .setTitle("Pemberitahuan: Anak Terdeteksi Keluar") + .setMessage("Anak Anda, " + nama + ", telah terdeteksi keluar dari area bermain. Ini menandakan sesi bermain telah berakhir. Silakan jemput anak Anda.") + .setAlertType(CustomAlertDialog.AlertType.WARNING) + .setPositiveButton("OKE", new CustomAlertDialog.OnAlertClickListener() { + @Override + public void onPositiveClick() { + Log.d(TAG, "Tombol OK pada dialog keluar anak ditekan."); + stopContinuousVibration(); + clearChildExitNotification(); + activeExitDialog = null; // Reset referensi dialog + startPollingHomeData(); // Lanjutkan polling + } + + @Override + public void onNegativeClick() { + // Tidak digunakan di sini + } + }) + .setCancelable(false) + .create(); + + activeExitDialog.show(); + } + + private void startContinuousVibration() { + // --- Periksa izin VIBRATE sebelum memulai getaran --- + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // API 23 = Android 6.0 + if (ContextCompat.checkSelfPermission(this, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "Vibration not allowed: VIBRATE permission denied."); + return; // Hentikan jika izin tidak diberikan + } + } + // --- Akhir pemeriksaan izin VIBRATE --- + + if (vibrator != null && vibrator.hasVibrator()) { + long[] pattern = {0, 1000, 500}; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createWaveform(pattern, 0)); + } else { + vibrator.vibrate(pattern, 0); + } + Log.d(TAG, "Continuous vibration started."); + } else { + Log.w(TAG, "Vibrator not available or not supported on this device."); + } + } + + private void stopContinuousVibration() { + if (vibrator != null) { + vibrator.cancel(); + Log.d(TAG, "Continuous vibration stopped."); + } + } + + private void clearChildExitNotification() { + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.cancel(NOTIFICATION_ID); + Log.d(TAG, "Child exit notification cleared."); + } + + private void populateTimeline(JSONArray timelineArray) { + timelineContainer.removeAllViews(); + Log.d(TAG, "Populating timeline with " + timelineArray.length() + " items"); + + try { + for (int i = 0; i < timelineArray.length(); i++) { + JSONObject timelineItem = timelineArray.getJSONObject(i); + String type = timelineItem.optString("type", "placeholder"); + String wahanaName = timelineItem.optString("wahana_name", ""); + String waktuMasuk = timelineItem.optString("waktu_masuk", ""); + String waktuKeluar = timelineItem.optString("waktu_keluar", ""); + String durasi = timelineItem.optString("durasi", ""); + + Log.d(TAG, "Creating timeline item - Type: " + type + ", Wahana: " + wahanaName); + + // Perbaiki inflate timeline_item agar tidak IllegalStateException + // Gunakan parent (timelineContainer) dan false untuk attachToRoot + View timelineView = createTimelineItem(type, wahanaName, waktuMasuk, waktuKeluar, durasi, i == timelineArray.length() - 1); + if (timelineView != null) { + timelineContainer.addView(timelineView); + } + } + animateTimelineIn(); + } catch (JSONException e) { + Log.e(TAG, "Error parsing timeline data", e); + } + } + + private View createTimelineItem(String type, String wahanaName, String waktuMasuk, String waktuKeluar, String durasi, boolean isLastItem) { + LayoutInflater inflater = LayoutInflater.from(this); + View timelineView = null; + + switch (type) { + case "active": + timelineView = inflater.inflate(R.layout.timeline_item_active, timelineContainer, false); + TextView tvActiveTitle = timelineView.findViewById(R.id.tvTimelineTitle); + TextView tvActiveTime = timelineView.findViewById(R.id.tvTimelineTime); + + if (tvActiveTitle != null) tvActiveTitle.setText(wahanaName); + if (tvActiveTime != null) { + String displayTime = waktuMasuk; + if (!durasi.isEmpty()) { + displayTime += " • " + durasi; + } + tvActiveTime.setText(displayTime); + } + break; + + case "inactive": + timelineView = inflater.inflate(R.layout.timeline_item_inactive, timelineContainer, false); + TextView tvInactiveTitle = timelineView.findViewById(R.id.tvTimelineTitle); + TextView tvInactiveTime = timelineView.findViewById(R.id.tvTimelineTime); + TextView tvInactiveExitTime = timelineView.findViewById(R.id.tvTimelineExitTime); + + if (tvInactiveTitle != null) tvInactiveTitle.setText(wahanaName); + if (tvInactiveTime != null) tvInactiveTime.setText(waktuMasuk); + if (tvInactiveExitTime != null) tvInactiveExitTime.setText(waktuKeluar); + break; + + case "entry": + timelineView = inflater.inflate(R.layout.timeline_item_entry, timelineContainer, false); + TextView tvEntryTimeView = timelineView.findViewById(R.id.tvTimelineTime); // Ubah nama variabel agar tidak konflik + if (tvEntryTimeView != null) tvEntryTimeView.setText(waktuMasuk); + break; + + case "placeholder": + timelineView = inflater.inflate(R.layout.timeline_item_placeholder, timelineContainer, false); + break; + + default: + Log.w(TAG, "Unknown timeline type: " + type); + return null; + } + + if (isLastItem && timelineView != null) { + View timelineLine = timelineView.findViewById(R.id.timelineLine); + if (timelineLine != null) { + timelineLine.setVisibility(View.GONE); + } + } + + return timelineView; + } + + private void animateTimelineIn() { + for (int i = 0; i < timelineContainer.getChildCount(); i++) { + View child = timelineContainer.getChildAt(i); + child.setAlpha(0f); + child.setTranslationX(100f); + + ObjectAnimator fadeIn = ObjectAnimator.ofFloat(child, "alpha", 0f, 1f); + ObjectAnimator slideIn = ObjectAnimator.ofFloat(child, "translationX", 100f, 0f); + + fadeIn.setDuration(400); + slideIn.setDuration(400); + fadeIn.setStartDelay(i * 100); + slideIn.setStartDelay(i * 100); + fadeIn.setInterpolator(new AccelerateDecelerateInterpolator()); + slideIn.setInterpolator(new AccelerateDecelerateInterpolator()); + + fadeIn.start(); + slideIn.start(); + } + } + + private void setupLogout() { + cardLogout.setOnClickListener(v -> { + ObjectAnimator scaleX = ObjectAnimator.ofFloat(cardLogout, "scaleX", 1f, 0.95f, 1f); + ObjectAnimator scaleY = ObjectAnimator.ofFloat(cardLogout, "scaleY", 1f, 0.95f, 1f); + scaleX.setDuration(150); + scaleY.setDuration(150); + scaleX.start(); + scaleY.start(); + + performLogout(); + }); + } + + private void performLogout() { + new CustomAlertDialog.Builder(this) + .setTitle("Konfirmasi Logout") + .setMessage("Apakah Anda yakin ingin keluar dari akun ini?") + .setAlertType(CustomAlertDialog.AlertType.INFO) + .setPositiveButton("YA", new CustomAlertDialog.OnAlertClickListener() { + @Override + public void onPositiveClick() { + showSuccessAnimation(); + new Handler().postDelayed(() -> { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear(); + editor.apply(); + + stopPollingHomeData(); + + Intent intent = new Intent(HomeActivity.this, LoginActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + }, 500); + } + + @Override + public void onNegativeClick() { + // Not used + } + }) + .setNegativeButton("TIDAK", null) + .setCancelable(false) + .show(); + } + + private void showSuccessAnimation() { + ObjectAnimator.ofFloat(cardLogout, "alpha", 1f, 0.5f, 1f) + .setDuration(300) + .start(); + } + + private void setupAnimations() { + ObjectAnimator fadeInTitle = ObjectAnimator.ofFloat(llTitle, "alpha", 0f, 1f); + fadeInTitle.setDuration(800); + fadeInTitle.setInterpolator(new AccelerateDecelerateInterpolator()); + fadeInTitle.start(); + + ObjectAnimator fadeInStatistik = ObjectAnimator.ofFloat(llStatistik, "alpha", 0f, 1f); + fadeInStatistik.setDuration(800); + fadeInStatistik.setInterpolator(new AccelerateDecelerateInterpolator()); + fadeInStatistik.start(); + + ObjectAnimator fadeInLogout = ObjectAnimator.ofFloat(llLogout, "alpha", 0f, 1f); + fadeInLogout.setDuration(800); + fadeInLogout.setInterpolator(new AccelerateDecelerateInterpolator()); + fadeInLogout.start(); + } + + @Override + public void onBackPressed() { + // Karena CustomAlertDialog Anda sudah punya method show() dan dismiss() publik, + // kita bisa menggunakannya secara langsung. Namun, tidak ada method isShowing() publik. + // Jadi, kita berasumsi jika activeExitDialog tidak null, berarti dia sedang ditampilkan. + // Pilihan yang lebih baik adalah menambahkan method isShowing() publik di CustomAlertDialog. + if (activeExitDialog != null) { + return; // Jangan lakukan apa-apa jika dialog notifikasi anak keluar sedang tampil + } + + new CustomAlertDialog.Builder(this) + .setTitle("Konfirmasi Logout") + .setMessage("Apakah Anda yakin ingin keluar dari akun ini?") + .setAlertType(CustomAlertDialog.AlertType.INFO) + .setPositiveButton("KELUAR", new CustomAlertDialog.OnAlertClickListener() { + @Override + public void onPositiveClick() { + showSuccessAnimation(); + new Handler().postDelayed(() -> { + stopPollingHomeData(); + Intent intent = new Intent(HomeActivity.this, LoginActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + }, 500); + } + + @Override + public void onNegativeClick() { + // Not used + } + }) + .setNegativeButton("BATAL", null) + .show(); + } + + private void showError(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + @Override + protected void onResume() { + super.onResume(); + // Inisialisasi HOME_URL dari IPManager di onResume + HOME_URL = ipManager.getURL("home.php"); + Log.d(TAG, "Using API URL: " + HOME_URL); + + if (idAnak != 0) { + startPollingHomeData(); + } + } + + @Override + protected void onPause() { + super.onPause(); + stopPollingHomeData(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + stopPollingHomeData(); + if (vibrator != null) { + vibrator.cancel(); + } + if (activeExitDialog != null) { + activeExitDialog.dismiss(); + activeExitDialog = null; + } + // Batalkan semua request Volley untuk Activity ini saat dihancurkan + if (requestQueue != null) { + requestQueue.cancelAll(TAG); + } + } +} \ No newline at end of file diff --git a/APLIKASI/app/src/main/java/com/tugasakhir/monikids/view/IPSettingsActivity.java b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/view/IPSettingsActivity.java new file mode 100644 index 0000000..c4b1bc9 --- /dev/null +++ b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/view/IPSettingsActivity.java @@ -0,0 +1,184 @@ +package com.tugasakhir.monikids.view; + +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; +import com.tugasakhir.monikids.R; +import com.tugasakhir.monikids.utils.IPManager; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +public class IPSettingsActivity extends AppCompatActivity { + + private IPManager ipManager; + private TextView tvCurrentIPDisplay; + private TextInputEditText etNewIP; + private TextInputLayout inputLayoutIP; + private Button btnSaveIP, btnTestConnection; + private Button btnLocalhost, btnEmulator, btnDefault; + private TextView tvConnectionStatus; + private ImageButton btnBack; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_ip_settings); + + initViews(); + initIPManager(); + setupClickListeners(); + updateCurrentIPDisplay(); + } + + private void initViews() { + btnBack = findViewById(R.id.btnBack); + tvCurrentIPDisplay = findViewById(R.id.tvCurrentIPDisplay); + etNewIP = findViewById(R.id.etNewIP); + inputLayoutIP = findViewById(R.id.inputLayoutIP); + btnSaveIP = findViewById(R.id.btnSaveIP); + btnTestConnection = findViewById(R.id.btnTestConnection); + btnLocalhost = findViewById(R.id.btnLocalhost); + btnEmulator = findViewById(R.id.btnEmulator); + btnDefault = findViewById(R.id.btnDefault); + tvConnectionStatus = findViewById(R.id.tvConnectionStatus); + } + + private void initIPManager() { + ipManager = IPManager.getInstance(this); + } + + private void setupClickListeners() { + btnBack.setOnClickListener(v -> finish()); + + btnSaveIP.setOnClickListener(v -> saveNewIP()); + + btnTestConnection.setOnClickListener(v -> testConnection()); + + btnLocalhost.setOnClickListener(v -> { + etNewIP.setText(IPManager.QuickIPs.LOCALHOST); + clearError(); + }); + + btnEmulator.setOnClickListener(v -> { + etNewIP.setText(IPManager.QuickIPs.EMULATOR); + clearError(); + }); + + btnDefault.setOnClickListener(v -> { + etNewIP.setText(IPManager.QuickIPs.DEFAULT); + clearError(); + }); + } + + private void updateCurrentIPDisplay() { + String currentIP = ipManager.getCurrentIP(); + tvCurrentIPDisplay.setText(currentIP); + etNewIP.setText(currentIP); + } + + private void saveNewIP() { + String newIP = etNewIP.getText().toString().trim(); + + if (newIP.isEmpty()) { + showError("IP tidak boleh kosong"); + return; + } + + boolean success = ipManager.setServerIP(newIP); + + if (success) { + updateCurrentIPDisplay(); + clearError(); + Toast.makeText(this, "IP berhasil disimpan: " + newIP, Toast.LENGTH_SHORT).show(); + + // Broadcast change to other activities if needed + broadcastIPChange(); + } else { + showError("Format IP tidak valid"); + } + } + + private void testConnection() { + String testIP = etNewIP.getText().toString().trim(); + + if (testIP.isEmpty()) { + showError("Masukkan IP terlebih dahulu"); + return; + } + + btnTestConnection.setEnabled(false); + btnTestConnection.setText("TESTING..."); + tvConnectionStatus.setVisibility(View.VISIBLE); + tvConnectionStatus.setText("Menguji koneksi..."); + tvConnectionStatus.setTextColor(Color.GRAY); + + new TestConnectionTask().execute(testIP); + } + + private void showError(String message) { + inputLayoutIP.setError(message); + } + + private void clearError() { + inputLayoutIP.setError(null); + } + + private void broadcastIPChange() { + // Optional: Send broadcast to notify other activities about IP change + // Intent intent = new Intent("IP_CHANGED"); + // sendBroadcast(intent); + } + + private class TestConnectionTask extends AsyncTask { + private String testIP; + private String errorMessage = ""; + + @Override + protected Boolean doInBackground(String... params) { + testIP = params[0]; + try { + String testUrl = "http://" + testIP + "/playground_api/"; + URL url = new URL(testUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); // 5 seconds timeout + connection.setReadTimeout(5000); + + int responseCode = connection.getResponseCode(); + connection.disconnect(); + + return responseCode == HttpURLConnection.HTTP_OK || + responseCode == HttpURLConnection.HTTP_NOT_FOUND || + responseCode == HttpURLConnection.HTTP_FORBIDDEN; + + } catch (IOException e) { + errorMessage = e.getMessage(); + return false; + } + } + + @Override + protected void onPostExecute(Boolean success) { + btnTestConnection.setEnabled(true); + btnTestConnection.setText("UJI KONEKSI"); + + if (success) { + tvConnectionStatus.setText("✓ Koneksi berhasil ke " + testIP); + tvConnectionStatus.setTextColor(Color.GREEN); + } else { + tvConnectionStatus.setText("✗ Koneksi gagal: " + errorMessage); + tvConnectionStatus.setTextColor(Color.RED); + } + } + } +} \ No newline at end of file diff --git a/APLIKASI/app/src/main/java/com/tugasakhir/monikids/view/LoginActivity.java b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/view/LoginActivity.java new file mode 100644 index 0000000..2411a2a --- /dev/null +++ b/APLIKASI/app/src/main/java/com/tugasakhir/monikids/view/LoginActivity.java @@ -0,0 +1,627 @@ +package com.tugasakhir.monikids.view; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.text.InputType; +import android.util.Log; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; +import com.tugasakhir.monikids.R; +import com.tugasakhir.monikids.utils.CustomAlertDialog; +import com.tugasakhir.monikids.utils.IPManager; // Ensure this import is present + +import org.json.JSONException; +import org.json.JSONObject; + +public class LoginActivity extends AppCompatActivity { + + private Button btnLoginByName, btnLoginByCode, btnLoginByPhone, btnLogin; + private TextInputEditText etLoginInput; + private LinearLayout llLogin; + private TextInputLayout inputLayout; + private RequestQueue requestQueue; + private SharedPreferences sharedPreferences; + + private IPManager ipManager; // IPManager instance + private TextView tvCurrentIP; // TextView to display current IP + private ImageButton btnIPSettings; // Button for IP settings + + // Remove hardcoded BASE_URL, it will be retrieved from IPManager + // private static final String BASE_URL = "http://192.168.18.120/playground_api/"; + private String LOGIN_URL; // This will be set dynamically + private static final String TAG = "LoginActivity"; + private static final String PREFS_NAME = "MoniKidsPrefs"; + + private enum LoginType { + NAME, CODE, PHONE + } + + private LoginType currentLoginType = LoginType.NAME; + private boolean isLoading = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + initViews(); + initNetworkAndIPManager(); // Renamed to include IPManager init + setupClickListeners(); + updateInputForLoginType(); + setupAnimations(); + updateCurrentIPDisplay(); // Display current IP on create + } + + @Override + protected void onResume() { + super.onResume(); + updateCurrentIPDisplay(); // Update IP display when returning to this activity + // Re-initialize LOGIN_URL to ensure it reflects potential IP changes + LOGIN_URL = ipManager.getURL("login.php"); + Log.d(TAG, "Final LOGIN_URL: " + LOGIN_URL); + Log.d(TAG, "Current IP from IPManager: " + ipManager.getCurrentIP()); + Log.d(TAG, "Base URL: " + ipManager.getBaseURL()); + Log.d(TAG, "Using API URL: " + LOGIN_URL); + } + + private void initViews() { + btnLoginByName = findViewById(R.id.btnLoginByName); + btnLoginByCode = findViewById(R.id.btnLoginByCode); + btnLoginByPhone = findViewById(R.id.btnLoginByPhone); + btnLogin = findViewById(R.id.btnLogin); + etLoginInput = findViewById(R.id.etLoginInput); + inputLayout = findViewById(R.id.inputLayout); + llLogin = findViewById(R.id.llLogin); + + // Initialize IP-related views + tvCurrentIP = findViewById(R.id.tvCurrentIP); + btnIPSettings = findViewById(R.id.btnIPSettings); + } + + private void initNetworkAndIPManager() { // Renamed method + requestQueue = Volley.newRequestQueue(this); + sharedPreferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + ipManager = IPManager.getInstance(this); // Initialize IPManager + + // Set the LOGIN_URL dynamically using IPManager + LOGIN_URL = ipManager.getURL("login.php"); + Log.d(TAG, "Initialized with API URL: " + LOGIN_URL); + } + + private void setupClickListeners() { + btnLoginByName.setOnClickListener(v -> { + if (currentLoginType != LoginType.NAME) { + animateButtonSelection(v); + selectLoginType(LoginType.NAME); + } + }); + + btnLoginByCode.setOnClickListener(v -> { + if (currentLoginType != LoginType.CODE) { + animateButtonSelection(v); + selectLoginType(LoginType.CODE); + } + }); + + btnLoginByPhone.setOnClickListener(v -> { + if (currentLoginType != LoginType.PHONE) { + animateButtonSelection(v); + selectLoginType(LoginType.PHONE); + } + }); + + btnLogin.setOnClickListener(v -> { + if (!isLoading) { + animateButtonPress(v); + performLogin(); + } + }); + + // Click listener for IP Settings button + btnIPSettings.setOnClickListener(v -> { + Intent intent = new Intent(LoginActivity.this, IPSettingsActivity.class); + startActivity(intent); + }); + } + + private void setupAnimations() { + // Add entrance animations + ObjectAnimator fadeIn = ObjectAnimator.ofFloat(llLogin, "alpha", 0f, 1f); + fadeIn.setDuration(800); + fadeIn.setInterpolator(new AccelerateDecelerateInterpolator()); + fadeIn.start(); + } + + private void animateButtonSelection(View button) { + ObjectAnimator scaleX = ObjectAnimator.ofFloat(button, "scaleX", 1f, 1.1f, 1f); + ObjectAnimator scaleY = ObjectAnimator.ofFloat(button, "scaleY", 1f, 1.1f, 1f); + scaleX.setDuration(200); + scaleY.setDuration(200); + scaleX.start(); + scaleY.start(); + } + + private void animateButtonPress(View button) { + ObjectAnimator scaleX = ObjectAnimator.ofFloat(button, "scaleX", 1f, 0.95f, 1f); + ObjectAnimator scaleY = ObjectAnimator.ofFloat(button, "scaleY", 1f, 0.95f, 1f); + scaleX.setDuration(150); + scaleY.setDuration(150); + scaleX.start(); + scaleY.start(); + } + + private void selectLoginType(LoginType loginType) { + currentLoginType = loginType; + + // Reset all buttons to unselected state + resetButtonStyles(); + + // Set selected button style with animation + switch (loginType) { + case NAME: + btnLoginByName.setBackgroundResource(R.drawable.login_option_selected); + btnLoginByName.setTextColor(ContextCompat.getColor(this, android.R.color.white)); + break; + case CODE: + btnLoginByCode.setBackgroundResource(R.drawable.login_option_selected); + btnLoginByCode.setTextColor(ContextCompat.getColor(this, android.R.color.white)); + break; + case PHONE: + btnLoginByPhone.setBackgroundResource(R.drawable.login_option_selected); + btnLoginByPhone.setTextColor(ContextCompat.getColor(this, android.R.color.white)); + break; + } + + updateInputForLoginType(); + } + + private void resetButtonStyles() { + int unselectedColor = ContextCompat.getColor(this, R.color.purple_primary); + + btnLoginByName.setBackgroundResource(R.drawable.login_option_unselected); + btnLoginByName.setTextColor(unselectedColor); + + btnLoginByCode.setBackgroundResource(R.drawable.login_option_unselected); + btnLoginByCode.setTextColor(unselectedColor); + + btnLoginByPhone.setBackgroundResource(R.drawable.login_option_unselected); + btnLoginByPhone.setTextColor(unselectedColor); + } + + private void updateInputForLoginType() { + etLoginInput.setText(""); // Clear previous input + inputLayout.setError(null); // Clear any previous errors + + // Animate input field change + ObjectAnimator fadeOut = ObjectAnimator.ofFloat(inputLayout, "alpha", 1f, 0.7f, 1f); + fadeOut.setDuration(300); + fadeOut.start(); + + switch (currentLoginType) { + case NAME: + inputLayout.setHint("Masukkan nama anak"); + inputLayout.setStartIconDrawable(R.drawable.ic_child); + etLoginInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PERSON_NAME); + break; + case CODE: + inputLayout.setHint("Masukkan kode gelang"); + inputLayout.setStartIconDrawable(R.drawable.ic_code); + etLoginInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL); + break; + case PHONE: + inputLayout.setHint("Masukkan nomor HP"); + inputLayout.setStartIconDrawable(R.drawable.ic_phone); + etLoginInput.setInputType(InputType.TYPE_CLASS_PHONE); + break; + } + } + + private void updateCurrentIPDisplay() { + // Update the TextView with the current IP from IPManager + if (tvCurrentIP != null && ipManager != null) { + tvCurrentIP.setText(ipManager.getCurrentIP()); + } + } + + private void performLogin() { + String input = etLoginInput.getText().toString().trim(); + + if (input.isEmpty()) { + String errorMessage = getEmptyErrorMessage(); + + new CustomAlertDialog.Builder(this) + .setTitle("Input Kosong") + .setMessage(errorMessage) + .setAlertType(CustomAlertDialog.AlertType.WARNING) + .setPositiveButton("OK", new CustomAlertDialog.OnAlertClickListener() { + @Override + public void onPositiveClick() { + etLoginInput.requestFocus(); + } + + @Override + public void onNegativeClick() { + // Not used + } + }) + .show(); + + inputLayout.setError(errorMessage); + animateErrorShake(); + return; + } + + inputLayout.setError(null); + + if (!validateInput(input)) { + animateErrorShake(); + return; + } + + setLoadingState(true); + + performAPILogin(input); + } + + private void performAPILogin(String input) { + try { + JSONObject jsonBody = new JSONObject(); + jsonBody.put("login_type", currentLoginType.name()); + jsonBody.put("input_value", input); + + JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, LOGIN_URL, jsonBody, new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + handleLoginResponse(response, input); + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + handleLoginError(error); + } + }); + + requestQueue.add(request); + + } catch (JSONException e) { + Log.e(TAG, "Error creating JSON request", e); + setLoadingState(false); + showErrorAlert("Error", "Terjadi kesalahan dalam membuat request: " + e.getMessage()); + } + } + + private void handleLoginResponse(JSONObject response, String input) { + setLoadingState(false); + + try { + boolean success = response.getBoolean("success"); + String message = response.getString("message"); + + if (success) { + new CustomAlertDialog.Builder(this) + .setTitle("Login Berhasil!") + .setMessage("Selamat datang! Anda akan diarahkan ke halaman utama.") + .setAlertType(CustomAlertDialog.AlertType.SUCCESS) + .setPositiveButton("LANJUTKAN", new CustomAlertDialog.OnAlertClickListener() { + @Override + public void onPositiveClick() { + try { + JSONObject data = response.getJSONObject("data"); + saveLoginData(data); + + showSuccessAnimation(); + + new Handler().postDelayed(() -> { + navigateToHomeActivity(data); + }, 500); + + } catch (JSONException e) { + Log.e(TAG, "Error parsing data", e); + showErrorAlert("Error", "Terjadi kesalahan dalam memproses data login."); + } + } + + @Override + public void onNegativeClick() { + // Not used + } + }) + .setCancelable(false) + .show(); + + } else { + new CustomAlertDialog.Builder(this) + .setTitle("Login Gagal") + .setMessage(message) + .setAlertType(CustomAlertDialog.AlertType.ERROR) + .setPositiveButton("COBA LAGI", new CustomAlertDialog.OnAlertClickListener() { + @Override + public void onPositiveClick() { + etLoginInput.setText(""); + etLoginInput.requestFocus(); + } + + @Override + public void onNegativeClick() { + // Not used + } + }) + .show(); + + inputLayout.setError(message); + animateErrorShake(); + } + + } catch (JSONException e) { + Log.e(TAG, "Error parsing login response", e); + showErrorAlert("Error", "Terjadi kesalahan dalam memproses data. Silakan coba lagi."); + } + } + + private void handleLoginError(VolleyError error) { + setLoadingState(false); + + String errorMessage = "Koneksi gagal. Pastikan server berjalan dan IP server diatur dengan benar."; // Updated message + + if (error.networkResponse != null) { + try { + String responseBody = new String(error.networkResponse.data, "utf-8"); + JSONObject data = new JSONObject(responseBody); + errorMessage = data.getString("message"); + } catch (Exception e) { + Log.e(TAG, "Error parsing error response", e); + } + } else if (error.getCause() instanceof java.net.ConnectException) { + errorMessage = "Tidak dapat terhubung ke server. Pastikan IP server sudah benar dan server sedang berjalan."; + } else if (error.getCause() instanceof java.net.UnknownHostException) { + errorMessage = "IP server tidak dikenal. Pastikan IP yang dimasukkan valid."; + } + + + Log.e(TAG, "Login API Error: " + error.toString()); + Log.e(TAG, "Error details - NetworkResponse: " + error.networkResponse); + Log.e(TAG, "Error details - Cause: " + error.getCause()); + Log.e(TAG, "Full URL being called: " + LOGIN_URL); + + new CustomAlertDialog.Builder(this) + .setTitle("Koneksi Bermasalah") + .setMessage(errorMessage) + .setAlertType(CustomAlertDialog.AlertType.ERROR) + .setPositiveButton("COBA LAGI", new CustomAlertDialog.OnAlertClickListener() { + @Override + public void onPositiveClick() { + String input = etLoginInput.getText().toString().trim(); + if (!input.isEmpty()) { + performLogin(); + } + } + + @Override + public void onNegativeClick() { + // Not used + } + }) + .setNegativeButton("BATAL", null) // Just close dialog + .show(); + + inputLayout.setError(errorMessage); + animateErrorShake(); + } + + private void saveLoginData(JSONObject data) { + try { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt("id_anak", data.getInt("id_anak")); + editor.putString("nama", data.getString("nama")); + editor.putString("kode_gelang", data.getString("kode_gelang")); + editor.putString("login_type", data.getString("login_type")); + editor.putLong("login_time", System.currentTimeMillis()); + editor.apply(); + } catch (JSONException e) { + Log.e(TAG, "Error saving login data", e); + } + } + + private void showSuccessAnimation() { + ObjectAnimator successScale = ObjectAnimator.ofFloat(btnLogin, "scaleX", 1f, 1.1f, 1f); + ObjectAnimator successScaleY = ObjectAnimator.ofFloat(btnLogin, "scaleY", 1f, 1.1f, 1f); + successScale.setDuration(300); + successScaleY.setDuration(300); + successScale.start(); + successScaleY.start(); + } + + private void navigateToHomeActivity(JSONObject data) { + try { + Intent intent = new Intent(LoginActivity.this, HomeActivity.class); + intent.putExtra("id_anak", data.getInt("id_anak")); + intent.putExtra("nama", data.getString("nama")); + intent.putExtra("kode_gelang", data.getString("kode_gelang")); + intent.putExtra("login_type", data.getString("login_type")); + startActivity(intent); + finish(); + } catch (JSONException e) { + Log.e(TAG, "Error navigating to home activity", e); + showErrorAlert("Error", "Terjadi kesalahan saat navigasi ke halaman utama."); + } + } + + private String getEmptyErrorMessage() { + switch (currentLoginType) { + case NAME: + return "Nama anak tidak boleh kosong"; + case CODE: + return "Kode gelang tidak boleh kosong"; + case PHONE: + return "Nomor HP tidak boleh kosong"; + default: + return "Input tidak boleh kosong"; + } + } + + private void animateErrorShake() { + ObjectAnimator shake = ObjectAnimator.ofFloat(inputLayout, "translationX", 0, 25, -25, 25, -25, 15, -15, 6, -6, 0); + shake.setDuration(600); + shake.start(); + } + + private boolean validateInput(String input) { + switch (currentLoginType) { + case NAME: + if (input.length() < 2) { + showValidationAlert("Nama minimal 2 karakter"); + return false; + } + if (!input.matches("^[a-zA-Z\\s]+$")) { + showValidationAlert("Nama hanya boleh berisi huruf dan spasi"); + return false; + } + break; + case CODE: + if (input.length() < 4) { + showValidationAlert("Kode gelang minimal 4 karakter"); + return false; + } + if (!input.matches("^[A-Za-z0-9]+$")) { + showValidationAlert("Kode hanya boleh berisi huruf dan angka"); + return false; + } + break; + case PHONE: + // Regex for common phone number formats, allows +, -, (, ), space and digits, min 10 digits + if (!input.matches("^[0-9+\\-\\s()]+$") || input.length() < 10) { + showValidationAlert("Format nomor HP tidak valid (minimal 10 digit)"); + return false; + } + break; + } + return true; + } + + private void showValidationAlert(String message) { + new CustomAlertDialog.Builder(this) + .setTitle("Input Tidak Valid") + .setMessage(message) + .setAlertType(CustomAlertDialog.AlertType.WARNING) + .setPositiveButton("OK", new CustomAlertDialog.OnAlertClickListener() { + @Override + public void onPositiveClick() { + etLoginInput.requestFocus(); + } + + @Override + public void onNegativeClick() { + // Not used + } + }) + .show(); + + inputLayout.setError(message); + } + + private void setLoadingState(boolean loading) { + isLoading = loading; + btnLogin.setEnabled(!loading); + + if (loading) { + btnLogin.setText("MEMUAT..."); + ObjectAnimator pulseAnimator = ObjectAnimator.ofFloat(btnLogin, "alpha", 1f, 0.7f, 1f); + pulseAnimator.setDuration(800); + pulseAnimator.setRepeatCount(ValueAnimator.INFINITE); + pulseAnimator.start(); + } else { + btnLogin.setText("MASUK"); + btnLogin.clearAnimation(); + btnLogin.setAlpha(1f); + } + } + + private void showSuccessAlert(String title, String message) { + new CustomAlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setAlertType(CustomAlertDialog.AlertType.SUCCESS) + .setPositiveButton("OK", null) + .show(); + } + + private void showErrorAlert(String title, String message) { + new CustomAlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setAlertType(CustomAlertDialog.AlertType.ERROR) + .setPositiveButton("OK", null) + .show(); + } + + private void showWarningAlert(String title, String message) { + new CustomAlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setAlertType(CustomAlertDialog.AlertType.WARNING) + .setPositiveButton("OK", null) + .show(); + } + + private void showInfoAlert(String title, String message) { + new CustomAlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setAlertType(CustomAlertDialog.AlertType.INFO) + .setPositiveButton("OK", null) + .show(); + } + + @Override + public void onBackPressed() { + new CustomAlertDialog.Builder(this) + .setTitle("Keluar Aplikasi") + .setMessage("Apakah Anda yakin ingin keluar dari MoniKids?") + .setAlertType(CustomAlertDialog.AlertType.INFO) + .setPositiveButton("KELUAR", new CustomAlertDialog.OnAlertClickListener() { + @Override + public void onPositiveClick() { + LoginActivity.super.onBackPressed(); + } + + @Override + public void onNegativeClick() { + // Not used + } + }) + .setNegativeButton("BATAL", null) + .show(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (btnLogin != null) { + btnLogin.clearAnimation(); + } + if (requestQueue != null) { + requestQueue.cancelAll(TAG); + } + } +} \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/button_press.xml b/APLIKASI/app/src/main/res/anim/button_press.xml new file mode 100644 index 0000000..2a5d4c8 --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/button_press.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/button_select.xml b/APLIKASI/app/src/main/res/anim/button_select.xml new file mode 100644 index 0000000..f5ec4ef --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/button_select.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/fade_in.xml b/APLIKASI/app/src/main/res/anim/fade_in.xml new file mode 100644 index 0000000..20c1a8f --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/loading_pulse.xml b/APLIKASI/app/src/main/res/anim/loading_pulse.xml new file mode 100644 index 0000000..18b003e --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/loading_pulse.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/shake.xml b/APLIKASI/app/src/main/res/anim/shake.xml new file mode 100644 index 0000000..abfd60c --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/shake.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/shake_interpolator.xml b/APLIKASI/app/src/main/res/anim/shake_interpolator.xml new file mode 100644 index 0000000..4bfb143 --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/shake_interpolator.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/slide_in_right.xml b/APLIKASI/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..b85c08e --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/slide_out_left.xml b/APLIKASI/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..c68f6f5 --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/slide_up.xml b/APLIKASI/app/src/main/res/anim/slide_up.xml new file mode 100644 index 0000000..2811cf0 --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/slide_up.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/anim/success_bounce.xml b/APLIKASI/app/src/main/res/anim/success_bounce.xml new file mode 100644 index 0000000..7f34067 --- /dev/null +++ b/APLIKASI/app/src/main/res/anim/success_bounce.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/alert_button_negative.xml b/APLIKASI/app/src/main/res/drawable/alert_button_negative.xml new file mode 100644 index 0000000..a054034 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/alert_button_negative.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/alert_button_positive.xml b/APLIKASI/app/src/main/res/drawable/alert_button_positive.xml new file mode 100644 index 0000000..ad7a8a1 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/alert_button_positive.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/alert_dialog_background.xml b/APLIKASI/app/src/main/res/drawable/alert_dialog_background.xml new file mode 100644 index 0000000..7ec427d --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/alert_dialog_background.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/alert_icon_background.xml b/APLIKASI/app/src/main/res/drawable/alert_icon_background.xml new file mode 100644 index 0000000..fd487d5 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/alert_icon_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/back_button_background.xml b/APLIKASI/app/src/main/res/drawable/back_button_background.xml new file mode 100644 index 0000000..8c3c57c --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/back_button_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/bg_bottom_accent_line.xml b/APLIKASI/app/src/main/res/drawable/bg_bottom_accent_line.xml new file mode 100644 index 0000000..bd211b7 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/bg_bottom_accent_line.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/bg_user_avatar_gradient.xml b/APLIKASI/app/src/main/res/drawable/bg_user_avatar_gradient.xml new file mode 100644 index 0000000..f19dd5c --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/bg_user_avatar_gradient.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/button_group_background.xml b/APLIKASI/app/src/main/res/drawable/button_group_background.xml new file mode 100644 index 0000000..8ba2305 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/button_group_background.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/current_ip_background.xml b/APLIKASI/app/src/main/res/drawable/current_ip_background.xml new file mode 100644 index 0000000..99660f0 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/current_ip_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/entry_dot_active.xml b/APLIKASI/app/src/main/res/drawable/entry_dot_active.xml new file mode 100644 index 0000000..8ad1728 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/entry_dot_active.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/entry_dot_glow.xml b/APLIKASI/app/src/main/res/drawable/entry_dot_glow.xml new file mode 100644 index 0000000..ed5739f --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/entry_dot_glow.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/entry_dot_highlight.xml b/APLIKASI/app/src/main/res/drawable/entry_dot_highlight.xml new file mode 100644 index 0000000..b05a87f --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/entry_dot_highlight.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/entry_dot_inactive.xml b/APLIKASI/app/src/main/res/drawable/entry_dot_inactive.xml new file mode 100644 index 0000000..08f0ba9 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/entry_dot_inactive.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/entry_dot_inactive_ring.xml b/APLIKASI/app/src/main/res/drawable/entry_dot_inactive_ring.xml new file mode 100644 index 0000000..b686c71 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/entry_dot_inactive_ring.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/entry_dot_placeholder.xml b/APLIKASI/app/src/main/res/drawable/entry_dot_placeholder.xml new file mode 100644 index 0000000..8fb16c1 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/entry_dot_placeholder.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/entry_dot_placeholder_ring.xml b/APLIKASI/app/src/main/res/drawable/entry_dot_placeholder_ring.xml new file mode 100644 index 0000000..cc8abdb --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/entry_dot_placeholder_ring.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/entry_dot_pulse_ring.xml b/APLIKASI/app/src/main/res/drawable/entry_dot_pulse_ring.xml new file mode 100644 index 0000000..8ee651d --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/entry_dot_pulse_ring.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/gradient_background.xml b/APLIKASI/app/src/main/res/drawable/gradient_background.xml new file mode 100644 index 0000000..6f6f019 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/gradient_background.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/header_background.xml b/APLIKASI/app/src/main/res/drawable/header_background.xml new file mode 100644 index 0000000..6ddd8f2 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/header_background.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/help_section_background.xml b/APLIKASI/app/src/main/res/drawable/help_section_background.xml new file mode 100644 index 0000000..5548503 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/help_section_background.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ic_arrow_left.xml b/APLIKASI/app/src/main/res/drawable/ic_arrow_left.xml new file mode 100644 index 0000000..9acfe27 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_arrow_left.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_arrow_right.xml b/APLIKASI/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000..5f02d3d --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_band.xml b/APLIKASI/app/src/main/res/drawable/ic_band.xml new file mode 100644 index 0000000..8a74de2 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_band.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ic_chart.xml b/APLIKASI/app/src/main/res/drawable/ic_chart.xml new file mode 100644 index 0000000..178cce0 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_chart.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_check_circle.xml b/APLIKASI/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..25f0f2e --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_child.xml b/APLIKASI/app/src/main/res/drawable/ic_child.xml new file mode 100644 index 0000000..fbebac6 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_child.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_child_care.xml b/APLIKASI/app/src/main/res/drawable/ic_child_care.xml new file mode 100644 index 0000000..1ee9771 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_child_care.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ic_clock_mini.xml b/APLIKASI/app/src/main/res/drawable/ic_clock_mini.xml new file mode 100644 index 0000000..407932f --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_clock_mini.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ic_code.xml b/APLIKASI/app/src/main/res/drawable/ic_code.xml new file mode 100644 index 0000000..de073bb --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_code.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_entrance.xml b/APLIKASI/app/src/main/res/drawable/ic_entrance.xml new file mode 100644 index 0000000..208fad2 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_entrance.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_error_circle.xml b/APLIKASI/app/src/main/res/drawable/ic_error_circle.xml new file mode 100644 index 0000000..7233869 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_error_circle.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_id_card.xml b/APLIKASI/app/src/main/res/drawable/ic_id_card.xml new file mode 100644 index 0000000..bcad906 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_id_card.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ic_info_circle.xml b/APLIKASI/app/src/main/res/drawable/ic_info_circle.xml new file mode 100644 index 0000000..a224323 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_info_circle.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_ip.xml b/APLIKASI/app/src/main/res/drawable/ic_ip.xml new file mode 100644 index 0000000..030ce26 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_ip.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_launcher_background.xml b/APLIKASI/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_launcher_foreground.xml b/APLIKASI/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ic_location_mini.xml b/APLIKASI/app/src/main/res/drawable/ic_location_mini.xml new file mode 100644 index 0000000..0b3e54c --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_location_mini.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ic_logout.xml b/APLIKASI/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..29b74cb --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,12 @@ + + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_network.xml b/APLIKASI/app/src/main/res/drawable/ic_network.xml new file mode 100644 index 0000000..71e2927 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_network.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_phone.xml b/APLIKASI/app/src/main/res/drawable/ic_phone.xml new file mode 100644 index 0000000..3603200 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_phone_mini.xml b/APLIKASI/app/src/main/res/drawable/ic_phone_mini.xml new file mode 100644 index 0000000..ddc25e4 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_phone_mini.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ic_route.xml b/APLIKASI/app/src/main/res/drawable/ic_route.xml new file mode 100644 index 0000000..2ee7985 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_route.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_settings.xml b/APLIKASI/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..35fbaba --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_test_connection.xml b/APLIKASI/app/src/main/res/drawable/ic_test_connection.xml new file mode 100644 index 0000000..aafdfe1 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_test_connection.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/ic_timer_mini.xml b/APLIKASI/app/src/main/res/drawable/ic_timer_mini.xml new file mode 100644 index 0000000..4d6a15a --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_timer_mini.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ic_warning_circle.xml b/APLIKASI/app/src/main/res/drawable/ic_warning_circle.xml new file mode 100644 index 0000000..b9a08c9 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ic_warning_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/APLIKASI/app/src/main/res/drawable/input_field_background.xml b/APLIKASI/app/src/main/res/drawable/input_field_background.xml new file mode 100644 index 0000000..da04445 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/input_field_background.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ip_info_background.xml b/APLIKASI/app/src/main/res/drawable/ip_info_background.xml new file mode 100644 index 0000000..49e0289 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ip_info_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/login_button_gradient.xml b/APLIKASI/app/src/main/res/drawable/login_button_gradient.xml new file mode 100644 index 0000000..05ff69a --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/login_button_gradient.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/login_option_selected.xml b/APLIKASI/app/src/main/res/drawable/login_option_selected.xml new file mode 100644 index 0000000..7d0bbff --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/login_option_selected.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/login_option_unselected.xml b/APLIKASI/app/src/main/res/drawable/login_option_unselected.xml new file mode 100644 index 0000000..360d0b7 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/login_option_unselected.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/logo_circle_background.xml b/APLIKASI/app/src/main/res/drawable/logo_circle_background.xml new file mode 100644 index 0000000..de875d2 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/logo_circle_background.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/logout_gradient_background.xml b/APLIKASI/app/src/main/res/drawable/logout_gradient_background.xml new file mode 100644 index 0000000..5d2f0c5 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/logout_gradient_background.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/quick_ip_button.xml b/APLIKASI/app/src/main/res/drawable/quick_ip_button.xml new file mode 100644 index 0000000..919cb24 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/quick_ip_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/ripple_effect.xml b/APLIKASI/app/src/main/res/drawable/ripple_effect.xml new file mode 100644 index 0000000..33a3f04 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/ripple_effect.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/rounded_vertical_bar_active.xml b/APLIKASI/app/src/main/res/drawable/rounded_vertical_bar_active.xml new file mode 100644 index 0000000..2621c7d --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/rounded_vertical_bar_active.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/rounded_vertical_bar_inactive.xml b/APLIKASI/app/src/main/res/drawable/rounded_vertical_bar_inactive.xml new file mode 100644 index 0000000..9a01c80 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/rounded_vertical_bar_inactive.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/settings_button_background.xml b/APLIKASI/app/src/main/res/drawable/settings_button_background.xml new file mode 100644 index 0000000..353329a --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/settings_button_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/status_dot_active.xml b/APLIKASI/app/src/main/res/drawable/status_dot_active.xml new file mode 100644 index 0000000..78f1db5 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/status_dot_active.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/status_dot_pulse.xml b/APLIKASI/app/src/main/res/drawable/status_dot_pulse.xml new file mode 100644 index 0000000..8f7ab95 --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/status_dot_pulse.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/test_button_background.xml b/APLIKASI/app/src/main/res/drawable/test_button_background.xml new file mode 100644 index 0000000..ed7b21d --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/test_button_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/drawable/test_connection_background.xml b/APLIKASI/app/src/main/res/drawable/test_connection_background.xml new file mode 100644 index 0000000..d3a352f --- /dev/null +++ b/APLIKASI/app/src/main/res/drawable/test_connection_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/APLIKASI/app/src/main/res/font/quicksand.ttf b/APLIKASI/app/src/main/res/font/quicksand.ttf new file mode 100644 index 0000000..60323ed Binary files /dev/null and b/APLIKASI/app/src/main/res/font/quicksand.ttf differ diff --git a/APLIKASI/app/src/main/res/font/quicksand_bold.ttf b/APLIKASI/app/src/main/res/font/quicksand_bold.ttf new file mode 100644 index 0000000..07d5127 Binary files /dev/null and b/APLIKASI/app/src/main/res/font/quicksand_bold.ttf differ diff --git a/APLIKASI/app/src/main/res/font/quicksand_light.ttf b/APLIKASI/app/src/main/res/font/quicksand_light.ttf new file mode 100644 index 0000000..8005310 Binary files /dev/null and b/APLIKASI/app/src/main/res/font/quicksand_light.ttf differ diff --git a/APLIKASI/app/src/main/res/font/quicksand_medium.ttf b/APLIKASI/app/src/main/res/font/quicksand_medium.ttf new file mode 100644 index 0000000..f4634cd Binary files /dev/null and b/APLIKASI/app/src/main/res/font/quicksand_medium.ttf differ diff --git a/APLIKASI/app/src/main/res/font/quicksand_semi_bold.ttf b/APLIKASI/app/src/main/res/font/quicksand_semi_bold.ttf new file mode 100644 index 0000000..52059c3 Binary files /dev/null and b/APLIKASI/app/src/main/res/font/quicksand_semi_bold.ttf differ diff --git a/APLIKASI/app/src/main/res/layout/activity_home.xml b/APLIKASI/app/src/main/res/layout/activity_home.xml new file mode 100644 index 0000000..bdbfc34 --- /dev/null +++ b/APLIKASI/app/src/main/res/layout/activity_home.xml @@ -0,0 +1,647 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/APLIKASI/app/src/main/res/layout/activity_ip_settings.xml b/APLIKASI/app/src/main/res/layout/activity_ip_settings.xml new file mode 100644 index 0000000..7c532d1 --- /dev/null +++ b/APLIKASI/app/src/main/res/layout/activity_ip_settings.xml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Kembali ke Dashboard + + + + + + + + \ No newline at end of file diff --git a/WEB-playground/pages/tambah_pengunjung.html b/WEB-playground/pages/tambah_pengunjung.html new file mode 100644 index 0000000..6404a6a --- /dev/null +++ b/WEB-playground/pages/tambah_pengunjung.html @@ -0,0 +1,129 @@ + + + + + + Tambah Pengunjung Baru + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WEB-playground/simulate_rfid_direct.php b/WEB-playground/simulate_rfid_direct.php new file mode 100644 index 0000000..06ad14e --- /dev/null +++ b/WEB-playground/simulate_rfid_direct.php @@ -0,0 +1,52 @@ +connect(); + + // Pastikan koneksi berhasil sebelum melanjutkan + if (!$conn) { + throw new Exception("Koneksi database gagal."); + } + + // Masukkan kode gelang baru ke rfid_temp + // Data LAMA TIDAK DIHAPUS, hanya ditambahkan + $queryInsert = "INSERT INTO rfid_temp (kode_gelang) VALUES (:kode_gelang)"; + $stmtInsert = $conn->prepare($queryInsert); + $stmtInsert->bindParam(':kode_gelang', $kodeGelangOtomatis); + $stmtInsert->execute(); + + echo json_encode([ + 'status' => 'success', + 'message' => 'Simulasi RFID berhasil, kode gelang telah disimpan ke tabel rfid_temp.', + 'kode_gelang_terkirim' => $kodeGelangOtomatis + ]); + +} catch (PDOException $e) { + // Tangani error database jika terjadi + http_response_code(500); // Internal Server Error + echo json_encode([ + 'status' => 'error', + 'message' => 'Gagal melakukan simulasi RFID: ' . $e->getMessage() + ]); +} catch (Exception $e) { + // Tangani error umum lainnya + http_response_code(500); + echo json_encode([ + 'status' => 'error', + 'message' => 'Terjadi kesalahan: ' . $e->getMessage() + ]); +} +?> \ No newline at end of file