commit 8a0b99550033aded264eadbad766dedd33a533eb Author: BenedictoGeraldo Date: Mon Sep 8 14:13:18 2025 +0700 deploy unifind diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.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/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..72ac6b1 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +unifind \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..1f35ade --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..7b3006b --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..bb44937 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..288b36b --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..70736c8 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Unifind + +UniFind adalah aplikasi mobile berbasis Android yang dirancang untuk membantu mahasiswa dalam melaporkan dan menemukan barang hilang di lingkungan kampus secara cepat, efisien, dan terpusat. +------------------------------------------ + +Android Version : 7.0(Nougat) + +Bahasa Pemrograman : Kotlin + +Database : Firebase diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..36fb064 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,87 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + id("com.google.gms.google-services") + id("kotlin-parcelize") +} + +android { + namespace = "com.androidprojek.unifind" + compileSdk = 35 + + defaultConfig { + applicationId = "com.androidprojek.unifind" + minSdk = 24 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.annotation) + implementation(libs.androidx.activity) + implementation(libs.firebase.auth) + implementation(libs.firebase.firestore) + implementation(libs.firebase.storage) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + implementation(platform("com.google.firebase:firebase-bom:33.12.0")) + implementation("com.google.firebase:firebase-analytics") + implementation("com.github.bumptech.glide:glide:4.16.0") + implementation ("com.google.android.gms:play-services-base:18.5.0") + implementation ("com.google.android.gms:play-services-maps:18.2.0") + implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") + // Untuk CardView dan MaterialButton + implementation("com.google.android.material:material:1.12.0") + // Untuk CircleImageView + implementation("de.hdodenhof:circleimageview:3.1.0") + // Untuk ViewPager2 + implementation("androidx.viewpager2:viewpager2:1.1.0") + // Untuk Dots Indicator + implementation("com.tbuonomo:dotsindicator:5.0") + //bentuk filter kategori di home + implementation("com.google.android.flexbox:flexbox:3.0.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.1") + // google maps + implementation("com.google.android.gms:play-services-maps:18.1.0") + // firebase realtime database + implementation("com.google.firebase:firebase-database-ktx") + //exoplayer + implementation("androidx.media3:media3-exoplayer:1.3.1") + implementation("androidx.media3:media3-ui:1.3.1") +} \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..1baa10a --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,30 @@ +{ + "project_info": { + "project_number": "918794329241", + "firebase_url": "https://unifind-56d85-default-rtdb.asia-southeast1.firebasedatabase.app", + "project_id": "unifind-56d85", + "storage_bucket": "unifind-56d85.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:918794329241:android:962f5097f661946e966c08", + "android_client_info": { + "package_name": "com.androidprojek.unifind" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCE52qhP_KZs0prS189wc2aX5x2UeXUtGQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/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/app/src/androidTest/java/com/androidprojek/unifind/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/androidprojek/unifind/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..cbc8d65 --- /dev/null +++ b/app/src/androidTest/java/com/androidprojek/unifind/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.androidprojek.unifind + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.androidprojek.unifind", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8d3f081 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..69a3add Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/androidprojek/unifind/LoginActivity.kt b/app/src/main/java/com/androidprojek/unifind/LoginActivity.kt new file mode 100644 index 0000000..9eadfec --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/LoginActivity.kt @@ -0,0 +1,120 @@ +package com.androidprojek.unifind + +import android.content.Intent +import android.os.Bundle +import android.text.InputType +import android.widget.ImageView +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.androidprojek.unifind.databinding.ActivityLoginBinding +import com.google.firebase.auth.FirebaseAuth + +class LoginActivity : AppCompatActivity() { + + lateinit var binding: ActivityLoginBinding + lateinit var auth: FirebaseAuth + private var isPasswordVisible = false // untuk toggle password + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.login)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + auth = FirebaseAuth.getInstance() + // ======================= + // TOGGLE VISIBILITY PASSWORD + // ======================= + val passwordField = binding.PasswordField + val eyeIcon = findViewById(R.id.eye_icon) // pastikan id di XML adalah eye_icon + + eyeIcon.setOnClickListener { + isPasswordVisible = !isPasswordVisible + if (isPasswordVisible) { + passwordField.inputType = + InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + eyeIcon.setImageResource(R.drawable.eye_icon) // opsional: ubah ke ikon "eye open" + } else { + passwordField.inputType = + InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + eyeIcon.setImageResource(R.drawable.eye_icon) // kembali ke ikon default + } + // Move cursor to the end + passwordField.setSelection(passwordField.text.length) + } + + // ======================= + // LOGIN BUTTON + // ======================= + binding.btnLogin.setOnClickListener{ + val nim = binding.NimField.text.toString() + val email = "$nim@mahasiswa.upnvj.ac.id" + val password = binding.PasswordField.text.toString() + + // Reset error visibility setiap kali tombol diklik + binding.nimErr.visibility = android.view.View.GONE + binding.passErr.visibility = android.view.View.GONE + + var hasError = false + + // Validasi input kosong + if (email.isEmpty()) { + binding.txtNimErr.text = "Field ini wajib diisi!" + binding.nimErr.visibility = android.view.View.VISIBLE + hasError = true + } + + if (password.isEmpty()) { + binding.txtPassErr.text = "Field ini wajib diisi!" + binding.passErr.visibility = android.view.View.VISIBLE + hasError = true + } + + if (hasError) return@setOnClickListener + + // Coba login ke Firebase + auth.signInWithEmailAndPassword(email, password) + .addOnCompleteListener(this) { task -> + if (task.isSuccessful) { + // Login berhasil + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + } else { + // Login gagal, kita cek error-nya + val errorMessage = task.exception?.message + + // Contoh handling berdasarkan isi error (bisa disesuaikan) + if (errorMessage != null) { + if (errorMessage?.contains("no user record") == true || errorMessage.contains("There is no user")) { + binding.txtNimErr.text = "NIM tidak terdaftar" + binding.nimErr.visibility = android.view.View.VISIBLE + } else if (errorMessage?.contains("password is invalid") == true) { + binding.txtPassErr.text = "Password tidak sesuai" + binding.passErr.visibility = android.view.View.VISIBLE + } else { + binding.txtPassErr.text = "Gagal masuk, periksa kembali NIM dan password" + binding.passErr.visibility = android.view.View.VISIBLE + } + } + } + } + } + + // ======================= + // NAVIGASI KE REGISTER + // ======================= + binding.tvDaftar.setOnClickListener { + val intent = Intent(this, RegisterActivity::class.java) + startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/MainActivity.kt b/app/src/main/java/com/androidprojek/unifind/MainActivity.kt new file mode 100644 index 0000000..f3068f6 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/MainActivity.kt @@ -0,0 +1,35 @@ +package com.androidprojek.unifind + +import android.os.Bundle +import com.google.android.material.bottomnavigation.BottomNavigationView +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.setupActionBarWithNavController +import androidx.navigation.ui.setupWithNavController +import com.androidprojek.unifind.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + val navView: BottomNavigationView = binding.navView + + val navController = findNavController(R.id.nav_host_fragment_activity_main) + // Passing each menu ID as a set of Ids because each + // menu should be considered as top level destinations. +// val appBarConfiguration = AppBarConfiguration( +// setOf( +// R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications, R.id.navigation_profile +// ) +// ) +// setupActionBarWithNavController(navController, appBarConfiguration) + navView.setupWithNavController(navController) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/RegisterActivity.kt b/app/src/main/java/com/androidprojek/unifind/RegisterActivity.kt new file mode 100644 index 0000000..9b61bad --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/RegisterActivity.kt @@ -0,0 +1,118 @@ +package com.androidprojek.unifind + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.androidprojek.unifind.databinding.ActivityRegisterBinding +import com.androidprojek.unifind.model.UserModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseAuthUserCollisionException +import com.google.firebase.firestore.FirebaseFirestore + +class RegisterActivity : AppCompatActivity() { + + private lateinit var binding: ActivityRegisterBinding + private lateinit var auth: FirebaseAuth + private lateinit var db: FirebaseFirestore + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityRegisterBinding.inflate(layoutInflater) + setContentView(binding.root) + + auth = FirebaseAuth.getInstance() + db = FirebaseFirestore.getInstance() + + binding.btnRegister.setOnClickListener { + registerUser() + } + } + + private fun registerUser() { + val nama = binding.etNama.text.toString().trim() + val nim = binding.etNim.text.toString().trim() + val password = binding.etPassword.text.toString().trim() + val confirmPassword = binding.etConfirmPassword.text.toString().trim() + + // Validasi input dasar + if (nama.isEmpty() || nim.isEmpty() || password.isEmpty() || confirmPassword.isEmpty()) { + Toast.makeText(this, "Semua field wajib diisi", Toast.LENGTH_SHORT).show() + return + } + if (password.length < 6) { + Toast.makeText(this, "Password minimal 6 karakter", Toast.LENGTH_SHORT).show() + return + } + if (password != confirmPassword) { + Toast.makeText(this, "Password dan konfirmasi password tidak cocok", Toast.LENGTH_SHORT).show() + return + } + + setLoading(true) + + // LANGKAH 1: Cek dulu ke Firestore apakah NIM sudah ada di collection 'users' + db.collection("users").whereEqualTo("nim", nim).get() + .addOnSuccessListener { documents -> + if (documents.isEmpty) { + // Jika hasil pencarian kosong, berarti NIM belum terdaftar. Lanjutkan pendaftaran. + createFirebaseUser(nama, nim, password) + } else { + // Jika ada dokumen yang ditemukan, berarti NIM sudah terdaftar. + setLoading(false) + Toast.makeText(this, "NIM ini sudah terdaftar.", Toast.LENGTH_LONG).show() + } + } + .addOnFailureListener { e -> + setLoading(false) + Toast.makeText(this, "Gagal memverifikasi NIM: ${e.message}", Toast.LENGTH_LONG).show() + } + } + + private fun createFirebaseUser(nama: String, nim: String, password: String) { + val email = "$nim@mahasiswa.upnvj.ac.id" + + // LANGKAH 2: Buat user di Firebase Authentication + auth.createUserWithEmailAndPassword(email, password) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + // LANGKAH 3: Simpan profil ke Firestore + saveUserProfile(uid = task.result.user!!.uid, nama = nama, nim = nim, email = email) + } else { + setLoading(false) + // Cek jenis errornya (sebagai fallback jika terjadi race condition) + if (task.exception is FirebaseAuthUserCollisionException) { + Toast.makeText(this, "NIM ini sudah terdaftar.", Toast.LENGTH_LONG).show() + } else { + Toast.makeText(this, "Gagal mendaftar: ${task.exception?.message}", Toast.LENGTH_LONG).show() + } + } + } + } + + private fun saveUserProfile(uid: String, nama: String, nim: String, email: String) { + val user = UserModel( + uid = uid, + nama = nama, + nim = nim, + email = email + ) + + // 2. Simpan objek user ke Cloud Firestore di collection "users" + db.collection("users").document(uid).set(user) + .addOnSuccessListener { + setLoading(false) + Toast.makeText(this, "Pendaftaran berhasil! Silakan login.", Toast.LENGTH_LONG).show() + finish() // Kembali ke halaman Login + } + .addOnFailureListener { e -> + setLoading(false) + Toast.makeText(this, "Gagal menyimpan profil: ${e.message}", Toast.LENGTH_LONG).show() + } + } + + private fun setLoading(isLoading: Boolean) { + binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + binding.btnRegister.isEnabled = !isLoading + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/SplashScreenActivity.kt b/app/src/main/java/com/androidprojek/unifind/SplashScreenActivity.kt new file mode 100644 index 0000000..8d6ef5f --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/SplashScreenActivity.kt @@ -0,0 +1,56 @@ +package com.androidprojek.unifind + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView + +class SplashScreenActivity : AppCompatActivity() { + + private lateinit var playerView: PlayerView + private lateinit var player: ExoPlayer + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_splash_screen) + + playerView = findViewById(R.id.playerView) + + // Inisialisasi ExoPlayer + player = ExoPlayer.Builder(this).build() + playerView.player = player + + // Ambil video dari folder raw + val videoUri = Uri.parse("android.resource://$packageName/${R.raw.splash_screen_animations}") + val mediaItem = MediaItem.fromUri(videoUri) + player.setMediaItem(mediaItem) + + // Set tidak mengulang video + player.repeatMode = Player.REPEAT_MODE_OFF + + // Mulai video saat sudah siap + player.prepare() + player.play() + + // Saat video selesai, pindah ke LoginActivity + player.addListener(object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_ENDED) { + val intent = Intent(this@SplashScreenActivity, LoginActivity::class.java) + startActivity(intent) + player.release() + finish() + } + } + }) + } + + override fun onStop() { + super.onStop() + player.release() + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/BarangAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/BarangAdapter.kt new file mode 100644 index 0000000..7fc5be6 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/BarangAdapter.kt @@ -0,0 +1,143 @@ +package com.androidprojek.unifind.adapter + +import android.content.Intent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 +import com.androidprojek.unifind.R +import com.androidprojek.unifind.model.BarangModel +import com.androidprojek.unifind.ui.KontakPelaporActivity +import com.androidprojek.unifind.ui.LaporPenemuanActivity +import com.bumptech.glide.Glide +import com.google.android.material.button.MaterialButton +import com.tbuonomo.viewpagerdotsindicator.DotsIndicator +import de.hdodenhof.circleimageview.CircleImageView + +class BarangAdapter(private val listBarang: List) : + RecyclerView.Adapter() { + + inner class BarangViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + // Header + val ivFotoProfil: CircleImageView = itemView.findViewById(R.id.ivFotoProfil) + val tvNamaPelapor: TextView = itemView.findViewById(R.id.tvNamaPelapor) + val tvNimPelapor: TextView = itemView.findViewById(R.id.tvNimPelapor) + val ivToggleDetail: ImageView = itemView.findViewById(R.id.ivToggleDetail) + + // Gambar + val viewPagerGambarBarang: ViewPager2 = itemView.findViewById(R.id.viewPagerGambarBarang) + val dotsIndicator: DotsIndicator = itemView.findViewById(R.id.dotsIndicator) + + // Info Utama + val tvNamaBarang: TextView = itemView.findViewById(R.id.tvNamaBarang) + val tvStatus: TextView = itemView.findViewById(R.id.tvStatus) + + // Detail Tersembunyi + val layoutDetail: LinearLayout = itemView.findViewById(R.id.layoutDetail) + val tvDetailKategori: TextView = itemView.findViewById(R.id.tvDetailKategori) + val tvDetailDeskripsi: TextView = itemView.findViewById(R.id.tvDetailDeskripsi) + val tvDetailTanggal: TextView = itemView.findViewById(R.id.tvDetailTanggal) + val tvDetailWaktu: TextView = itemView.findViewById(R.id.tvDetailWaktu) + val tvDetailLokasi: TextView = itemView.findViewById(R.id.tvDetailLokasi) + + // Tombol + val btnDetail: MaterialButton = itemView.findViewById(R.id.btnDetail) + val btnAksiUtama: MaterialButton = itemView.findViewById(R.id.btnAksiUtama) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BarangViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_barang, parent, false) + return BarangViewHolder(view) + } + + override fun onBindViewHolder(holder: BarangViewHolder, position: Int) { + val barang = listBarang[position] + + // 1. Set data header + holder.tvNamaPelapor.text = barang.nama + holder.tvNimPelapor.text = barang.nim + + // --- PERUBAHAN UTAMA DI SINI --- + // Logika Baru untuk Foto Profil + if (barang.pelaporPhotoUrl.isNotEmpty()) { + // Jika ada URL foto profil, muat dari URL tersebut + Glide.with(holder.itemView.context) + .load(barang.pelaporPhotoUrl) + .placeholder(R.drawable.baseline_person_outline_24) // Opsional: gambar saat loading + .error(R.drawable.baseline_person_outline_24) // Tampil jika URL error/kosong + .into(holder.ivFotoProfil) + } else { + // Jika tidak ada URL, tampilkan gambar default secara eksplisit + holder.ivFotoProfil.setImageResource(R.drawable.baseline_person_outline_24) + } + // --------------------------------- + + // 2. Set data info utama + holder.tvNamaBarang.text = barang.namaBarang + holder.tvStatus.text = barang.status + + // 3. Set data untuk detail tersembunyi + holder.tvDetailKategori.text = barang.kategori + holder.tvDetailDeskripsi.text = barang.deskripsi + holder.tvDetailTanggal.text = barang.tanggalHilang + holder.tvDetailWaktu.text = barang.waktuHilang + holder.tvDetailLokasi.text = barang.lokasiHilang + + // 4. Setup Image Slider + if (barang.fotoUris.isNotEmpty()) { + holder.viewPagerGambarBarang.adapter = ImageSliderAdapter(barang.fotoUris) + holder.dotsIndicator.attachTo(holder.viewPagerGambarBarang) + holder.viewPagerGambarBarang.visibility = View.VISIBLE + holder.dotsIndicator.visibility = View.VISIBLE + } else { + holder.viewPagerGambarBarang.visibility = View.GONE + holder.dotsIndicator.visibility = View.GONE + } + + // 5. Implementasi expand/collapse detail + holder.ivToggleDetail.setOnClickListener { + val isVisible = holder.layoutDetail.visibility == View.VISIBLE + if (isVisible) { + holder.layoutDetail.visibility = View.GONE + holder.ivToggleDetail.animate().rotation(0f).setDuration(200).start() + } else { + holder.layoutDetail.visibility = View.VISIBLE + holder.ivToggleDetail.animate().rotation(180f).setDuration(200).start() + } + } + + // 6. Set listener untuk tombol + holder.btnDetail.setOnClickListener { + // === PERUBAHAN DI SINI === + val context = holder.itemView.context + // Buat intent untuk membuka KontakPelaporActivity + val intent = Intent(context, KontakPelaporActivity::class.java).apply { + // Kirim data kontak pelapor ke activity baru + putExtra(KontakPelaporActivity.EXTRA_INSTAGRAM, barang.pelaporInstagram) + putExtra(KontakPelaporActivity.EXTRA_LINE, barang.pelaporLine) + putExtra(KontakPelaporActivity.EXTRA_WHATSAPP, barang.pelaporWhatsapp) + } + context.startActivity(intent) + // ========================= + } + holder.btnAksiUtama.setOnClickListener { + val context = holder.itemView.context + val intent = Intent(context, LaporPenemuanActivity::class.java).apply { + // --- PERUBAHAN DI SINI: KIRIM NAMA PELAPOR JUGA --- + putExtra(LaporPenemuanActivity.EXTRA_BARANG_ID, barang.id) + putExtra(LaporPenemuanActivity.EXTRA_PELAPOR_UID, barang.pelaporUid) + putExtra(LaporPenemuanActivity.EXTRA_NAMA_BARANG, barang.namaBarang) + putExtra(LaporPenemuanActivity.EXTRA_KATEGORI, barang.kategori) + putExtra(LaporPenemuanActivity.EXTRA_NAMA_PELAPOR, barang.nama) // <-- KIRIM NAMA + } + context.startActivity(intent) + } + } + + override fun getItemCount(): Int = listBarang.size +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/ImageSliderAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/ImageSliderAdapter.kt new file mode 100644 index 0000000..0cbef2c --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/ImageSliderAdapter.kt @@ -0,0 +1,27 @@ +package com.androidprojek.unifind.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.androidprojek.unifind.databinding.ItemGambarSliderBinding // Buat layout XML ini +import com.bumptech.glide.Glide + +class ImageSliderAdapter(private val imageUrls: List) : RecyclerView.Adapter() { + + inner class ImageViewHolder(val binding: ItemGambarSliderBinding) : RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { + val binding = ItemGambarSliderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ImageViewHolder(binding) + } + + override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { + val imageUrl = imageUrls[position] + Glide.with(holder.itemView.context) + .load(imageUrl) + .centerCrop() + .into(holder.binding.ivGambarSlider) + } + + override fun getItemCount(): Int = imageUrls.size +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/KategoriFilterAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/KategoriFilterAdapter.kt new file mode 100644 index 0000000..dfe15cf --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/KategoriFilterAdapter.kt @@ -0,0 +1,35 @@ +package com.androidprojek.unifind.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.androidprojek.unifind.databinding.ItemKategoriFilterBinding +import com.androidprojek.unifind.model.KategoriModel // Pastikan ini mengimpor dari package model + +class KategoriFilterAdapter( + private val listKategori: List, + private val onCategoryClick: (KategoriModel) -> Unit +) : RecyclerView.Adapter() { + + inner class ViewHolder(val binding: ItemKategoriFilterBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(kategori: KategoriModel) { + binding.chipKategori.text = kategori.nama + binding.chipKategori.isChecked = kategori.isSelected + + binding.chipKategori.setOnClickListener { + onCategoryClick(kategori) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemKategoriFilterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(listKategori[position]) + } + + override fun getItemCount(): Int = listKategori.size +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/LacakFormulirAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/LacakFormulirAdapter.kt new file mode 100644 index 0000000..8f6c5c8 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/LacakFormulirAdapter.kt @@ -0,0 +1,65 @@ +package com.androidprojek.unifind.adapter + +import android.content.res.ColorStateList +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.PenemuanItemLacakFormulirBinding +import com.androidprojek.unifind.model.PenemuanLacakFormulirModel +import com.bumptech.glide.Glide + +class LacakFormulirAdapter( + private var listLacak: List +) : RecyclerView.Adapter() { + + inner class ViewHolder(private val binding: PenemuanItemLacakFormulirBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: PenemuanLacakFormulirModel) { + binding.tvLacakNamaBarang.text = item.namaBarangPostingan + binding.tvLacakNamaPenemu.text = "Ditemukan oleh: ${item.namaPenemu}" + binding.chipLacakStatus.text = item.statusKlaim + + Glide.with(itemView.context) + .load(item.imageUrlPostingan) + .placeholder(R.drawable.baseline_image_24) + .into(binding.ivLacakFotoBarang) + + // --- LOGIKA UNTUK MENGUBAH WARNA STATUS CHIP --- + val context = itemView.context + when (item.statusKlaim) { + "Diterima" -> { + // Tampilan untuk status DITERIMA + binding.chipLacakStatus.chipBackgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.status_diterima_bg)) + binding.chipLacakStatus.setTextColor(ContextCompat.getColor(context, R.color.status_text_color)) + } + "Ditolak" -> { + // Tampilan untuk status DITOLAK + binding.chipLacakStatus.chipBackgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.status_ditolak_bg)) + binding.chipLacakStatus.setTextColor(ContextCompat.getColor(context, R.color.status_text_color)) + } + else -> { // Default untuk "Menunggu Konfirmasi" + // Tampilan DEFAULT + binding.chipLacakStatus.chipBackgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.grey_pending_bg)) + binding.chipLacakStatus.setTextColor(ContextCompat.getColor(context, R.color.grey_pending_text)) + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = PenemuanItemLacakFormulirBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(listLacak[position]) + } + + override fun getItemCount(): Int = listLacak.size + + fun updateData(newList: List) { + listLacak = newList + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/LaporanMasukAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/LaporanMasukAdapter.kt new file mode 100644 index 0000000..e88ba62 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/LaporanMasukAdapter.kt @@ -0,0 +1,69 @@ +package com.androidprojek.unifind.adapter + +import android.content.res.ColorStateList +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.ItemLaporanMasukBinding +import com.androidprojek.unifind.model.LaporanPenemuanModel +import com.bumptech.glide.Glide + +class LaporanMasukAdapter(private val listLaporan: List) : + RecyclerView.Adapter() { + + var onDetailClickListener: ((LaporanPenemuanModel) -> Unit)? = null + var onHubungiClickListener: ((LaporanPenemuanModel) -> Unit)? = null + + inner class LaporanViewHolder(val binding: ItemLaporanMasukBinding) : RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LaporanViewHolder { + val binding = ItemLaporanMasukBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return LaporanViewHolder(binding) + } + + override fun onBindViewHolder(holder: LaporanViewHolder, position: Int) { + val laporan = listLaporan[position] + holder.binding.apply { + // Mengikat data ke TextViews + tvNamaPenemu.text = laporan.penemuNama + tvNimPenemu.text = laporan.penemuNim + + // Memuat foto profil penemu ke CircleImageView + Glide.with(holder.itemView.context) + .load(laporan.penemuPhotoUrl) + .placeholder(R.drawable.baseline_person_outline_24) + .error(R.drawable.baseline_person_outline_24) + .into(ivPenemuProfile) + + // Mengikat data ke Chip status + chipStatusLaporan.text = laporan.statusLaporan + val context = holder.itemView.context + when (laporan.statusLaporan) { + "Disetujui" -> { + chipStatusLaporan.chipBackgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.green_100)) + chipStatusLaporan.setTextColor(ContextCompat.getColor(context, R.color.green_700)) + } + "Ditolak" -> { + chipStatusLaporan.chipBackgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.red_100)) + chipStatusLaporan.setTextColor(ContextCompat.getColor(context, R.color.red_700)) + } + else -> { // Menunggu Verifikasi + chipStatusLaporan.chipBackgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.blue_100)) + chipStatusLaporan.setTextColor(ContextCompat.getColor(context, R.color.blue_700)) + } + } + + // Menghubungkan listener ke tombol yang benar + btnLihatDetailLaporan.setOnClickListener { + onDetailClickListener?.invoke(laporan) + } + btnHubungiPenemu.setOnClickListener { + onHubungiClickListener?.invoke(laporan) + } + } + } + + override fun getItemCount(): Int = listLaporan.size +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/NotificationAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/NotificationAdapter.kt new file mode 100644 index 0000000..9de29a5 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/NotificationAdapter.kt @@ -0,0 +1,46 @@ +package com.androidprojek.unifind.adapter + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.androidprojek.unifind.R +import com.androidprojek.unifind.model.NotificationModel + +// Ubah parameter menjadi var agar bisa diubah +class NotificationAdapter(private var notificationList: List) : + RecyclerView.Adapter() { + + class NotificationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val icon: ImageView = itemView.findViewById(R.id.iv_notification_icon) + val message: TextView = itemView.findViewById(R.id.tv_notification_message) + val timestamp: TextView = itemView.findViewById(R.id.tv_notification_timestamp) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_notification, parent, false) + return NotificationViewHolder(view) + } + + override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) { + val notification = notificationList[position] + holder.icon.setImageResource(notification.iconResId) + holder.message.text = notification.message + holder.timestamp.text = notification.timestamp + } + + override fun getItemCount(): Int { + return notificationList.size + } + + // <-- FUNGSI BARU UNTUK UPDATE DATA + @SuppressLint("NotifyDataSetChanged") + fun updateData(newNotificationList: List) { + this.notificationList = newNotificationList + notifyDataSetChanged() // Perintahkan RecyclerView untuk menggambar ulang dirinya + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/PencarianLacakAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/PencarianLacakAdapter.kt new file mode 100644 index 0000000..b73f93c --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/PencarianLacakAdapter.kt @@ -0,0 +1,63 @@ +package com.androidprojek.unifind.adapter + +import android.content.res.ColorStateList +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.ItemLacakFormulirPencarianBinding +import com.androidprojek.unifind.model.PencarianLacakFormulirModel +import com.bumptech.glide.Glide + +class PencarianLacakAdapter(private var listLacak: List) : + RecyclerView.Adapter() { + + inner class LacakViewHolder(val binding: ItemLacakFormulirPencarianBinding) : RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LacakViewHolder { + val binding = ItemLacakFormulirPencarianBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return LacakViewHolder(binding) + } + + override fun onBindViewHolder(holder: LacakViewHolder, position: Int) { + val item = listLacak[position] + holder.binding.apply { + // Mengisi data teks sesuai format baru + tvLacakNamaBarang.text = item.namaBarang + tvLacakNamaPoster.text = "Barang Hilang Milik: ${item.namaPoster}" + + // Memuat gambar barang menggunakan Glide + Glide.with(holder.itemView.context) + .load(item.imageUrlPostingan) + .placeholder(R.drawable.baseline_image_24) // Gambar default saat loading + .error(R.drawable.baseline_image_24) // Gambar default jika ada error + .into(ivLacakFotoBarang) + + // Mengatur teks dan warna Chip status + chipLacakStatus.text = item.statusLaporan + val context = holder.itemView.context + when (item.statusLaporan) { + "Disetujui" -> { + chipLacakStatus.chipBackgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.green_100)) + chipLacakStatus.setTextColor(ContextCompat.getColor(context, R.color.green_700)) + } + "Ditolak" -> { + chipLacakStatus.chipBackgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.red_100)) + chipLacakStatus.setTextColor(ContextCompat.getColor(context, R.color.red_700)) + } + else -> { // Menunggu Verifikasi + chipLacakStatus.chipBackgroundColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.blue_100)) + chipLacakStatus.setTextColor(ContextCompat.getColor(context, R.color.blue_700)) + } + } + } + } + + override fun getItemCount(): Int = listLacak.size + + fun updateData(newList: List) { + listLacak = newList + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/PencarianPostinganSayaAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/PencarianPostinganSayaAdapter.kt new file mode 100644 index 0000000..0cd254e --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/PencarianPostinganSayaAdapter.kt @@ -0,0 +1,80 @@ +package com.androidprojek.unifind.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView +import com.androidprojek.unifind.databinding.ItemPencarianPostinganSayaBinding // <-- Import View Binding +import com.androidprojek.unifind.model.BarangModel +import com.bumptech.glide.Glide + +class PencarianPostinganSayaAdapter(private val listBarang: List) : + RecyclerView.Adapter() { + + // Listener untuk menangani klik tombol + var onLihatLaporanClickListener: ((BarangModel) -> Unit)? = null + + // ViewHolder sekarang menggunakan View Binding, lebih bersih! + inner class PencarianViewHolder(val binding: ItemPencarianPostinganSayaBinding) : + RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PencarianViewHolder { + // Inflate layout menggunakan View Binding + val binding = ItemPencarianPostinganSayaBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return PencarianViewHolder(binding) + } + + override fun onBindViewHolder(holder: PencarianViewHolder, position: Int) { + val barang = listBarang[position] + + // Kita akan binding data menggunakan 'holder.binding' + holder.binding.apply { + // Info Utama + tvNamaBarang.text = barang.namaBarang + tvStatus.text = barang.status + + // Detail Tersembunyi + tvDetailKategori.text = barang.kategori + tvDetailDeskripsi.text = barang.deskripsi + tvDetailTanggal.text = barang.tanggalHilang + tvDetailWaktu.text = barang.waktuHilang + tvDetailLokasi.text = barang.lokasiHilang + + // Setup Image Slider (logika sama seperti BarangAdapter) + if (barang.fotoUris.isNotEmpty()) { + viewPagerGambarBarang.adapter = ImageSliderAdapter(barang.fotoUris) + dotsIndicator.attachTo(viewPagerGambarBarang) + viewPagerGambarBarang.visibility = View.VISIBLE + dotsIndicator.visibility = View.VISIBLE + } else { + viewPagerGambarBarang.visibility = View.GONE + dotsIndicator.visibility = View.GONE + } + + // Implementasi expand/collapse detail (logika sama seperti BarangAdapter) + ivToggleDetail.setOnClickListener { + val isVisible = layoutDetail.visibility == View.VISIBLE + if (isVisible) { + layoutDetail.visibility = View.GONE + ivToggleDetail.animate().rotation(0f).setDuration(200).start() + } else { + layoutDetail.visibility = View.VISIBLE + ivToggleDetail.animate().rotation(180f).setDuration(200).start() + } + } + + // Set listener untuk tombol "Lihat Laporan Penemuan" + btnLihatLaporan.setOnClickListener { + // Panggil listener yang akan diimplementasikan di Fragment + onLihatLaporanClickListener?.invoke(barang) + } + } + } + + override fun getItemCount(): Int = listBarang.size +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/PenemuanAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/PenemuanAdapter.kt new file mode 100644 index 0000000..b909d90 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/PenemuanAdapter.kt @@ -0,0 +1,121 @@ +package com.androidprojek.unifind.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import androidx.transition.TransitionManager +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.ItemPenemuanBinding +import com.androidprojek.unifind.model.PenemuanModel +import com.bumptech.glide.Glide + +// --- 1. UBAH INTERFACE --- +// Tambahkan fungsi baru untuk menangani klik "Verifikasi" +interface OnItemClickListener { + fun onKlaimClick(postId: String) + fun onVerifikasiClick(postId: String) // Fungsi baru +} + +// --- 2. UBAH KONSTRUKTOR ADAPTER --- +// Tambahkan 'isMyPostsPage' untuk menandai konteks halaman. +class PenemuanAdapter( + private var listPenemuan: MutableList, + private val listener: OnItemClickListener, + private val isMyPostsPage: Boolean = false // Defaultnya false (untuk halaman Home) +) : RecyclerView.Adapter() { + + inner class ViewHolder(private val binding: ItemPenemuanBinding) : RecyclerView.ViewHolder(binding.root) { + + fun bind(penemuan: PenemuanModel) { + // Bagian ini tidak ada perubahan + binding.tvNamaPenemu.text = penemuan.namaPelapor + binding.tvNimPenemu.text = penemuan.nim + binding.tvNamaBarang.text = penemuan.namaBarang + binding.tvStatus.text = "Dalam Pencarian" + binding.ivFotoProfil.setImageResource(R.drawable.ic_launcher_background) + penemuan.imageUrl?.let { url -> + if (url.isNotEmpty()) { + val imageUrlList = listOf(url) + val imageAdapter = ImageSliderAdapter(imageUrlList) + binding.viewPagerGambarBarang.adapter = imageAdapter + binding.dotsIndicator.visibility = View.GONE + binding.viewPagerGambarBarang.visibility = View.VISIBLE + } else { + binding.viewPagerGambarBarang.visibility = View.GONE + binding.dotsIndicator.visibility = View.GONE + } + } ?: run { + binding.viewPagerGambarBarang.visibility = View.GONE + binding.dotsIndicator.visibility = View.GONE + } + binding.tvDetailNamaBarang.text = penemuan.namaBarang + binding.tvDetailKategori.text = penemuan.kategori + binding.tvDetailDeskripsi.text = penemuan.deskripsi + binding.tvDetailTanggal.text = penemuan.tanggalPenemuan + binding.tvDetailWaktu.text = penemuan.waktuPenemuan + binding.tvDetailLokasi.text = penemuan.lokasiPenemuan + + setupToggleButton(binding, penemuan) + } + + private fun setupToggleButton(binding: ItemPenemuanBinding, penemuan: PenemuanModel) { + binding.btnKiri.text = "Detail Barang" + + // --- 3. LOGIKA KONDISIONAL UNTUK TOMBOL --- + if (isMyPostsPage) { + // Jika ini adalah halaman "Postingan Saya" + binding.btnKanan.text = "Verifikasi" + binding.btnKanan.setOnClickListener { + penemuan.id?.let { postId -> + listener.onVerifikasiClick(postId) + } + } + } else { + // Jika ini adalah halaman Home (Beranda) + binding.btnKanan.text = "Klaim Barang" + binding.btnKanan.setOnClickListener { + penemuan.id?.let { postId -> + listener.onKlaimClick(postId) + } + } + } + // --- SELESAI PERUBAHAN LOGIKA TOMBOL --- + + binding.ivToggleDetail.setOnClickListener { + val isVisible = binding.layoutDetail.visibility == View.VISIBLE + TransitionManager.beginDelayedTransition(binding.root as ViewGroup) + if (isVisible) { + binding.layoutDetail.visibility = View.GONE + binding.ivToggleDetail.setImageResource(R.drawable.ic_arrow_down) + binding.btnKiri.text = "Detail Barang" + } else { + binding.layoutDetail.visibility = View.VISIBLE + binding.ivToggleDetail.setImageResource(R.drawable.ic_arrow_up) + binding.btnKiri.text = "Tutup Detail" + } + } + + binding.btnKiri.setOnClickListener { + binding.ivToggleDetail.performClick() + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemPenemuanBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(listPenemuan[position]) + } + + override fun getItemCount(): Int = listPenemuan.size + + fun updateData(newList: List) { + listPenemuan.clear() + listPenemuan.addAll(newList) + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/PenemuanPengklaimAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/PenemuanPengklaimAdapter.kt new file mode 100644 index 0000000..27979fa --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/PenemuanPengklaimAdapter.kt @@ -0,0 +1,83 @@ +package com.androidprojek.unifind.adapter + +import android.content.res.ColorStateList +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.ItemVerifikasiPemilikBinding +import com.androidprojek.unifind.model.PenemuanKlaimModel + +// --- PERUBAHAN UTAMA DI KONSTRUKTOR --- +// Kita tidak lagi menggunakan interface, tapi langsung menerima dua fungsi (lambda). +class PenemuanPengklaimAdapter( + private val listKlaim: List, + private val onLihatJawaban: (PenemuanKlaimModel) -> Unit, + private val onKontak: (PenemuanKlaimModel) -> Unit +) : RecyclerView.Adapter() { + + inner class ViewHolder(private val binding: ItemVerifikasiPemilikBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(klaim: PenemuanKlaimModel) { + binding.tvPengklaimNama.text = klaim.namaPengklaim + binding.tvPengklaimNim.text = klaim.nimPengklaim + binding.chipStatusVerifikasi.text = klaim.statusKlaim + + // --- LOGIKA UNTUK MENGUBAH WARNA BERDASARKAN STATUS --- + val context = itemView.context + when (klaim.statusKlaim) { + "Diterima" -> { + // Tampilan untuk status DITERIMA + binding.chipStatusVerifikasi.setChipBackgroundColorResource(R.color.status_diterima_bg) + binding.chipStatusVerifikasi.setTextColor(ContextCompat.getColor(context, R.color.status_text_color)) + + binding.btnKontakPengklaim.isEnabled = true + binding.btnKontakPengklaim.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.kontak_diterima_bg)) + binding.btnKontakPengklaim.setTextColor(ContextCompat.getColor(context, R.color.status_text_color)) + binding.btnKontakPengklaim.setIconTintResource(R.color.status_text_color) + } + "Ditolak" -> { + // Tampilan untuk status DITOLAK + binding.chipStatusVerifikasi.setChipBackgroundColorResource(R.color.status_ditolak_bg) + binding.chipStatusVerifikasi.setTextColor(ContextCompat.getColor(context, R.color.status_text_color)) + + binding.btnKontakPengklaim.isEnabled = false + binding.btnKontakPengklaim.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.kontak_ditolak_bg)) + binding.btnKontakPengklaim.setTextColor(ContextCompat.getColor(context, R.color.status_text_color)) + binding.btnKontakPengklaim.setIconTintResource(R.color.status_text_color) + } + else -> { // Default untuk "Menunggu Konfirmasi" + // Tampilan DEFAULT + binding.chipStatusVerifikasi.setChipBackgroundColorResource(R.color.grey_pending_bg) + binding.chipStatusVerifikasi.setTextColor(ContextCompat.getColor(context, R.color.grey_pending_text)) + + binding.btnKontakPengklaim.isEnabled = false + binding.btnKontakPengklaim.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.kontak_ditolak_bg)) + binding.btnKontakPengklaim.setTextColor(ContextCompat.getColor(context, R.color.status_text_color)) + binding.btnKontakPengklaim.setIconTintResource(R.color.status_text_color) + } + } + + // Panggil lambda secara langsung saat tombol diklik + binding.btnLihatJawaban.setOnClickListener { + onLihatJawaban(klaim) + } + binding.btnKontakPengklaim.setOnClickListener { + if (it.isEnabled) { + onKontak(klaim) + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemVerifikasiPemilikBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(listKlaim[position]) + } + + override fun getItemCount(): Int = listKlaim.size +} diff --git a/app/src/main/java/com/androidprojek/unifind/adapter/TrackingAdapter.kt b/app/src/main/java/com/androidprojek/unifind/adapter/TrackingAdapter.kt new file mode 100644 index 0000000..fe8c819 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/adapter/TrackingAdapter.kt @@ -0,0 +1,111 @@ +package com.androidprojek.unifind.ui.dashboard + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.navigation.findNavController +import androidx.recyclerview.widget.RecyclerView +import com.androidprojek.unifind.R +import com.androidprojek.unifind.model.Tracking +import com.bumptech.glide.Glide + +class TrackingAdapter( + private val trackingList: List, + private val onLacakClick: (Tracking) -> Unit +) : RecyclerView.Adapter() { + + private val expandedPositions = mutableSetOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackingViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_tracking, parent, false) + return TrackingViewHolder(view) + } + + override fun onBindViewHolder(holder: TrackingViewHolder, position: Int) { + val tracking = trackingList[position] + val isExpanded = expandedPositions.contains(position) + + // === Compact view === + holder.tvNamaBarang.text = tracking.namaBarang + holder.tvKategoriBarang.text = "Kategori: ${tracking.kategoriBarang}" + if (tracking.imageUrl != null) { + Glide.with(holder.itemView.context) + .load(tracking.imageUrl) + .into(holder.ivTrackingImage) + } else { + holder.ivTrackingImage.setImageResource(R.drawable.ic_dashboard_black_24dp) + } + + // === Expanded view === + holder.tvNamaBarangExpand.text = tracking.namaBarang + holder.tvKategoriBarangExpand.text = "${tracking.kategoriBarang}" + holder.tvDeskripsi.text = "${tracking.deskripsiBarang}" + holder.idPerangkat.text = "${tracking.idPerangkat}" + if (tracking.imageUrl != null) { + Glide.with(holder.itemView.context) + .load(tracking.imageUrl) + .into(holder.ivTrackingImageExpand) + } else { + holder.ivTrackingImageExpand.setImageResource(R.drawable.ic_dashboard_black_24dp) + } + + // === Visibility toggle === + holder.compactLayout.visibility = if (isExpanded) View.GONE else View.VISIBLE + holder.expandedLayout.visibility = if (isExpanded) View.VISIBLE else View.GONE + + holder.btnDropdown.setImageResource( + if (isExpanded) R.drawable.arrow_up else R.drawable.arrow_down + ) + + // Expand/collapse behavior + holder.btnDropdown.setOnClickListener { + if (isExpanded) expandedPositions.remove(position) + else expandedPositions.add(position) + notifyItemChanged(position) + } + + // Handle "Lacak" button in both states + holder.btnLacakCompact.setOnClickListener { + val bundle = Bundle().apply { + putString("idPerangkat", tracking.idPerangkat) + } + it.findNavController().navigate(R.id.detailTrackingFragment, bundle) + } + + holder.btnLacakExpand.setOnClickListener { + val bundle = Bundle().apply { + putString("idPerangkat", tracking.idPerangkat) + } + it.findNavController().navigate(R.id.detailTrackingFragment, bundle) + } + } + + override fun getItemCount(): Int = trackingList.size + + inner class TrackingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + // Compact layout views + val compactLayout: View = itemView.findViewById(R.id.compact_layout) + val ivTrackingImage: ImageView = itemView.findViewById(R.id.iv_tracking_image) + val tvNamaBarang: TextView = itemView.findViewById(R.id.tv_nama_barang) + val tvKategoriBarang: TextView = itemView.findViewById(R.id.tv_kategori_barang) + val btnLacakCompact: View = itemView.findViewById(R.id.btn_lacak_compact) + + // Expanded layout views + val expandedLayout: View = itemView.findViewById(R.id.expanded_layout) + val tvNamaBarangExpand: TextView = itemView.findViewById(R.id.tv_nama_barang_expand) + val tvKategoriBarangExpand: TextView = itemView.findViewById(R.id.tv_kategori_barang_expand) + val ivTrackingImageExpand: ImageView = itemView.findViewById(R.id.iv_tracking_image_expand) + val tvDeskripsi: TextView = itemView.findViewById(R.id.tv_deskripsi) + val idPerangkat: TextView = itemView.findViewById(R.id.idPerangkat) + val btnLacakExpand: View = itemView.findViewById(R.id.btn_lacak_expand) + + // Dropdown button + val btnDropdown: ImageView = itemView.findViewById(R.id.btn_dropdown) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/datanotification/NotificationRepository.kt b/app/src/main/java/com/androidprojek/unifind/datanotification/NotificationRepository.kt new file mode 100644 index 0000000..e553bd3 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/datanotification/NotificationRepository.kt @@ -0,0 +1,34 @@ +package com.androidprojek.unifind.datanotification + +import androidx.lifecycle.MutableLiveData +import com.androidprojek.unifind.R +import com.androidprojek.unifind.model.NotificationModel +import java.util.Date + +/** + * Ini adalah Singleton Repository, satu-satunya sumber data notifikasi + * untuk seluruh aplikasi. + * 'object' memastikan hanya ada satu instance dari kelas ini. + */ +object NotificationRepository { + val notificationList = MutableLiveData>().apply { + // Sekarang listnya dimulai dalam keadaan benar-benar kosong. + value = mutableListOf() + } + + /** + * Fungsi terpusat untuk menambah notifikasi baru ke dalam daftar. + * Fungsi ini bisa dipanggil dari mana saja di dalam aplikasi. + * @param notification Objek NotificationModel yang akan ditambahkan. + */ + fun addNotification(notification: NotificationModel) { + // Ambil daftar yang ada saat ini, atau buat list baru jika null + val currentList = notificationList.value ?: mutableListOf() + + // Tambahkan item baru ke posisi paling atas (indeks 0) + currentList.add(0, notification) + + // Set nilai baru ke LiveData agar semua observer mendeteksi perubahan + notificationList.value = currentList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/model/BarangModel.kt b/app/src/main/java/com/androidprojek/unifind/model/BarangModel.kt new file mode 100644 index 0000000..2d4f577 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/BarangModel.kt @@ -0,0 +1,33 @@ +package com.androidprojek.unifind.model + +import android.os.Parcelable +import com.google.firebase.firestore.Exclude +import com.google.firebase.firestore.ServerTimestamp +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Parcelize +data class BarangModel( + @get:Exclude var id: String? = null, + + // --- Data Pelapor --- + val pelaporUid: String = "", + val nama: String = "", + val nim: String = "", + val pelaporPhotoUrl: String = "", // <-- FOTO PROFIL PELAPOR + val pelaporInstagram: String = "", // <-- KONTAK INSTAGRAM PELAPOR + val pelaporLine: String = "", // <-- KONTAK LINE PELAPOR + val pelaporWhatsapp: String = "", // <-- KONTAK WHATSAPP PELAPOR + + // --- Data Barang --- + val namaBarang: String = "", + val kategori: String = "", + val deskripsi: String = "", + val tanggalHilang: String = "", + val waktuHilang: String = "", + val lokasiHilang: String = "", + val fotoUris: List = emptyList(), + val status: String = "Dalam Pencarian", + + @ServerTimestamp val timestamp: Date? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/model/KategoriModel.kt b/app/src/main/java/com/androidprojek/unifind/model/KategoriModel.kt new file mode 100644 index 0000000..917fa0c --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/KategoriModel.kt @@ -0,0 +1,3 @@ +package com.androidprojek.unifind.model // Pastikan package-nya benar + +data class KategoriModel(val nama: String, var isSelected: Boolean = false) \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/model/LaporanPenemuanModel.kt b/app/src/main/java/com/androidprojek/unifind/model/LaporanPenemuanModel.kt new file mode 100644 index 0000000..f539717 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/LaporanPenemuanModel.kt @@ -0,0 +1,40 @@ +package com.androidprojek.unifind.model + +import android.os.Parcelable +import com.google.firebase.firestore.Exclude +import com.google.firebase.firestore.ServerTimestamp +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Parcelize +data class LaporanPenemuanModel( + @get:Exclude var id: String? = null, // Untuk menyimpan ID unik dari setiap laporan + + // Info Penghubung + val idBarangHilang: String = "", + val uidPelaporAsli: String = "", + + // Info Si Penemu (yang mengisi form ini) + val penemuUid: String = "", + val penemuNama: String = "", + val penemuNim: String = "", + val penemuInstagram: String? = null, + val penemuLine: String? = null, + val penemuWhatsapp: String? = null, + val penemuPhotoUrl: String? = null, + + // --- TAMBAHKAN DUA FIELD INI --- + val namaBarangPostingan: String? = null, + val kategoriPostingan: String? = null, + + // Info Laporan Penemuan + val deskripsiTambahan: String = "", + val tanggalTemuan: String = "", + val waktuTemuan: String = "", + val lokasiTemuan: String = "", + val fotoLaporanUris: List = emptyList(), // Foto dari si penemu + + // Status + val statusLaporan: String = "Menunggu Verifikasi", + @ServerTimestamp val timestamp: Date? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/model/NotificationModel.kt b/app/src/main/java/com/androidprojek/unifind/model/NotificationModel.kt new file mode 100644 index 0000000..a029114 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/NotificationModel.kt @@ -0,0 +1,19 @@ +package com.androidprojek.unifind.model + +import androidx.annotation.DrawableRes + +/** + * Data class ini merepresentasikan satu buah item di dalam daftar notifikasi. + */ +data class NotificationModel( + val id: Long, // ID unik untuk setiap notifikasi, bisa dari timestamp atau database + + @DrawableRes + val iconResId: Int, // Resource ID untuk gambar ikon (contoh: R.drawable.form) + + val message: String, // Teks utama notifikasi (contoh: "Form Verifikasi Penemu...") + + val timestamp: String, // Teks waktu (contoh: "baru saja", "1 menit yang lalu") + + val isRead: Boolean = false // Status untuk menandai notifikasi sudah dibaca atau belum +) \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/model/PencarianLacakFormulirModel.kt b/app/src/main/java/com/androidprojek/unifind/model/PencarianLacakFormulirModel.kt new file mode 100644 index 0000000..2a307be --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/PencarianLacakFormulirModel.kt @@ -0,0 +1,8 @@ +package com.androidprojek.unifind.model + +data class PencarianLacakFormulirModel( + val namaBarang: String?, + val namaPoster: String?, + val imageUrlPostingan: String?, + val statusLaporan: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/model/PenemuanKlaimModel.kt b/app/src/main/java/com/androidprojek/unifind/model/PenemuanKlaimModel.kt new file mode 100644 index 0000000..ce0a386 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/PenemuanKlaimModel.kt @@ -0,0 +1,24 @@ +package com.androidprojek.unifind.model + +import android.os.Parcelable +import com.google.firebase.firestore.Exclude +import kotlinx.parcelize.Parcelize + +// --- 1. TAMBAHKAN ANOTASI @Parcelize --- +@Parcelize +data class PenemuanKlaimModel( + @get:Exclude var id: String? = null, + val uidPengklaim: String? = null, + val namaPengklaim: String? = null, + val nimPengklaim: String? = null, + val namaBarangKlaim: String? = null, + val kategoriKlaim: String? = null, + val deskripsiKlaim: String? = null, + val tanggalHilangKlaim: String? = null, + val waktuHilangKlaim: String? = null, + val lokasiHilangKlaim: String? = null, + val imageUrlKlaim: String? = null, + val timestampKlaim: Long? = 0L, + val statusKlaim: String? = null +// --- 2. IMPLEMENTASIKAN Parcelable --- +) : Parcelable diff --git a/app/src/main/java/com/androidprojek/unifind/model/PenemuanLacakFormulirModel.kt b/app/src/main/java/com/androidprojek/unifind/model/PenemuanLacakFormulirModel.kt new file mode 100644 index 0000000..94b41d9 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/PenemuanLacakFormulirModel.kt @@ -0,0 +1,24 @@ +package com.androidprojek.unifind.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Model data ini khusus untuk halaman "Lacak Formulir". + * Ia menggabungkan informasi dari postingan asli dan klaim yang dikirim. + */ +@Parcelize +data class PenemuanLacakFormulirModel( + // Info dari postingan asli + val postId: String? = null, + val namaBarangPostingan: String? = null, + val imageUrlPostingan: String? = null, + val namaPenemu: String? = null, + + // Info dari klaim yang dikirim + val klaimId: String? = null, + val statusKlaim: String? = null, + + // Seluruh data dari formulir klaim, untuk dikirim jika "Lihat Jawaban" diklik + val detailKlaim: PenemuanKlaimModel? = null +) : Parcelable diff --git a/app/src/main/java/com/androidprojek/unifind/model/PenemuanModel.kt b/app/src/main/java/com/androidprojek/unifind/model/PenemuanModel.kt new file mode 100644 index 0000000..b796bcd --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/PenemuanModel.kt @@ -0,0 +1,25 @@ +package com.androidprojek.unifind.model + +import android.os.Parcelable +import com.google.firebase.firestore.Exclude +import kotlinx.parcelize.Parcelize + +@Parcelize +data class PenemuanModel( + // TAMBAHKAN PROPERTI INI UNTUK MENYIMPAN ID DOKUMEN DARI FIRESTORE + @get:Exclude var id: String? = null, + + // Properti lain yang sudah ada + val namaPelapor: String? = null, + val nim: String? = null, + val namaBarang: String? = null, + val kategori: String? = null, + val deskripsi: String? = null, + val tanggalPenemuan: String? = null, + val waktuPenemuan: String? = null, + val lokasiPenemuan: String? = null, + val imageUrl: String? = null, + val timestamp: Long? = 0L, + val uid: String? = null, + val status: String? = "Dalam Pencarian" +) : Parcelable diff --git a/app/src/main/java/com/androidprojek/unifind/model/Tracking.kt b/app/src/main/java/com/androidprojek/unifind/model/Tracking.kt new file mode 100644 index 0000000..fa9fa75 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/Tracking.kt @@ -0,0 +1,11 @@ +package com.androidprojek.unifind.model + +data class Tracking( + val id: String = "", + val namaBarang: String = "", + val kategoriBarang: String = "", + val deskripsiBarang: String = "", + val idPerangkat: String = "", + val imageUrl: String? = null, + val timestamp: Long = 0L +) \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/model/UserModel.kt b/app/src/main/java/com/androidprojek/unifind/model/UserModel.kt new file mode 100644 index 0000000..49629b0 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/model/UserModel.kt @@ -0,0 +1,12 @@ +package com.androidprojek.unifind.model + +data class UserModel( + val uid: String = "", + val nama: String = "", + val nim: String = "", + val email: String = "", + val photoUrl: String = "", + val instagram: String = "", + val line: String = "", + val whatsapp: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/DetailBarangActivity.kt b/app/src/main/java/com/androidprojek/unifind/ui/DetailBarangActivity.kt new file mode 100644 index 0000000..b1a23ad --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/DetailBarangActivity.kt @@ -0,0 +1,37 @@ +//package com.androidprojek.unifind.ui +// +//import android.net.Uri +//import android.os.Bundle +//import androidx.appcompat.app.AppCompatActivity +//import com.androidprojek.unifind.R +//import com.androidprojek.unifind.databinding.ActivityDetailBarangBinding +//import com.androidprojek.unifind.model.BarangModel +// +//class DetailBarangActivity : AppCompatActivity() { +// +// private lateinit var binding: ActivityDetailBarangBinding +// +// override fun onCreate(savedInstanceState: Bundle?) { +// super.onCreate(savedInstanceState) +// binding = ActivityDetailBarangBinding.inflate(layoutInflater) +// setContentView(binding.root) +// +// val barang = intent.getParcelableExtra("DATA_BARANG") +// barang?.let { +// binding.tvNama.text = "Nama: ${it.nama}" +// binding.tvNim.text = "NIM: ${it.nim}" +// binding.tvNamaBarang.text = "Nama Barang: ${it.namaBarang}" +// binding.tvKategori.text = "Kategori: ${it.kategori}" +// binding.tvDeskripsi.text = "Deskripsi: ${it.deskripsi}" +// binding.tvTanggalWaktu.text = "Hilang: ${it.tanggalHilang} pukul ${it.waktuHilang}" +// binding.tvLokasi.text = "Lokasi: ${it.lokasiHilang}" +// try { +// val uri = Uri.parse(barang.fotoUri) +// binding.imgDetail.setImageURI(uri) +// } catch (e: Exception) { +// e.printStackTrace() +// binding.imgDetail.setImageResource(R.drawable.warning_vector) +// } +// } +// } +//} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/FormBarangActivity.kt b/app/src/main/java/com/androidprojek/unifind/ui/FormBarangActivity.kt new file mode 100644 index 0000000..d938204 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/FormBarangActivity.kt @@ -0,0 +1,254 @@ +package com.androidprojek.unifind.ui + +import android.app.Activity +import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.MediaStore +import android.view.View +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider // <-- PASTIKAN IMPORT INI ADA +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.ActivityFormBarangBinding +import com.androidprojek.unifind.model.BarangModel +import com.androidprojek.unifind.model.UserModel +import com.androidprojek.unifind.viewmodel.NotificationMainViewModel // <-- PASTIKAN IMPORT INI ADA +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.storage.FirebaseStorage +import java.text.SimpleDateFormat +import java.util.* + +class FormBarangActivity : AppCompatActivity() { + + private lateinit var binding: ActivityFormBarangBinding + private val selectedImageUris = mutableListOf() + private val calendar = Calendar.getInstance() + + private lateinit var db: FirebaseFirestore + private lateinit var storage: FirebaseStorage + private lateinit var auth: FirebaseAuth + + // <-- PERUBAHAN 1: Deklarasikan ViewModel + private lateinit var sharedViewModel: NotificationMainViewModel + + private var currentUserProfile: UserModel? = null + + companion object { + const val PICK_IMAGES_REQUEST = 1 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityFormBarangBinding.inflate(layoutInflater) + setContentView(binding.root) + + val btnBack: ImageView = findViewById(R.id.btnBack) + + // 2. Set OnClickListener untuk ImageView + btnBack.setOnClickListener { + // 3. Panggil finish() saat di-klik untuk menutup Activity ini dan kembali + finish() + } + + db = FirebaseFirestore.getInstance() + storage = FirebaseStorage.getInstance() + auth = FirebaseAuth.getInstance() + + // <-- PERUBAHAN 2: Inisialisasi ViewModel + sharedViewModel = ViewModelProvider(this).get(NotificationMainViewModel::class.java) + + fetchUserProfile() + setupSpinner() + setupDateTimePickers() + setupButtonListeners() + } + + private fun fetchUserProfile() { + val user = auth.currentUser + if (user == null) { + Toast.makeText(this, "Sesi login tidak valid.", Toast.LENGTH_LONG).show() + finish() + return + } + + setLoading(true) + db.collection("users").document(user.uid).get() + .addOnSuccessListener { document -> + if (document.exists()) { + currentUserProfile = document.toObject(UserModel::class.java) + } else { + Toast.makeText(this, "Data profil tidak ditemukan.", Toast.LENGTH_SHORT).show() + } + setLoading(false) + } + .addOnFailureListener { + setLoading(false) + Toast.makeText(this, "Gagal mengambil data profil.", Toast.LENGTH_SHORT).show() + } + } + + private fun saveDataToFirestore(imageUrls: List) { + val currentUser = auth.currentUser + if (currentUser == null || currentUserProfile == null) { + setLoading(false) + Toast.makeText(this, "Tidak bisa menyimpan, data pengguna tidak lengkap.", Toast.LENGTH_SHORT).show() + return + } + + val barang = BarangModel( + pelaporUid = currentUser.uid, + nama = currentUserProfile!!.nama, + nim = currentUserProfile!!.nim, + pelaporPhotoUrl = currentUserProfile!!.photoUrl, + pelaporInstagram = currentUserProfile!!.instagram, + pelaporLine = currentUserProfile!!.line, + pelaporWhatsapp = currentUserProfile!!.whatsapp, + namaBarang = binding.etNamaBarang.text.toString(), + kategori = binding.spinnerKategori.selectedItem.toString(), + deskripsi = binding.etDeskripsi.text.toString(), + tanggalHilang = binding.tvTanggal.text.toString(), + waktuHilang = binding.tvWaktu.text.toString(), + lokasiHilang = binding.etLokasi.text.toString(), + fotoUris = imageUrls, + status = "Dalam Pencarian" + ) + + db.collection("barangHilang").add(barang) + .addOnSuccessListener { + // <-- PERUBAHAN 3: Panggil ViewModel untuk membuat notifikasi + sharedViewModel.addSearchSuccessNotification() + + setLoading(false) + Toast.makeText(this, "Laporan berhasil dibuat!", Toast.LENGTH_LONG).show() + finish() + } + .addOnFailureListener { e -> + setLoading(false) + Toast.makeText(this, "Gagal menyimpan data: ${e.message}", Toast.LENGTH_LONG).show() + } + } + + // ... (Sisa fungsi lainnya tidak perlu diubah) ... + private fun setupSpinner() { + ArrayAdapter.createFromResource(this, R.array.kategori_barang, android.R.layout.simple_spinner_item).also { adapter -> + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spinnerKategori.adapter = adapter + } + } + + private fun setupDateTimePickers() { + val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth -> + calendar.set(Calendar.YEAR, year); calendar.set(Calendar.MONTH, month); calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + updateDateInView() + } + val timeSetListener = TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute -> + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay); calendar.set(Calendar.MINUTE, minute) + updateTimeInView() + } + binding.tvTanggal.setOnClickListener { + DatePickerDialog(this, dateSetListener, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)).show() + } + binding.tvWaktu.setOnClickListener { + TimePickerDialog(this, timeSetListener, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true).show() + } + } + + private fun setupButtonListeners() { + binding.btnUploadGambar.setOnClickListener { + val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + startActivityForResult(intent, PICK_IMAGES_REQUEST) + } + binding.btnSimpan.setOnClickListener { + if (isValidInput()) { + uploadImagesAndSaveData() + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == PICK_IMAGES_REQUEST && resultCode == Activity.RESULT_OK && data != null) { + selectedImageUris.clear() + if (data.clipData != null) { + for (i in 0 until data.clipData!!.itemCount) { + selectedImageUris.add(data.clipData!!.getItemAt(i).uri) + } + } else if (data.data != null) { + selectedImageUris.add(data.data!!) + } + binding.tvImageCount.text = "${selectedImageUris.size} gambar dipilih" + binding.tvImageCount.visibility = View.VISIBLE + } + } + + private fun isValidInput(): Boolean { + if (binding.etNamaBarang.text.isBlank()) { + Toast.makeText(this, "Nama Barang tidak boleh kosong", Toast.LENGTH_SHORT).show() + return false + } + if (binding.spinnerKategori.selectedItemPosition == 0) { + Toast.makeText(this, "Silakan pilih kategori barang", Toast.LENGTH_SHORT).show() + return false + } + if (binding.tvTanggal.text.isBlank() || binding.tvWaktu.text.isBlank()) { + Toast.makeText(this, "Silakan tentukan tanggal dan waktu", Toast.LENGTH_SHORT).show() + return false + } + if (selectedImageUris.isEmpty()) { + Toast.makeText(this, "Silakan pilih gambar terlebih dahulu", Toast.LENGTH_SHORT).show() + return false + } + return true + } + + private fun uploadImagesAndSaveData() { + setLoading(true) + val uploadedImageUrls = mutableListOf() + var uploadCounter = 0 + if (selectedImageUris.isEmpty()) { + setLoading(false) + return + } + for (uri in selectedImageUris) { + val fileName = "images/${System.currentTimeMillis()}_${uri.lastPathSegment}" + val storageRef = storage.reference.child(fileName) + storageRef.putFile(uri) + .addOnSuccessListener { + storageRef.downloadUrl.addOnSuccessListener { downloadUrl -> + uploadedImageUrls.add(downloadUrl.toString()) + uploadCounter++ + if (uploadCounter == selectedImageUris.size) { + saveDataToFirestore(uploadedImageUrls) + } + } + } + .addOnFailureListener { e -> + setLoading(false) + Toast.makeText(this, "Gagal mengunggah gambar: ${e.message}", Toast.LENGTH_LONG).show() + } + } + } + + private fun setLoading(isLoading: Boolean) { + binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + binding.btnSimpan.isEnabled = !isLoading + binding.btnUploadGambar.isEnabled = !isLoading + } + + private fun updateDateInView() { + val myFormat = "dd-MM-yyyy"; val sdf = SimpleDateFormat(myFormat, Locale.getDefault()) + binding.tvTanggal.text = sdf.format(calendar.time) + } + + private fun updateTimeInView() { + val myFormat = "HH:mm"; val sdf = SimpleDateFormat(myFormat, Locale.getDefault()) + binding.tvWaktu.text = sdf.format(calendar.time) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/KontakPelaporActivity.kt b/app/src/main/java/com/androidprojek/unifind/ui/KontakPelaporActivity.kt new file mode 100644 index 0000000..d364412 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/KontakPelaporActivity.kt @@ -0,0 +1,96 @@ +package com.androidprojek.unifind.ui + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.androidprojek.unifind.databinding.ActivityKontakPelaporBinding + +class KontakPelaporActivity : AppCompatActivity() { + + private lateinit var binding: ActivityKontakPelaporBinding + + // Definisikan 'key' untuk intent agar konsisten dan aman dari typo + companion object { + const val EXTRA_INSTAGRAM = "extra_instagram" + const val EXTRA_LINE = "extra_line" + const val EXTRA_WHATSAPP = "extra_whatsapp" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityKontakPelaporBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Tombol kembali di toolbar + binding.topAppBar.setNavigationOnClickListener { + finish() + } + + // Ambil data yang dikirim dari adapter + val instagram = intent.getStringExtra(EXTRA_INSTAGRAM) + val line = intent.getStringExtra(EXTRA_LINE) + val whatsapp = intent.getStringExtra(EXTRA_WHATSAPP) + + // Tampilkan data ke UI dan atur visibilitas + setupContactView(binding.layoutInstagram, binding.tvInstagram, instagram) + setupContactView(binding.layoutLine, binding.tvLine, line) + setupContactView(binding.layoutWhatsapp, binding.tvWhatsapp, whatsapp) + + // Atur listener untuk membuka aplikasi terkait + setupClickListeners(instagram, line, whatsapp) + } + + private fun setupContactView(layout: View, textView: android.widget.TextView, data: String?) { + if (!data.isNullOrEmpty()) { + layout.visibility = View.VISIBLE + textView.text = data + } else { + // Sembunyikan baris kontak jika datanya tidak ada + layout.visibility = View.GONE + } + } + + private fun setupClickListeners(instagram: String?, line: String?, whatsapp: String?) { + binding.layoutInstagram.setOnClickListener { + if (!instagram.isNullOrEmpty()) { + val username = instagram.removePrefix("@") + val uri = Uri.parse("http://instagram.com/_u/$username") + val intent = Intent(Intent.ACTION_VIEW, uri) + try { + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "Aplikasi Instagram tidak terpasang.", Toast.LENGTH_SHORT).show() + } + } + } + + binding.layoutLine.setOnClickListener { + if (!line.isNullOrEmpty()) { + val uri = Uri.parse("https://line.me/R/ti/p/~$line") + val intent = Intent(Intent.ACTION_VIEW, uri) + try { + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "Aplikasi Line tidak terpasang.", Toast.LENGTH_SHORT).show() + } + } + } + + binding.layoutWhatsapp.setOnClickListener { + if (!whatsapp.isNullOrEmpty()) { + // Format nomor ke standar internasional tanpa + atau 0 di depan + val formattedNumber = whatsapp.replaceFirst("^0", "").replace(Regex("[^0-9]"), "") + val url = "https://wa.me/62$formattedNumber" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + try { + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "Aplikasi WhatsApp tidak terpasang.", Toast.LENGTH_SHORT).show() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/LaporPenemuanActivity.kt b/app/src/main/java/com/androidprojek/unifind/ui/LaporPenemuanActivity.kt new file mode 100644 index 0000000..1d9c561 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/LaporPenemuanActivity.kt @@ -0,0 +1,254 @@ +package com.androidprojek.unifind.ui + +import android.app.Activity +import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.MediaStore +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import com.androidprojek.unifind.databinding.ActivityLaporPenemuanBinding +import com.androidprojek.unifind.model.LaporanPenemuanModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.storage.FirebaseStorage +import java.text.SimpleDateFormat +import java.util.* + +class LaporPenemuanActivity : AppCompatActivity() { + + private lateinit var binding: ActivityLaporPenemuanBinding + private lateinit var auth: FirebaseAuth + private lateinit var db: FirebaseFirestore + private lateinit var storage: FirebaseStorage + + // Data dari postingan asli yang diterima via Intent + private var idBarangAsli: String? = null + private var uidPelaporAsli: String? = null + private var namaBarangAsli: String? = null + private var kategoriAsli: String? = null + + // Data Penemu (pengguna saat ini) + private var penemuNama: String? = null + private var penemuNim: String? = null + private var penemuInstagram: String? = null + private var penemuLine: String? = null + private var penemuWhatsapp: String? = null + private var penemuPhotoUrl: String? = null + + // Data Form + private val selectedImageUris = mutableListOf() + private val calendar = Calendar.getInstance() + + private val imagePickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + handleImageSelection(result.data) + } + } + + companion object { + const val EXTRA_BARANG_ID = "extra_barang_id" + const val EXTRA_NAMA_BARANG = "extra_nama_barang" + const val EXTRA_KATEGORI = "extra_kategori" + const val EXTRA_PELAPOR_UID = "extra_pelapor_uid" + const val EXTRA_NAMA_PELAPOR = "extra_nama_pelapor" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLaporPenemuanBinding.inflate(layoutInflater) + setContentView(binding.root) + + auth = FirebaseAuth.getInstance() + db = FirebaseFirestore.getInstance() + storage = FirebaseStorage.getInstance() + + getIntentData() + setupListeners() + + // Nonaktifkan tombol kirim di awal & mulai ambil data profil + setSubmitButtonEnabled(false) + fetchFinderProfile() + } + + private fun getIntentData() { + idBarangAsli = intent.getStringExtra(EXTRA_BARANG_ID) + uidPelaporAsli = intent.getStringExtra(EXTRA_PELAPOR_UID) + // Simpan nama barang dan kategori ke variabel + namaBarangAsli = intent.getStringExtra(EXTRA_NAMA_BARANG) + kategoriAsli = intent.getStringExtra(EXTRA_KATEGORI) + } + + private fun fetchFinderProfile() { + val currentUser = auth.currentUser + if (currentUser == null) { + Toast.makeText(this, "Sesi tidak valid, silakan login ulang.", Toast.LENGTH_LONG).show() + finish() + return + } + + // Anda bisa menambahkan ProgressBar di XML jika ingin ada indikator loading visual + // binding.progressBar.visibility = View.VISIBLE + + db.collection("users").document(currentUser.uid).get() + .addOnSuccessListener { document -> + // binding.progressBar.visibility = View.GONE + if (document.exists()) { + // Ambil semua data profil penemu + penemuNama = document.getString("nama") + penemuNim = document.getString("nim") + penemuInstagram = document.getString("instagram") + penemuLine = document.getString("line") + penemuWhatsapp = document.getString("whatsapp") + penemuPhotoUrl = document.getString("photoUrl") + + // Langsung isikan ke TextView yang sekarang berfungsi sebagai field read-only + binding.tvPenemuNama.text = penemuNama ?: "Data tidak ditemukan" + binding.tvPenemuNim.text = penemuNim ?: "Data tidak ditemukan" + + // Aktifkan tombol kirim setelah data profil berhasil dimuat + setSubmitButtonEnabled(true) + } else { + Toast.makeText(this, "Data profil tidak ditemukan.", Toast.LENGTH_SHORT).show() + } + } + .addOnFailureListener { e -> + // binding.progressBar.visibility = View.GONE + Toast.makeText(this, "Gagal memuat profil: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + private fun setupListeners() { + binding.toolbarLaporPenemuan.setOnClickListener { finish() } + + binding.btnUnggahFoto.setOnClickListener { + val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply { + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + imagePickerLauncher.launch(intent) + } + + binding.btnKirimLaporan.setOnClickListener { + if (isValidInput()) { + uploadImagesAndSaveReport() + } + } + + val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth -> + calendar.set(Calendar.YEAR, year); calendar.set(Calendar.MONTH, month); calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + binding.tvTanggalTemuan.text = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(calendar.time) + } + val timeSetListener = TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute -> + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay); calendar.set(Calendar.MINUTE, minute) + binding.tvWaktuTemuan.text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(calendar.time) + } + // Pastikan ID untuk field tanggal dan waktu sesuai dengan layout XML yang baru + binding.fieldTanggalTemuan.setOnClickListener { + DatePickerDialog(this, dateSetListener, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)).show() + } + binding.fieldWaktuTemuan.setOnClickListener { + TimePickerDialog(this, timeSetListener, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true).show() + } + } + + private fun handleImageSelection(data: Intent?) { + if (data == null) return + selectedImageUris.clear() + if (data.clipData != null) { + for (i in 0 until data.clipData!!.itemCount) { + selectedImageUris.add(data.clipData!!.getItemAt(i).uri) + } + } else if (data.data != null) { + selectedImageUris.add(data.data!!) + } + // Di sini Anda bisa menampilkan preview gambar di RecyclerView jika mau + // binding.rvFotoBukti.visibility = View.VISIBLE + Toast.makeText(this, "${selectedImageUris.size} gambar dipilih", Toast.LENGTH_SHORT).show() + } + + private fun isValidInput(): Boolean { + if (binding.tvTanggalTemuan.text.isEmpty() || binding.tvWaktuTemuan.text.isEmpty() || binding.edtLokasiTemuan.text.isBlank()) { + Toast.makeText(this, "Tanggal, Waktu, dan Lokasi ditemukan wajib diisi.", Toast.LENGTH_SHORT).show() + return false + } + if (selectedImageUris.isEmpty()) { + Toast.makeText(this, "Foto barang bukti wajib diunggah.", Toast.LENGTH_SHORT).show() + return false + } + return true + } + + private fun uploadImagesAndSaveReport() { + // Anda bisa menampilkan loading di sini jika mau + val uploadedImageUrls = mutableListOf() + var uploadCounter = 0 + + for (uri in selectedImageUris) { + val fileName = "laporan_penemuan_images/${System.currentTimeMillis()}_${uri.lastPathSegment}" + val storageRef = storage.reference.child(fileName) + storageRef.putFile(uri) + .addOnSuccessListener { + storageRef.downloadUrl.addOnSuccessListener { downloadUrl -> + uploadedImageUrls.add(downloadUrl.toString()) + uploadCounter++ + if (uploadCounter == selectedImageUris.size) { + saveReportToFirestore(uploadedImageUrls) + } + } + } + .addOnFailureListener { e -> + Toast.makeText(this, "Gagal mengunggah gambar: ${e.message}", Toast.LENGTH_LONG).show() + } + } + } + + private fun saveReportToFirestore(imageUrls: List) { + val penemu = auth.currentUser + if (penemu == null || idBarangAsli == null || uidPelaporAsli == null || penemuNama == null) { + Toast.makeText(this, "Gagal menyimpan, data pengguna tidak lengkap.", Toast.LENGTH_SHORT).show() + return + } + + val laporan = LaporanPenemuanModel( + idBarangHilang = idBarangAsli!!, + uidPelaporAsli = uidPelaporAsli!!, + penemuUid = penemu.uid, + penemuNama = this.penemuNama!!, + penemuNim = this.penemuNim ?: "", + penemuInstagram = this.penemuInstagram, + penemuLine = this.penemuLine, + penemuWhatsapp = this.penemuWhatsapp, + penemuPhotoUrl = this.penemuPhotoUrl, + // Menyimpan nama barang dan kategori dari postingan asli + namaBarangPostingan = this.namaBarangAsli, + kategoriPostingan = this.kategoriAsli, + deskripsiTambahan = binding.edtDeskripsiTambahan.text.toString().trim(), + tanggalTemuan = binding.tvTanggalTemuan.text.toString(), + waktuTemuan = binding.tvWaktuTemuan.text.toString(), + lokasiTemuan = binding.edtLokasiTemuan.text.toString().trim(), + fotoLaporanUris = imageUrls, + statusLaporan = "Menunggu Verifikasi" + ) + + db.collection("barangHilang").document(idBarangAsli!!) + .collection("laporanPenemuan") + .add(laporan) + .addOnSuccessListener { + Toast.makeText(this, "Laporan penemuan berhasil dikirim!", Toast.LENGTH_LONG).show() + finish() + } + .addOnFailureListener { e -> + Toast.makeText(this, "Gagal mengirim laporan: ${e.message}", Toast.LENGTH_LONG).show() + } + } + + private fun setSubmitButtonEnabled(isEnabled: Boolean) { + binding.btnKirimLaporan.isEnabled = isEnabled + binding.btnKirimLaporan.alpha = if (isEnabled) 1.0f else 0.5f + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/dashboard/AddTrackingFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/dashboard/AddTrackingFragment.kt new file mode 100644 index 0000000..b969995 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/dashboard/AddTrackingFragment.kt @@ -0,0 +1,212 @@ +package com.androidprojek.unifind.ui.dashboard + +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.FragmentAddTrackingBinding +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.DocumentReference +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.storage.FirebaseStorage +import java.util.UUID + +class AddTrackingFragment : Fragment() { + + private var _binding: FragmentAddTrackingBinding? = null + private val binding get() = _binding!! + + private var selectedCategory: String? = null + private var imageUri: Uri? = null + + private val db = FirebaseFirestore.getInstance() + private val storage = FirebaseStorage.getInstance() + private val auth = FirebaseAuth.getInstance() + + // Launcher untuk memilih gambar dari galeri + private val pickImageLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { + // Tampilkan gambar di ImageView + binding.ivFotoBarang.setImageURI(it) + binding.ivFotoBarang.visibility = View.VISIBLE + // Sembunyikan ikon unggah dan petunjuk setelah gambar dipilih + binding.unggahBg.visibility = View.GONE + // Simpan URI gambar untuk digunakan nanti (misalnya untuk upload ke server) + // Anda bisa menyimpan URI ini di variabel atau ViewModel + imageUri = it + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAddTrackingBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + if (auth.currentUser == null) { + Log.d("Auth", "User not logged in") + findNavController().navigate(R.id.navigation_home) + return + } else { + Log.d("Auth", "User logged in with UID: ${auth.currentUser?.uid}") + } + + // Atur navigasi kembali pada tombol Back + binding.back.setOnClickListener { + findNavController().navigateUp() // Kembali ke halaman sebelumnya (menu Pelacakan) + } + + // Setup Spinner untuk kategori barang + setupSpinner() + + // Atur listener untuk tombol Unggah Foto + binding.btnUploadFoto.setOnClickListener { + // Buka galeri untuk memilih gambar + pickImageLauncher.launch("image/*") + } + + // Atur listener untuk tombol Hubungkan + binding.btnHubungkan.setOnClickListener { + val namaBarang = binding.edtNama.text.toString() + val kategoriBarang = selectedCategory + val deskripsiBarang = binding.edtDeskripsi.text.toString() + val idPerangkat = binding.edtId.text.toString() + + // Validasi field wajib + if (namaBarang.isEmpty() || kategoriBarang == null || deskripsiBarang.isEmpty()) { + Toast.makeText(context, "Lengkapi semua field wajib", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + // Jika ada gambar, unggah ke Firebase Storage + if (imageUri != null) { + val storageRef = storage.reference.child("tracking_images/${UUID.randomUUID()}.jpg") + storageRef.putFile(imageUri!!) + .continueWithTask { task -> + if (!task.isSuccessful) { + task.exception?.let { throw it } + } + storageRef.downloadUrl + } + .addOnSuccessListener { uri -> + val imageUrl = uri.toString() + saveToFirestore(namaBarang, kategoriBarang, deskripsiBarang, idPerangkat, imageUrl) + } + .addOnFailureListener { e -> + Toast.makeText(context, "Gagal mengunggah gambar: ${e.message}", Toast.LENGTH_SHORT).show() + } + } else { + // Simpan data ke Firestore tanpa gambar + saveToFirestore(namaBarang, kategoriBarang, deskripsiBarang, idPerangkat, null) + } + } + } + + private fun saveToFirestore( + namaBarang: String, + kategoriBarang: String, + deskripsiBarang: String, + idPerangkat: String, + imageUrl: String? + ) { + // Mengambil User Id Saat Ini + val userId = auth.currentUser?.uid + + // Buat data yang akan disimpan + val trackingData = hashMapOf( + "namaBarang" to namaBarang, + "kategoriBarang" to kategoriBarang, + "deskripsiBarang" to deskripsiBarang, + "idPerangkat" to idPerangkat, + "imageUrl" to imageUrl, + "timestamp" to System.currentTimeMillis(), + "userId" to userId + ) + + // Simpan ke Firestore di collection "trackings" + db.collection("trackings") + .add(trackingData) + .addOnSuccessListener { documentReference -> + Toast.makeText(context, "Data pelacakan berhasil disimpan!", Toast.LENGTH_SHORT).show() + // Kembali ke halaman sebelumnya (menu Pelacakan) + findNavController().navigateUp() + } + .addOnFailureListener { e -> + Toast.makeText(context, "Gagal menyimpan data: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + private fun setupSpinner() { + // Ambil daftar kategori dari arrays.xml + val categories = resources.getStringArray(R.array.kategori_barang).toMutableList() + + // Buat adapter untuk Spinner + val adapter = object : ArrayAdapter( + requireContext(), + android.R.layout.simple_spinner_item, + categories + ) { + override fun isEnabled(position: Int): Boolean { + // Nonaktifkan item pertama (placeholder) + return position != 0 + } + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getDropDownView(position, convertView, parent) as TextView + if (position == 0) { + view.setTextColor(requireContext().getColor(android.R.color.darker_gray)) + } else { + view.setTextColor(requireContext().getColor(android.R.color.black)) + } + return view + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getView(position, convertView, parent) as TextView + if (position == 0 && selectedCategory == null) { + view.setTextColor(requireContext().getColor(android.R.color.darker_gray)) + } else { + view.setTextColor(requireContext().getColor(android.R.color.black)) + } + return view + } + } + + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spinnerKategoriBarang.adapter = adapter + + binding.spinnerKategoriBarang.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + if (position == 0) { + selectedCategory = null + } else { + selectedCategory = categories[position] + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + selectedCategory = null + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/dashboard/DashboardFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/dashboard/DashboardFragment.kt new file mode 100644 index 0000000..ddcb9a7 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/dashboard/DashboardFragment.kt @@ -0,0 +1,123 @@ +package com.androidprojek.unifind.ui.dashboard + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.FragmentDashboardBinding +import com.androidprojek.unifind.model.Tracking +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class DashboardFragment : Fragment() { + + private var _binding: FragmentDashboardBinding? = null + private val binding get() = _binding!! + + private val db = FirebaseFirestore.getInstance() + private lateinit var trackingAdapter: TrackingAdapter + private val trackingList = mutableListOf() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val dashboardViewModel = + ViewModelProvider(this).get(DashboardViewModel::class.java) + + _binding = FragmentDashboardBinding.inflate(inflater, container, false) + val root: View = binding.root + + val textView: TextView = binding.textDashboard + dashboardViewModel.text.observe(viewLifecycleOwner) { + textView.text = it + } + return root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Setup RecyclerView + trackingAdapter = TrackingAdapter(trackingList) { tracking -> + // Kirim idPerangkat ke fragment lain +// val action = DashboardFragmentDirections +// .actionNavigationPelacakanToDetailTrackingFragment(tracking.idPerangkat ?: "") +// findNavController().navigate(action) + val bundle = Bundle().apply { + putString("idPerangkat", tracking.idPerangkat) + } + findNavController().navigate(R.id.detailTrackingFragment, bundle) + } + binding.rvTrackings.layoutManager = LinearLayoutManager(context) + binding.rvTrackings.adapter = trackingAdapter + + binding.btnLacak.setOnClickListener { + findNavController().navigate(R.id.action_navigation_pelacakan_to_addTrackingFragment) + } + + binding.fabTambah.setOnClickListener { + findNavController().navigate(R.id.action_navigation_pelacakan_to_addTrackingFragment) + } + + val userId = FirebaseAuth.getInstance().currentUser?.uid + + // Lifecycle-aware listener using callbackFlow + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.STARTED) { + val snapshotFlow = callbackFlow { + val listenerRegistration = db.collection("trackings") + .whereEqualTo("userId", userId) + .addSnapshotListener { snapshot, e -> + if (e != null) { + close(e) + return@addSnapshotListener + } + trySend(snapshot).isSuccess + } + + awaitClose { listenerRegistration.remove() } + } + + snapshotFlow.collectLatest { snapshot -> + trackingList.clear() + for (document in snapshot?.documents ?: emptyList()) { + val tracking = document.toObject(Tracking::class.java)?.copy(id = document.id) + if (tracking != null) { + trackingList.add(tracking) + } + } + + if (trackingList.isEmpty()) { + binding.empty.visibility = View.VISIBLE + binding.rvTrackings.visibility = View.GONE + } else { + binding.empty.visibility = View.GONE + binding.rvTrackings.visibility = View.VISIBLE + trackingAdapter.notifyDataSetChanged() + } + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/com/androidprojek/unifind/ui/dashboard/DashboardViewModel.kt new file mode 100644 index 0000000..484376d --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/dashboard/DashboardViewModel.kt @@ -0,0 +1,13 @@ +package com.androidprojek.unifind.ui.dashboard + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class DashboardViewModel : ViewModel() { + + private val _text = MutableLiveData().apply { + value = "Pelacakan" + } + val text: LiveData = _text +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/dashboard/DetailTrackingFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/dashboard/DetailTrackingFragment.kt new file mode 100644 index 0000000..cdb6220 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/dashboard/DetailTrackingFragment.kt @@ -0,0 +1,147 @@ +package com.androidprojek.unifind.ui.dashboard + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.androidprojek.unifind.R +import com.bumptech.glide.Glide +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.SupportMapFragment +import com.google.android.gms.maps.model.* +import com.google.firebase.database.* +import com.google.firebase.firestore.FirebaseFirestore + +class DetailTrackingFragment : Fragment(), OnMapReadyCallback { + + private var idPerangkat: String? = null + private lateinit var googleMap: GoogleMap + private var currentMarker: Marker? = null + private lateinit var database: DatabaseReference + private val db = FirebaseFirestore.getInstance() + private val currentUid = com.google.firebase.auth.FirebaseAuth.getInstance().currentUser?.uid + + // Simulasi koordinat +// private val latitude = -6.200000 +// private val longitude = 106.816666 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + idPerangkat = it.getString("idPerangkat") + } + database = FirebaseDatabase.getInstance().getReference("tracking/$idPerangkat") + Log.d("DEBUG_TRACKING", "Reference to database initialized") + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_detail_tracking, container, false) + + // Tampilkan ID + val tvIdPerangkat = view.findViewById(R.id.tv_id_perangkat_detail) + tvIdPerangkat.text = idPerangkat ?: "ID tidak ditemukan" + + // Inisialisasi map + val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment + mapFragment.getMapAsync(this) + + val btnBack = view.findViewById(R.id.back) + btnBack.setOnClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + // Atau bisa juga: + // findNavController().navigateUp() + } + + val tvKategori = view.findViewById(R.id.item_kategori) + val tvNama = view.findViewById(R.id.item_namaBarang) + val imageView = view.findViewById(R.id.item_image) + val tvKoordinat = view.findViewById(R.id.item_koordinat) + + if (idPerangkat != null && currentUid != null) { + db.collection("trackings") + .whereEqualTo("userId", currentUid) + .whereEqualTo("idPerangkat", idPerangkat) + .get() + .addOnSuccessListener { documents -> + if (!documents.isEmpty) { + val doc = documents.first() + val namaBarang = doc.getString("namaBarang") + val kategoriBarang = doc.getString("kategoriBarang") + val imageUrl = doc.getString("imageUrl") + + tvNama.text = namaBarang ?: "Tanpa Nama" + tvKategori.text = kategoriBarang ?: "Tanpa Kategori" + if (!imageUrl.isNullOrEmpty()) { + Glide.with(this) + .load(imageUrl) + .into(imageView) + } + } else { + Toast.makeText(requireContext(), "Data tidak ditemukan", Toast.LENGTH_SHORT).show() + } + } + .addOnFailureListener { + Toast.makeText(requireContext(), "Gagal mengambil data: ${it.message}", Toast.LENGTH_SHORT).show() + } + } + + return view + } + + override fun onMapReady(map: GoogleMap) { + googleMap = map + + // Coba tambahkan marker dummy dulu +// val dummyLocation = LatLng(-6.2, 106.816666) +// googleMap.addMarker(MarkerOptions().position(dummyLocation).title("Dummy Marker")) +// googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(dummyLocation, 15f)) + + // Pantau perubahan data realtime + database.addValueEventListener(object : ValueEventListener { + override fun onDataChange(snapshot: DataSnapshot) { +// Log.d("DEBUG_TRACKING", "Snapshot exists: ${snapshot.exists()}") +// Log.d("DEBUG_TRACKING", "Snapshot children count: ${snapshot.childrenCount}") +// Log.d("DEBUG_TRACKING", "Snapshot value: ${snapshot.value}") + + val lat = snapshot.child("latitude").getValue(Double::class.java) + val lng = snapshot.child("longitude").getValue(Double::class.java) +// Log.d("DEBUG_TRACKING", "Lat: $lat, Lng: $lng") + + if (lat != null && lng != null) { + val newLocation = LatLng(lat, lng) + + if (currentMarker == null) { + currentMarker = googleMap.addMarker( + MarkerOptions().position(newLocation).title("Barang") + ) + googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(newLocation, 15f)) + } else { + currentMarker!!.position = newLocation + // Optional: animate camera + googleMap.animateCamera(CameraUpdateFactory.newLatLng(newLocation)) + } + + val koordinatText = "(${lat}, ${lng})" + view?.findViewById(R.id.item_koordinat)?.text = koordinatText + + } + } + + override fun onCancelled(error: DatabaseError) { + // Handle error + Log.e("DEBUG_TRACKING", "Database read cancelled: ${error.message}") + Toast.makeText(requireContext(), "Gagal membaca data: ${error.message}", Toast.LENGTH_SHORT).show() + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/data/LoginDataSource.kt b/app/src/main/java/com/androidprojek/unifind/ui/data/LoginDataSource.kt new file mode 100644 index 0000000..354d32c --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/data/LoginDataSource.kt @@ -0,0 +1,24 @@ +package com.androidprojek.unifind.ui.data + +import com.androidprojek.unifind.ui.data.model.LoggedInUser +import java.io.IOException + +/** + * Class that handles authentication w/ login credentials and retrieves user information. + */ +class LoginDataSource { + + fun login(username: String, password: String): Result { + try { + // TODO: handle loggedInUser authentication + val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe") + return Result.Success(fakeUser) + } catch (e: Throwable) { + return Result.Error(IOException("Error logging in", e)) + } + } + + fun logout() { + // TODO: revoke authentication + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/data/LoginRepository.kt b/app/src/main/java/com/androidprojek/unifind/ui/data/LoginRepository.kt new file mode 100644 index 0000000..ac543e2 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/data/LoginRepository.kt @@ -0,0 +1,46 @@ +package com.androidprojek.unifind.ui.data + +import com.androidprojek.unifind.ui.data.model.LoggedInUser + +/** + * Class that requests authentication and user information from the remote data source and + * maintains an in-memory cache of login status and user credentials information. + */ + +class LoginRepository(val dataSource: LoginDataSource) { + + // in-memory cache of the loggedInUser object + var user: LoggedInUser? = null + private set + + val isLoggedIn: Boolean + get() = user != null + + init { + // If user credentials will be cached in local storage, it is recommended it be encrypted + // @see https://developer.android.com/training/articles/keystore + user = null + } + + fun logout() { + user = null + dataSource.logout() + } + + fun login(username: String, password: String): Result { + // handle login + val result = dataSource.login(username, password) + + if (result is Result.Success) { + setLoggedInUser(result.data) + } + + return result + } + + private fun setLoggedInUser(loggedInUser: LoggedInUser) { + this.user = loggedInUser + // If user credentials will be cached in local storage, it is recommended it be encrypted + // @see https://developer.android.com/training/articles/keystore + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/data/Result.kt b/app/src/main/java/com/androidprojek/unifind/ui/data/Result.kt new file mode 100644 index 0000000..887b262 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/data/Result.kt @@ -0,0 +1,18 @@ +package com.androidprojek.unifind.ui.data + +/** + * A generic class that holds a value with its loading status. + * @param + */ +sealed class Result { + + data class Success(val data: T) : Result() + data class Error(val exception: Exception) : Result() + + override fun toString(): String { + return when (this) { + is Success<*> -> "Success[data=$data]" + is Error -> "Error[exception=$exception]" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/data/model/LoggedInUser.kt b/app/src/main/java/com/androidprojek/unifind/ui/data/model/LoggedInUser.kt new file mode 100644 index 0000000..29070f3 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/data/model/LoggedInUser.kt @@ -0,0 +1,9 @@ +package com.androidprojek.unifind.ui.data.model + +/** + * Data class that captures user information for logged in users retrieved from LoginRepository + */ +data class LoggedInUser( + val userId: String, + val displayName: String +) \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/FilterBottomSheetFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/FilterBottomSheetFragment.kt new file mode 100644 index 0000000..bb3bcf3 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/FilterBottomSheetFragment.kt @@ -0,0 +1,85 @@ +package com.androidprojek.unifind.ui.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.recyclerview.widget.GridLayoutManager +import com.androidprojek.unifind.adapter.KategoriFilterAdapter +import com.androidprojek.unifind.model.KategoriModel +import com.androidprojek.unifind.databinding.FragmentFilterBottomSheetBinding +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +class FilterBottomSheetFragment : BottomSheetDialogFragment() { + + private var _binding: FragmentFilterBottomSheetBinding? = null + private val binding get() = _binding!! + + private val listKategori = mutableListOf( + KategoriModel("Laptop"), KategoriModel("Handphone"), KategoriModel("Charger"), + KategoriModel("Earphone"), KategoriModel("Jam Tangan"), KategoriModel("Alat Tulis"), + KategoriModel("Jaket/Hoodie"), KategoriModel("Helm"), KategoriModel("Kartu Identitas"), + KategoriModel("Kacamata") + ) + private val selectedKategori = mutableListOf() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentFilterBottomSheetBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val adapter = KategoriFilterAdapter(listKategori) { kategori -> + handleCategorySelection(kategori) + } + + binding.rvKategori.layoutManager = GridLayoutManager(context, 3) + binding.rvKategori.adapter = adapter + + // Listener untuk tombol Terapkan Filter (tidak berubah) + binding.btnTerapkanFilter.setOnClickListener { + val hasilFilter = ArrayList(selectedKategori.map { it.nama }) + val bundle = Bundle() + bundle.putStringArrayList("kategori_terpilih", hasilFilter) + parentFragmentManager.setFragmentResult("filter_request", bundle) + dismiss() + } + + // --- TAMBAHKAN LOGIKA UNTUK TOMBOL HAPUS FILTER --- + binding.btnHapusFilter.setOnClickListener { + // 1. Kosongkan daftar kategori yang sudah dipilih + selectedKategori.clear() + + // 2. Set semua item di daftar utama menjadi tidak terpilih + listKategori.forEach { it.isSelected = false } + + // 3. Perbarui tampilan chip di RecyclerView + binding.rvKategori.adapter?.notifyDataSetChanged() + } + } + + private fun handleCategorySelection(kategori: KategoriModel) { + if (kategori.isSelected) { + // Jika sudah dipilih, batalkan pilihan + kategori.isSelected = false + selectedKategori.remove(kategori) + } else { + // Jika belum dipilih, tambahkan ke pilihan + if (selectedKategori.size < 3) { + kategori.isSelected = true + selectedKategori.add(kategori) + } else { + Toast.makeText(context, "Maksimal 3 kategori", Toast.LENGTH_SHORT).show() + } + } + binding.rvKategori.adapter?.notifyDataSetChanged() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/FilterDialogFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/FilterDialogFragment.kt new file mode 100644 index 0000000..2f2be9f --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/FilterDialogFragment.kt @@ -0,0 +1,130 @@ +package com.androidprojek.unifind.ui.home + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import com.androidprojek.unifind.R +import com.androidprojek.unifind.model.KategoriModel +import com.androidprojek.unifind.databinding.FilterHomeBinding +import com.google.android.material.chip.Chip // Import Chip + +class FilterDialogFragment : DialogFragment() { + + private var _binding: FilterHomeBinding? = null + private val binding get() = _binding!! + + private val listKategori = mutableListOf( + KategoriModel("Laptop"), KategoriModel("Handphone"), KategoriModel("Charger"), + KategoriModel("Earphone"), KategoriModel("Jam Tangan"), KategoriModel("Alat Tulis"), + KategoriModel("Jaket/Hoodie"), KategoriModel("Helm"), KategoriModel("Kartu Identitas"), + KategoriModel("Kacamata") + ) + private val selectedKategori = mutableListOf() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FilterHomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.requestFeature(Window.FEATURE_NO_TITLE) + return dialog + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Panggil fungsi baru untuk mengisi kategori + populateCategoryChips() + + binding.btnClose.setOnClickListener { + dismiss() + } + + binding.btnTerapkanFilter.setOnClickListener { + val hasilFilter = ArrayList(selectedKategori.map { it.nama }) + val bundle = Bundle() + bundle.putStringArrayList("kategori_terpilih", hasilFilter) + parentFragmentManager.setFragmentResult("filter_request", bundle) + dismiss() + } + + binding.btnHapusFilter.setOnClickListener { + selectedKategori.clear() + listKategori.forEach { it.isSelected = false } + // Perbarui tampilan semua chip menjadi tidak terpilih + updateChipStates() + } + } + + // FUNGSI BARU: Mengisi FlexboxLayout dengan Chip + private fun populateCategoryChips() { + val context = requireContext() + val flexboxLayout = binding.flexboxKategori // Menggunakan ID FlexboxLayout + + // Hapus view lama jika ada (untuk mencegah duplikasi) + flexboxLayout.removeAllViews() + + for (kategori in listKategori) { + // Inflate layout chip dari item_kategori_filter.xml + val chip = layoutInflater.inflate(R.layout.item_kategori_filter, flexboxLayout, false) as Chip + chip.text = kategori.nama + chip.isChecked = kategori.isSelected + + chip.setOnClickListener { + handleCategorySelection(kategori) + // Update state visual chip secara langsung + (it as Chip).isChecked = kategori.isSelected + } + flexboxLayout.addView(chip) + } + } + + // FUNGSI BARU: Mengupdate state semua chip (untuk tombol Hapus) + private fun updateChipStates() { + val flexboxLayout = binding.flexboxKategori + for (i in 0 until flexboxLayout.childCount) { + val chip = flexboxLayout.getChildAt(i) as Chip + chip.isChecked = false + } + } + + private fun handleCategorySelection(kategori: KategoriModel) { + if (kategori.isSelected) { + // Jika sudah dipilih, batalkan pilihan + kategori.isSelected = false + selectedKategori.remove(kategori) + } else { + // Jika belum dipilih, tambahkan ke pilihan + if (selectedKategori.size < 3) { + kategori.isSelected = true + selectedKategori.add(kategori) + } else { + // Batalkan check pada chip yang gagal dipilih + (binding.flexboxKategori.findViewWithTag(kategori.nama) as? Chip)?.isChecked = false + Toast.makeText(context, "Maksimal 3 kategori", Toast.LENGTH_SHORT).show() + } + } + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/HomeFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/HomeFragment.kt new file mode 100644 index 0000000..40d7d1a --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/HomeFragment.kt @@ -0,0 +1,90 @@ +package com.androidprojek.unifind.ui.home + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.FragmentHomeBinding +import com.androidprojek.unifind.ui.home.adapter.HomePagerAdapter +import com.bumptech.glide.Glide +import com.google.android.material.tabs.TabLayoutMediator + +class HomeFragment : Fragment() { + + private var _binding: FragmentHomeBinding? = null + internal val binding get() = _binding!! + + private val homeViewModel: HomeViewModel by activityViewModels() + + private val tabTitles = arrayOf("Pencarian", "Penemuan") + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentHomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupTabs() + setupSearchListener() + observeViewModel() + + binding.btnFilter.setOnClickListener { + val filterDialog = FilterDialogFragment() + filterDialog.show(childFragmentManager, "FilterDialog") + } + } + + private fun observeViewModel() { + // Amati LiveData userProfileImageUrl + homeViewModel.userProfileImageUrl.observe(viewLifecycleOwner) { imageUrl -> + // Gunakan Glide untuk memuat gambar dari URL + if (!imageUrl.isNullOrEmpty()) { + Glide.with(this) + .load(imageUrl) + .placeholder(R.drawable.baseline_person_outline_24) // Gambar default saat loading + .error(R.drawable.baseline_person_outline_24) // Gambar default jika gagal + .circleCrop() // Membuat gambar menjadi bulat + .into(binding.ivProfile) // Masukkan ke ImageView Anda + } else { + // Jika URL kosong atau null, tampilkan gambar default + binding.ivProfile.setImageResource(R.drawable.baseline_person_outline_24) + } + } + } + + private fun setupTabs() { + val adapter = HomePagerAdapter(this) + binding.viewPager.adapter = adapter + + TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position -> + tab.text = tabTitles[position] + }.attach() + } + + private fun setupSearchListener() { + binding.etSearch.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + homeViewModel.setSearchQuery(s.toString()) + } + + override fun afterTextChanged(s: Editable?) {} + }) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/HomeViewModel.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..00557ff --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/HomeViewModel.kt @@ -0,0 +1,128 @@ +package com.androidprojek.unifind.ui.home + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.androidprojek.unifind.model.PenemuanModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.Query + +class HomeViewModel : ViewModel() { + + private val db = FirebaseFirestore.getInstance() + private val auth = FirebaseAuth.getInstance() + + private val _userProfileImageUrl = MutableLiveData() + val userProfileImageUrl: LiveData = _userProfileImageUrl + + // DATA SUMBER (INPUTS) + private val _originalList = MutableLiveData>() + private val _searchQuery = MutableLiveData("") + private val _activeCategories = MutableLiveData>(emptyList()) + + // DATA HASIL (OUTPUT) + val filteredPenemuanList = MediatorLiveData>() + + init { + fetchCurrentUserProfile() + listenToFirestoreChanges() + + filteredPenemuanList.addSource(_originalList) { applyFiltersAndSearch() } + filteredPenemuanList.addSource(_searchQuery) { applyFiltersAndSearch() } + filteredPenemuanList.addSource(_activeCategories) { applyFiltersAndSearch() } + } + + fun fetchCurrentUserProfile() { + val currentUser = auth.currentUser + if (currentUser != null) { + db.collection("users").document(currentUser.uid).get() + .addOnSuccessListener { document -> + if (document.exists()) { + val photoUrl = document.getString("photoUrl") + _userProfileImageUrl.value = photoUrl + Log.d("HomeViewModel", "URL Foto Profil berhasil diambil: $photoUrl") + } + } + .addOnFailureListener { e -> + Log.w("HomeViewModel", "Gagal mengambil foto profil.", e) + } + } + } + + private fun listenToFirestoreChanges() { + val currentUserUid = auth.currentUser?.uid + + // --- PERUBAHAN UTAMA DI SINI --- + // 1. Kita hanya meminta postingan yang statusnya masih aktif, TANPA diurutkan. + // Ini membuat query ke database menjadi sangat sederhana dan andal. + db.collection("form_penemuan") + .whereEqualTo("status", "Dalam Pencarian") + .addSnapshotListener { snapshots, error -> + if (error != null) { + Log.w("HomeViewModel", "Gagal mendengarkan data Firestore.", error) + return@addSnapshotListener + } + + if (snapshots != null) { + val penemuanList = mutableListOf() + for (document in snapshots.documents) { + val item = document.toObject(PenemuanModel::class.java) + if (item != null) { + item.id = document.id + penemuanList.add(item) + } + } + + // 2. Lakukan pengurutan di dalam aplikasi, bukan di database. + penemuanList.sortByDescending { it.timestamp } + + // 3. Filter secara manual untuk membuang postingan milik pengguna saat ini. + val listUntukDitampilkan = if (currentUserUid != null) { + penemuanList.filter { it.uid != currentUserUid } + } else { + penemuanList // Jika tidak login, tampilkan semua + } + + _originalList.value = listUntukDitampilkan + + } else { + _originalList.value = emptyList() + } + } + } + + private fun applyFiltersAndSearch() { + val original = _originalList.value ?: emptyList() + val query = _searchQuery.value?.lowercase()?.trim() ?: "" + val categories = _activeCategories.value ?: emptyList() + + val filteredList = original.filter { item -> + val searchCondition = if (query.isEmpty()) { + true + } else { + item.namaBarang?.lowercase()?.contains(query) == true || + item.kategori?.lowercase()?.contains(query) == true + } + + val filterCondition = if (categories.isEmpty()) { + true + } else { + categories.contains(item.kategori) + } + + searchCondition && filterCondition + } + filteredPenemuanList.value = filteredList + } + + fun setSearchQuery(query: String) { + _searchQuery.value = query + } + + fun setActiveCategories(categories: List) { + _activeCategories.value = categories + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/PencarianFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/PencarianFragment.kt new file mode 100644 index 0000000..24620d9 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/PencarianFragment.kt @@ -0,0 +1,132 @@ +package com.androidprojek.unifind.ui.home + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.adapter.BarangAdapter +import com.androidprojek.unifind.databinding.FragmentPencarianBinding +import com.androidprojek.unifind.model.BarangModel +import com.androidprojek.unifind.ui.FormBarangActivity +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ListenerRegistration +import com.google.firebase.firestore.Query + +class PencarianFragment : Fragment() { + + private var _binding: FragmentPencarianBinding? = null + private val binding get() = _binding!! + + private lateinit var db: FirebaseFirestore + private lateinit var auth: FirebaseAuth + + private val listBarang = mutableListOf() + private lateinit var barangAdapter: BarangAdapter + + private var firestoreListener: ListenerRegistration? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentPencarianBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + db = FirebaseFirestore.getInstance() + auth = FirebaseAuth.getInstance() + + setupRecyclerView() + + binding.fabTambah.setOnClickListener { + startActivity(Intent(requireContext(), FormBarangActivity::class.java)) + } + + // Panggil listener saat view sudah dibuat + listenToLostItems() + } + + private fun setupRecyclerView() { + barangAdapter = BarangAdapter(listBarang) + binding.rvBarang.apply { + layoutManager = LinearLayoutManager(context) + adapter = barangAdapter + } + } + + private fun listenToLostItems() { + val currentUser = auth.currentUser + if (currentUser == null) { + binding.tvEmpty.visibility = View.VISIBLE + binding.tvEmpty.text = "Silakan login untuk melihat postingan." + return + } + val uidPenggunaSaatIni = currentUser.uid + + binding.progressBar.visibility = View.VISIBLE + binding.tvEmpty.visibility = View.GONE + + // --- PERUBAHAN UTAMA PADA QUERY --- + // 1. Kita HANYA memfilter berdasarkan status yang masih aktif. + // Filter `whereNotEqualTo` kita pindahkan ke sisi aplikasi. + val query = db.collection("barangHilang") + .whereEqualTo("status", "Dalam Pencarian") + .orderBy("timestamp", Query.Direction.DESCENDING) + + firestoreListener = query.addSnapshotListener { snapshots, error -> + if (_binding == null) { + return@addSnapshotListener + } + binding.progressBar.visibility = View.GONE + + if (error != null) { + Log.w("PencarianFragment", "Error listening for documents.", error) + return@addSnapshotListener + } + + if (snapshots != null) { + listBarang.clear() + + // --- PERUBAHAN LOGIKA PENYARINGAN & PENGAMBILAN DATA --- + // Loop manual untuk mendapatkan ID dan melakukan filter tambahan + for (doc in snapshots.documents) { + val barang = doc.toObject(BarangModel::class.java) + if (barang != null) { + // Cek jika UID pelapor BUKAN pengguna saat ini + if (barang.pelaporUid != uidPenggunaSaatIni) { + // Ambil ID dokumen dan masukkan ke dalam model + barang.id = doc.id + // Tambahkan ke list hanya jika bukan milik sendiri + listBarang.add(barang) + } + } + } + // --- AKHIR PERUBAHAN --- + + barangAdapter.notifyDataSetChanged() + } + + if (listBarang.isEmpty()) { + binding.tvEmpty.visibility = View.VISIBLE + binding.tvEmpty.text = "Belum ada laporan kehilangan dari pengguna lain." + } else { + binding.tvEmpty.visibility = View.GONE + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + // Hentikan listener untuk mencegah memory leak + firestoreListener?.remove() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/PenemuanFormKlaimBarangFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/PenemuanFormKlaimBarangFragment.kt new file mode 100644 index 0000000..02f6962 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/PenemuanFormKlaimBarangFragment.kt @@ -0,0 +1,191 @@ +package com.androidprojek.unifind.ui.home + +import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.PenemuanFormKlaimBarangBinding +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.storage.FirebaseStorage +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale +import java.util.UUID + +class PenemuanFormKlaimBarangFragment : Fragment() { + + private var _binding: PenemuanFormKlaimBarangBinding? = null + private val binding get() = _binding!! + + private var postId: String? = null + private var imageUri: Uri? = null + + private val calendar = Calendar.getInstance() + + private val db = FirebaseFirestore.getInstance() + private val storage = FirebaseStorage.getInstance() + private val auth = FirebaseAuth.getInstance() + + private val pickImageLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { + imageUri = it + binding.ivFotoBarangKlaim.setImageURI(it) + binding.ivFotoBarangKlaim.visibility = View.VISIBLE + binding.btnUnggahFotoKlaim.visibility = View.GONE + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + postId = it.getString("postId") + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = PenemuanFormKlaimBarangBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupSpinner() + setupClickListeners() + } + + private fun setupSpinner() { + val categories = resources.getStringArray(R.array.kategori_barang_array) + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, categories) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spinnerKategoriKlaim.adapter = adapter + } + + private fun setupClickListeners() { + binding.toolbarKlaim.setOnClickListener { findNavController().navigateUp() } + + binding.tvTanggalHilangKlaim.setOnClickListener { showDatePicker() } + binding.tvWaktuHilangKlaim.setOnClickListener { showTimePicker() } + + binding.btnUnggahFotoKlaim.setOnClickListener { pickImageLauncher.launch("image/*") } + binding.btnKirimKlaim.setOnClickListener { validateAndSubmitClaim() } + } + + private fun showDatePicker() { + val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + val dateFormat = SimpleDateFormat("dd MMMM yyyy", Locale("id", "ID")) + binding.tvTanggalHilangKlaim.text = dateFormat.format(calendar.time) + } + DatePickerDialog(requireContext(), dateSetListener, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH)).show() + } + + private fun showTimePicker() { + val timeSetListener = TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute -> + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) + calendar.set(Calendar.MINUTE, minute) + val timeFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) + binding.tvWaktuHilangKlaim.text = timeFormat.format(calendar.time) + " WIB" + } + TimePickerDialog(requireContext(), timeSetListener, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), true).show() + } + + private fun validateAndSubmitClaim() { + val namaPengklaim = binding.edtNamaKlaim.text.toString().trim() + val nimPengklaim = binding.edtNimKlaim.text.toString().trim() + val namaBarang = binding.edtNamaBarangKlaim.text.toString().trim() + val deskripsi = binding.edtDeskripsiKlaim.text.toString().trim() + + if (namaPengklaim.isEmpty() || nimPengklaim.isEmpty() || namaBarang.isEmpty() || deskripsi.isEmpty()) { + Toast.makeText(context, "Harap isi semua kolom wajib!", Toast.LENGTH_SHORT).show() + return + } + + if (postId == null) { + Toast.makeText(context, "Error: ID Postingan tidak ditemukan.", Toast.LENGTH_SHORT).show() + return + } + + binding.btnKirimKlaim.isEnabled = false + Toast.makeText(context, "Mengirim klaim...", Toast.LENGTH_SHORT).show() + + if (imageUri != null) { + uploadImageAndSaveClaim() + } else { + saveClaimDataToFirestore(null) + } + } + + private fun uploadImageAndSaveClaim() { + val storageRef = storage.reference.child("foto_klaim/${UUID.randomUUID()}.jpg") + imageUri?.let { + storageRef.putFile(it) + .addOnSuccessListener { + storageRef.downloadUrl.addOnSuccessListener { uri -> + saveClaimDataToFirestore(uri.toString()) + } + } + .addOnFailureListener { e -> + Toast.makeText(context, "Gagal mengunggah gambar: ${e.message}", Toast.LENGTH_SHORT).show() + binding.btnKirimKlaim.isEnabled = true + } + } + } + + private fun saveClaimDataToFirestore(imageUrl: String?) { + val user = auth.currentUser + if (user == null) { + Toast.makeText(context, "Anda harus login untuk membuat klaim.", Toast.LENGTH_SHORT).show() + binding.btnKirimKlaim.isEnabled = true + return + } + + val claimData = hashMapOf( + "uidPengklaim" to user.uid, + "namaPengklaim" to binding.edtNamaKlaim.text.toString().trim(), + "nimPengklaim" to binding.edtNimKlaim.text.toString().trim(), + "namaBarangKlaim" to binding.edtNamaBarangKlaim.text.toString().trim(), + "kategoriKlaim" to binding.spinnerKategoriKlaim.selectedItem.toString(), + "deskripsiKlaim" to binding.edtDeskripsiKlaim.text.toString().trim(), + "tanggalHilangKlaim" to binding.tvTanggalHilangKlaim.text.toString().trim(), + "waktuHilangKlaim" to binding.tvWaktuHilangKlaim.text.toString().trim(), + "lokasiHilangKlaim" to binding.edtLokasiKlaim.text.toString().trim(), + "imageUrlKlaim" to imageUrl, + "timestampKlaim" to System.currentTimeMillis(), + "statusKlaim" to "Menunggu Konfirmasi" + ) + + db.collection("form_penemuan").document(postId!!) + .collection("klaim_barang") + .add(claimData) + .addOnSuccessListener { + Toast.makeText(context, "Klaim berhasil dikirim!", Toast.LENGTH_LONG).show() + findNavController().navigateUp() + } + .addOnFailureListener { e -> + Toast.makeText(context, "Gagal mengirim klaim: ${e.message}", Toast.LENGTH_SHORT).show() + binding.btnKirimKlaim.isEnabled = true + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/PenemuanFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/PenemuanFragment.kt new file mode 100644 index 0000000..4914774 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/PenemuanFragment.kt @@ -0,0 +1,85 @@ +package com.androidprojek.unifind.ui.home + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.R +import com.androidprojek.unifind.adapter.OnItemClickListener +import com.androidprojek.unifind.adapter.PenemuanAdapter +import com.androidprojek.unifind.databinding.FragmentPenemuanBinding + +class PenemuanFragment : Fragment(), OnItemClickListener { + + private var _binding: FragmentPenemuanBinding? = null + private val binding get() = _binding!! + + private val homeViewModel: HomeViewModel by activityViewModels() + private lateinit var penemuanAdapter: PenemuanAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentPenemuanBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupRecyclerView() + observeViewModel() + setupFilterResultListener() + + binding.fabTambah.setOnClickListener { + findNavController().navigate(R.id.action_navigation_home_to_penemuan_post_form) + } + } + + private fun setupRecyclerView() { + // Saat membuat adapter di Beranda, 'isMyPostsPage' tidak perlu diisi (defaultnya false) + penemuanAdapter = PenemuanAdapter(mutableListOf(), this) + binding.rvBarang.apply { + layoutManager = LinearLayoutManager(context) + adapter = penemuanAdapter + } + } + + private fun observeViewModel() { + homeViewModel.filteredPenemuanList.observe(viewLifecycleOwner) { penemuanList -> + penemuanAdapter.updateData(penemuanList) + // Anda bisa tambahkan logika untuk menampilkan pesan kosong di sini + // binding.tvEmpty.visibility = if (penemuanList.isEmpty()) View.VISIBLE else View.GONE + } + } + + private fun setupFilterResultListener() { + parentFragmentManager.setFragmentResultListener("filter_request", this) { _, bundle -> + val hasilFilter = bundle.getStringArrayList("kategori_terpilih") + homeViewModel.setActiveCategories(hasilFilter ?: emptyList()) + } + } + + override fun onKlaimClick(postId: String) { + val bundle = Bundle().apply { + putString("postId", postId) + } + findNavController().navigate(R.id.action_navigation_home_to_penemuanFormKlaimBarangFragment, bundle) + } + + // --- TAMBAHKAN FUNGSI INI UNTUK MEMPERBAIKI ERROR --- + override fun onVerifikasiClick(postId: String) { + // Biarkan kosong. Tombol "Verifikasi" tidak akan pernah muncul di halaman Beranda, + // jadi fungsi ini tidak akan pernah dipanggil di sini. + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/adapter/HomePagerAdapter.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/adapter/HomePagerAdapter.kt new file mode 100644 index 0000000..65324ed --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/adapter/HomePagerAdapter.kt @@ -0,0 +1,18 @@ +package com.androidprojek.unifind.ui.home.adapter + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.androidprojek.unifind.ui.home.PenemuanFragment +import com.androidprojek.unifind.ui.home.PencarianFragment + +class HomePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { + override fun getItemCount() = 2 + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> PencarianFragment() + 1 -> PenemuanFragment() + else -> throw IllegalStateException("Invalid position $position") + } + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/penemuan_post_form.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/penemuan_post_form.kt new file mode 100644 index 0000000..2a9831c --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/penemuan_post_form.kt @@ -0,0 +1,185 @@ +package com.androidprojek.unifind.ui.home + +import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels // <-- PASTIKAN IMPORT INI ADA +import androidx.navigation.fragment.findNavController +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.PenemuanPostFormBinding +import com.androidprojek.unifind.viewmodel.NotificationMainViewModel // <-- PASTIKAN IMPORT INI ADA +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.storage.FirebaseStorage +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale +import java.util.UUID + +class penemuan_post_form : Fragment() { + + private var _binding: PenemuanPostFormBinding? = null + private val binding get() = _binding!! + + // <-- LANGKAH 1: TAMBAHKAN BARIS INI UNTUK MENDAPATKAN VIEWMODEL + private val sharedViewModel: NotificationMainViewModel by activityViewModels() + + private var imageUri: Uri? = null + private val calendar = Calendar.getInstance() + + private val db = FirebaseFirestore.getInstance() + private val storage = FirebaseStorage.getInstance() + private val auth = FirebaseAuth.getInstance() + + private val pickImageLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { + imageUri = it + binding.ivFotoBarang.setImageURI(it) + binding.ivFotoBarang.visibility = View.VISIBLE + binding.btnUnggahFoto.visibility = View.GONE + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = PenemuanPostFormBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupSpinner() + setupClickListeners() + } + + private fun setupSpinner() { + val categories = resources.getStringArray(R.array.kategori_barang_array) + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, categories) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spinnerKategori.adapter = adapter + } + + private fun setupClickListeners() { + binding.btnBack.setOnClickListener { findNavController().navigateUp() } + binding.fieldTanggal.setOnClickListener { showDatePicker() } + binding.tvTanggalPenemuan.setOnClickListener { showDatePicker() } + binding.fieldWaktu.setOnClickListener { showTimePicker() } + binding.tvWaktuPenemuan.setOnClickListener { showTimePicker() } + binding.btnUnggahFoto.setOnClickListener { pickImageLauncher.launch("image/*") } + binding.btnBuatPostingan.setOnClickListener { validateAndPost() } + } + + private fun showDatePicker() { + val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + val dateFormat = SimpleDateFormat("dd MMMM yyyy", Locale("id", "ID")) + binding.tvTanggalPenemuan.text = dateFormat.format(calendar.time) + } + DatePickerDialog(requireContext(), dateSetListener, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH)).show() + } + + private fun showTimePicker() { + val timeSetListener = TimePickerDialog.OnTimeSetListener { _, hourOfDay, minute -> + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) + calendar.set(Calendar.MINUTE, minute) + val timeFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) + binding.tvWaktuPenemuan.text = timeFormat.format(calendar.time) + " WIB" + } + TimePickerDialog(requireContext(), timeSetListener, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), true).show() + } + + private fun validateAndPost() { + val nama = binding.edtNama.text.toString().trim() + val nim = binding.edtNim.text.toString().trim() + val namaBarang = binding.edtNamaBarang.text.toString().trim() + val deskripsi = binding.edtDeskripsi.text.toString().trim() + val tanggal = binding.tvTanggalPenemuan.text.toString().trim() + val waktu = binding.tvWaktuPenemuan.text.toString().trim() + val lokasi = binding.edtLokasi.text.toString().trim() + + if (nama.isEmpty() || nim.isEmpty() || namaBarang.isEmpty() || deskripsi.isEmpty() || + tanggal.isEmpty() || tanggal.equals("Pilih tanggal", true) || + waktu.isEmpty() || waktu.equals("Pilih waktu", true) || + lokasi.isEmpty() || imageUri == null) { + Toast.makeText(context, "Semua kolom wajib diisi!", Toast.LENGTH_SHORT).show() + return + } + + binding.btnBuatPostingan.isEnabled = false + Toast.makeText(context, "Membuat postingan...", Toast.LENGTH_SHORT).show() + + uploadImageAndSaveData() + } + + private fun uploadImageAndSaveData() { + val storageRef = storage.reference.child("foto_penemuan/${UUID.randomUUID()}.jpg") + imageUri?.let { + storageRef.putFile(it) + .addOnSuccessListener { + storageRef.downloadUrl.addOnSuccessListener { uri -> + saveDataToFirestore(uri.toString()) + } + } + .addOnFailureListener { e -> + Toast.makeText(context, "Gagal mengunggah gambar: ${e.message}", Toast.LENGTH_SHORT).show() + binding.btnBuatPostingan.isEnabled = true + } + } + } + + private fun saveDataToFirestore(imageUrl: String) { + val uid = auth.currentUser?.uid ?: "" + + val postData = hashMapOf( + "uid" to uid, + "namaPelapor" to binding.edtNama.text.toString().trim(), + "nim" to binding.edtNim.text.toString().trim(), + "namaBarang" to binding.edtNamaBarang.text.toString().trim(), + "kategori" to binding.spinnerKategori.selectedItem.toString(), + "deskripsi" to binding.edtDeskripsi.text.toString().trim(), + "tanggalPenemuan" to binding.tvTanggalPenemuan.text.toString().trim(), + "waktuPenemuan" to binding.tvWaktuPenemuan.text.toString().trim(), + "lokasiPenemuan" to binding.edtLokasi.text.toString().trim(), + "imageUrl" to imageUrl, + "timestamp" to System.currentTimeMillis(), + "status" to "Dalam Pencarian" + ) + + db.collection("form_penemuan") + .add(postData) + .addOnSuccessListener { + // <-- LANGKAH 2: TAMBAHKAN BARIS INI SETELAH SUKSES + sharedViewModel.addPostSuccessNotification() + + Toast.makeText(context, "Postingan berhasil dibuat!", Toast.LENGTH_LONG).show() + findNavController().navigateUp() + } + .addOnFailureListener { e -> + Toast.makeText(context, "Gagal membuat postingan: ${e.message}", Toast.LENGTH_SHORT).show() + binding.btnBuatPostingan.isEnabled = true + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/home/penemuan_verifikasi.kt b/app/src/main/java/com/androidprojek/unifind/ui/home/penemuan_verifikasi.kt new file mode 100644 index 0000000..ab0d538 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/home/penemuan_verifikasi.kt @@ -0,0 +1,13 @@ +package com.androidprojek.unifind.ui.home + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.androidprojek.unifind.R + +class penemuan_verifikasi(contentLayoutId: Int) : AppCompatActivity(contentLayoutId) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.penemuan_verifikasi_main) // Layout untuk halaman form + } +} + diff --git a/app/src/main/java/com/androidprojek/unifind/ui/notifications/NotificationsFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/notifications/NotificationsFragment.kt new file mode 100644 index 0000000..e21ec19 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/notifications/NotificationsFragment.kt @@ -0,0 +1,71 @@ +package com.androidprojek.unifind.ui.notifications + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.adapter.NotificationAdapter +import com.androidprojek.unifind.databinding.FragmentNotificationsBinding +import com.androidprojek.unifind.viewmodel.NotificationMainViewModel + +class NotificationsFragment : Fragment() { + + private var _binding: FragmentNotificationsBinding? = null + private val binding get() = _binding!! + + // Dapatkan referensi ke ViewModel yang sama dengan fragment form + private val sharedViewModel: NotificationMainViewModel by activityViewModels() + + // Deklarasikan adapter di level kelas agar bisa diakses di beberapa fungsi + private lateinit var notificationAdapter: NotificationAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentNotificationsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupRecyclerView() + observeViewModel() // Mulai 'mendengarkan' perubahan dari ViewModel + } + + private fun setupRecyclerView() { + // Inisialisasi adapter dengan daftar kosong pada awalnya. + // Datanya akan diisi oleh observer. + notificationAdapter = NotificationAdapter(emptyList()) + binding.rvNotifications.apply { + layoutManager = LinearLayoutManager(context) + adapter = notificationAdapter + } + } + + private fun observeViewModel() { + sharedViewModel.notificationList.observe(viewLifecycleOwner) { notifications -> + // Cek apakah daftar notifikasi yang diterima kosong atau tidak + if (notifications.isEmpty()) { + // Jika KOSONG: + binding.rvNotifications.visibility = View.GONE // Sembunyikan RecyclerView + binding.tvEmptyNotification.visibility = View.VISIBLE // Tampilkan teks "kosong" + } else { + // Jika ADA ISINYA: + binding.rvNotifications.visibility = View.VISIBLE // Tampilkan RecyclerView + binding.tvEmptyNotification.visibility = View.GONE // Sembunyikan teks "kosong" + notificationAdapter.updateData(notifications) // Update data di adapter + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/notifications/NotificationsViewModel.kt b/app/src/main/java/com/androidprojek/unifind/ui/notifications/NotificationsViewModel.kt new file mode 100644 index 0000000..3beff8a --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/notifications/NotificationsViewModel.kt @@ -0,0 +1,13 @@ +package com.androidprojek.unifind.ui.notifications + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class NotificationsViewModel : ViewModel() { + + private val _text = MutableLiveData().apply { + value = "This is notifications Fragment" + } + val text: LiveData = _text +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/KontakActivity.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/KontakActivity.kt new file mode 100644 index 0000000..2f85695 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/KontakActivity.kt @@ -0,0 +1,108 @@ +package com.androidprojek.unifind.ui.profile + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.androidprojek.unifind.databinding.ActivityKontakBinding +import com.androidprojek.unifind.model.UserModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore + +class KontakActivity : AppCompatActivity() { + + // Gunakan ViewBinding agar lebih aman dan bersih + private lateinit var binding: ActivityKontakBinding + private lateinit var auth: FirebaseAuth + private lateinit var db: FirebaseFirestore + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityKontakBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Inisialisasi Firebase + auth = FirebaseAuth.getInstance() + db = FirebaseFirestore.getInstance() + + // Fungsi untuk tombol kembali di toolbar + binding.topAppBar.setNavigationOnClickListener { + finish() + } + + // Ambil data kontak yang sudah ada dan tampilkan + loadContactData() + + // Beri logika pada tombol simpan + binding.btnSimpan.setOnClickListener { + saveContactData() + } + } + + private fun loadContactData() { + setLoading(true) + val user = auth.currentUser + if (user == null) { + Toast.makeText(this, "Sesi tidak valid, silakan login ulang.", Toast.LENGTH_SHORT).show() + setLoading(false) + finish() + return + } + + // Ambil data dari collection "users" berdasarkan UID pengguna yang login + db.collection("users").document(user.uid).get() + .addOnSuccessListener { document -> + if (document.exists()) { + val userProfile = document.toObject(UserModel::class.java) + // Tampilkan data yang sudah ada ke EditText + binding.etInstagram.setText(userProfile?.instagram) + binding.etLine.setText(userProfile?.line) + binding.etWhatsapp.setText(userProfile?.whatsapp) + } + setLoading(false) + } + .addOnFailureListener { e -> + setLoading(false) + Toast.makeText(this, "Gagal memuat data kontak: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + private fun saveContactData() { + val instagram = binding.etInstagram.text.toString().trim() + val line = binding.etLine.text.toString().trim() + val whatsapp = binding.etWhatsapp.text.toString().trim() + + val user = auth.currentUser + if (user == null) { + Toast.makeText(this, "Sesi tidak valid, silakan login ulang.", Toast.LENGTH_SHORT).show() + return + } + + setLoading(true) + + // Siapkan data yang akan di-update dalam bentuk Map + val contactData = mapOf( + "instagram" to instagram, + "line" to line, + "whatsapp" to whatsapp + ) + + // Gunakan .update() untuk hanya mengubah field-field ini di dokumen pengguna + db.collection("users").document(user.uid) + .update(contactData) + .addOnSuccessListener { + setLoading(false) + Toast.makeText(this, "Kontak berhasil diperbarui!", Toast.LENGTH_SHORT).show() + finish() // Kembali ke halaman profil setelah berhasil + } + .addOnFailureListener { e -> + setLoading(false) + Toast.makeText(this, "Gagal memperbarui kontak: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + private fun setLoading(isLoading: Boolean) { + binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + binding.btnSimpan.isEnabled = !isLoading + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/LaporanMasukActivity.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/LaporanMasukActivity.kt new file mode 100644 index 0000000..0ac75c9 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/LaporanMasukActivity.kt @@ -0,0 +1,210 @@ +package com.androidprojek.unifind.ui.profile + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.R +import com.androidprojek.unifind.adapter.LaporanMasukAdapter +import com.androidprojek.unifind.databinding.ActivityLaporanMasukBinding +import com.androidprojek.unifind.model.LaporanPenemuanModel +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.Query + +class LaporanMasukActivity : AppCompatActivity() { + + private lateinit var binding: ActivityLaporanMasukBinding + private lateinit var adapter: LaporanMasukAdapter + private val listLaporan = mutableListOf() + private val db = FirebaseFirestore.getInstance() + private var idBarangHilang: String? = null + + companion object { + const val EXTRA_BARANG_ID = "extra_barang_id" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLaporanMasukBinding.inflate(layoutInflater) + setContentView(binding.root) + + idBarangHilang = intent.getStringExtra(EXTRA_BARANG_ID) + if (idBarangHilang.isNullOrEmpty()) { // Pemeriksaan yang lebih aman + Toast.makeText(this, "ID Barang tidak valid.", Toast.LENGTH_SHORT).show() + finish() + return + } + + setupToolbar() + setupRecyclerView() + fetchLaporanPenemuan() + } + + private fun setupToolbar() { + binding.topAppBar.setNavigationOnClickListener { finish() } + } + + private fun setupRecyclerView() { + adapter = LaporanMasukAdapter(listLaporan) + binding.rvDaftarLaporan.layoutManager = LinearLayoutManager(this) + binding.rvDaftarLaporan.adapter = adapter + + adapter.onDetailClickListener = { detailLaporan -> + val intent = Intent(this, VerifikasiLaporanMasukActivity::class.java).apply { + putExtra(VerifikasiLaporanMasukActivity.EXTRA_LAPORAN, detailLaporan) + } + startActivity(intent) + } + // --- IMPLEMENTASIKAN LISTENER BARU DI SINI --- + adapter.onHubungiClickListener = { laporan -> + showKontakDialog(laporan) + } + } + + // --- TAMBAHKAN FUNGSI BARU INI UNTUK MENAMPILKAN DIALOG --- + private fun showKontakDialog(laporan: LaporanPenemuanModel) { + val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_kontak_penemu, null) + val builder = AlertDialog.Builder(this) + builder.setView(dialogView) + + val dialog = builder.create() + + // Ambil view dari layout dialog + val btnClose = dialogView.findViewById(R.id.btn_close_dialog) + + val layoutInstagram = dialogView.findViewById(R.id.layout_instagram) + val tvInstagram = dialogView.findViewById(R.id.tv_instagram) + + val layoutLine = dialogView.findViewById(R.id.layout_line) + val tvLine = dialogView.findViewById(R.id.tv_line) + + val layoutWhatsapp = dialogView.findViewById(R.id.layout_whatsapp) + val tvWhatsapp = dialogView.findViewById(R.id.tv_whatsapp) + + // Setup tampilan kontak (kita 'pinjam' logika dari KontakPelaporActivity) + setupDialogContactView(layoutInstagram, tvInstagram, laporan.penemuInstagram) + setupDialogContactView(layoutLine, tvLine, laporan.penemuLine) + setupDialogContactView(layoutWhatsapp, tvWhatsapp, laporan.penemuWhatsapp) + + // Setup klik listener (kita 'pinjam' lagi logikanya) + setupDialogClickListeners( + layoutInstagram, laporan.penemuInstagram, + layoutLine, laporan.penemuLine, + layoutWhatsapp, laporan.penemuWhatsapp + ) + + btnClose.setOnClickListener { + dialog.dismiss() + } + + // Atur agar background dialog transparan sehingga CardView bisa terlihat rounded + dialog.window?.setBackgroundDrawableResource(android.R.color.transparent) + dialog.show() + } + + // Fungsi helper untuk dialog, diadaptasi dari KontakPelaporActivity + private fun setupDialogContactView(layout: View, textView: TextView, data: String?) { + if (!data.isNullOrEmpty()) { + layout.visibility = View.VISIBLE + textView.text = data + } else { + layout.visibility = View.GONE + } + } + + // Fungsi helper untuk dialog, diadaptasi dari KontakPelaporActivity + private fun setupDialogClickListeners( + layoutInstagram: View, instagram: String?, + layoutLine: View, line: String?, + layoutWhatsapp: View, whatsapp: String? + ) { + layoutInstagram.setOnClickListener { + if (!instagram.isNullOrEmpty()) { + val username = instagram.removePrefix("@") + val uri = Uri.parse("http://instagram.com/_u/$username") + val intent = Intent(Intent.ACTION_VIEW, uri) + try { + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "Aplikasi Instagram tidak terpasang.", Toast.LENGTH_SHORT).show() + } + } + } + + layoutLine.setOnClickListener { + if (!line.isNullOrEmpty()) { + val uri = Uri.parse("https://line.me/R/ti/p/~$line") + val intent = Intent(Intent.ACTION_VIEW, uri) + try { + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "Aplikasi Line tidak terpasang.", Toast.LENGTH_SHORT).show() + } + } + } + + layoutWhatsapp.setOnClickListener { + if (!whatsapp.isNullOrEmpty()) { + val formattedNumber = whatsapp.replaceFirst("^0", "").replace(Regex("[^0-9]"), "") + val url = "https://wa.me/62$formattedNumber" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + try { + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "Aplikasi WhatsApp tidak terpasang.", Toast.LENGTH_SHORT).show() + } + } + } + } + + private fun fetchLaporanPenemuan() { + binding.tvEmptyLaporan.visibility = View.VISIBLE // Tampilkan teks "kosong" di awal + binding.rvDaftarLaporan.visibility = View.GONE + + db.collection("barangHilang").document(idBarangHilang!!) + .collection("laporanPenemuan") + .orderBy("timestamp", Query.Direction.DESCENDING) // Query ini sekarang aman + .addSnapshotListener { snapshots, error -> + if (error != null) { + // Ganti Log Tag agar konsisten dengan nama Activity + Log.w("LaporanMasukActivity", "Error listening for documents.", error) + return@addSnapshotListener + } + + if (snapshots != null) { + listLaporan.clear() + + // --- PERUBAHAN LOGIKA PENGAMBILAN DATA --- + // Loop manual untuk mendapatkan ID setiap laporan + for (doc in snapshots.documents) { + val laporan = doc.toObject(LaporanPenemuanModel::class.java) + if (laporan != null) { + laporan.id = doc.id // Masukkan ID dokumen ke dalam model + listLaporan.add(laporan) + } + } + // --- AKHIR PERUBAHAN --- + + adapter.notifyDataSetChanged() + + // Atur visibilitas berdasarkan apakah list kosong atau tidak + if (listLaporan.isEmpty()) { + binding.tvEmptyLaporan.visibility = View.VISIBLE + binding.rvDaftarLaporan.visibility = View.GONE + } else { + binding.tvEmptyLaporan.visibility = View.GONE + binding.rvDaftarLaporan.visibility = View.VISIBLE + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfileFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfileFragment.kt new file mode 100644 index 0000000..cd2d6b0 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfileFragment.kt @@ -0,0 +1,183 @@ +package com.androidprojek.unifind.ui.profile + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import com.androidprojek.unifind.ui.profile.KontakActivity +import com.androidprojek.unifind.LoginActivity +import com.androidprojek.unifind.R +import com.androidprojek.unifind.databinding.FragmentProfileBinding +import com.androidprojek.unifind.model.UserModel +import com.bumptech.glide.Glide +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.storage.FirebaseStorage +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await +import java.util.concurrent.CancellationException + +class ProfileFragment : Fragment() { + + private var _binding: FragmentProfileBinding? = null + private val binding get() = _binding!! + + private lateinit var auth: FirebaseAuth + private lateinit var db: FirebaseFirestore + private lateinit var storage: FirebaseStorage + + private val imagePickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { uploadProfileImage(it) } + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfileBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + auth = FirebaseAuth.getInstance() + db = FirebaseFirestore.getInstance() + storage = FirebaseStorage.getInstance() + + loadUserProfile() + setupClickListeners() + } + + private fun setupClickListeners() { + binding.ivEditProfile.setOnClickListener { + val intent = Intent(Intent.ACTION_PICK).apply { type = "image/*" } + imagePickerLauncher.launch(intent) + } + + binding.btnPostinganSaya.setOnClickListener { + findNavController().navigate(R.id.action_profileFragment_to_profileMyPostsFragment) + } + + binding.btnKontak.setOnClickListener { + startActivity(Intent(requireContext(), KontakActivity::class.java)) + } + + // --- PERUBAHAN UTAMA DI SINI --- + binding.btnLacakFormulir.setOnClickListener { + // Menggunakan NavController untuk berpindah ke halaman Lacak Formulir + findNavController().navigate(R.id.action_profileFragment_to_profileLacakFormulirFragment) + } + // --- SELESAI PERUBAHAN --- + + binding.btnLogout.setOnClickListener { + showLogoutConfirmationDialog() + } + } + + private fun showLogoutConfirmationDialog() { + AlertDialog.Builder(requireContext()) + .setTitle("Konfirmasi Logout") + .setMessage("Apakah Anda yakin ingin keluar dari akun ini?") + .setPositiveButton("Ya, Keluar") { dialog, _ -> + auth.signOut() + val intent = Intent(requireContext(), LoginActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + startActivity(intent) + dialog.dismiss() + } + .setNegativeButton("Batal") { dialog, _ -> + dialog.dismiss() + } + .create() + .show() + } + + private fun loadUserProfile() { + val user = auth.currentUser ?: return + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + try { + val document = db.collection("users").document(user.uid).get().await() + + if (document.exists()) { + val userProfile = document.toObject(UserModel::class.java) + binding.tvNama.text = userProfile?.nama + binding.tvNim.text = userProfile?.nim + + if (userProfile?.photoUrl?.isNotEmpty() == true) { + Glide.with(this@ProfileFragment).load(userProfile.photoUrl).into(binding.ivProfile) + } else { + Glide.with(this@ProfileFragment).load(R.drawable.baseline_person_outline_24).into(binding.ivProfile) + } + } + } catch (e: CancellationException) { + // Ini normal terjadi saat pindah halaman, tidak perlu di-handle + } catch (e: Exception) { + Toast.makeText(context, "Gagal memuat profil.", Toast.LENGTH_SHORT).show() + } + } + } + } + + private fun uploadProfileImage(imageUri: Uri) { + setLoading(true) + val user = auth.currentUser ?: return + val storageRef = storage.reference.child("profile_pictures/${user.uid}") + + viewLifecycleOwner.lifecycleScope.launch { + try { + storageRef.putFile(imageUri).await() + val downloadUrl = storageRef.downloadUrl.await() + saveImageUrlToFirestore(downloadUrl.toString()) + } catch (e: Exception) { + setLoading(false) + Toast.makeText(context, "Gagal mengunggah foto: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + } + + private fun saveImageUrlToFirestore(imageUrl: String) { + val user = auth.currentUser ?: return + + viewLifecycleOwner.lifecycleScope.launch { + try { + db.collection("users").document(user.uid).update("photoUrl", imageUrl).await() + Toast.makeText(context, "Foto profil berhasil diperbarui.", Toast.LENGTH_SHORT).show() + Glide.with(this@ProfileFragment).load(imageUrl).into(binding.ivProfile) + } catch (e: Exception) { + Toast.makeText(context, "Gagal menyimpan URL foto: ${e.message}", Toast.LENGTH_SHORT).show() + } finally { + setLoading(false) + } + } + } + + private fun setLoading(isLoading: Boolean) { + // Pengecekan tambahan untuk mencegah crash + _binding?.let { + it.profileProgressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + it.ivEditProfile.isEnabled = !isLoading + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfileMyPostsFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfileMyPostsFragment.kt new file mode 100644 index 0000000..0833bb7 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfileMyPostsFragment.kt @@ -0,0 +1,49 @@ +package com.androidprojek.unifind.ui.profile + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.androidprojek.unifind.databinding.FragmentProfileMyPostsBinding +import com.google.android.material.tabs.TabLayoutMediator + +class ProfileMyPostsFragment : Fragment() { + + private var _binding: FragmentProfileMyPostsBinding? = null + private val binding get() = _binding!! + + private val tabTitles = arrayOf("Pencarian", "Penemuan") + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfileMyPostsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Setup ViewPager dengan PagerAdapter yang baru + val adapter = ProfilePostsPagerAdapter(this) + binding.viewPagerMyPosts.adapter = adapter + + // Hubungkan TabLayout dengan ViewPager + TabLayoutMediator(binding.tabLayoutMyPosts, binding.viewPagerMyPosts) { tab, position -> + tab.text = tabTitles[position] + }.attach() + + // Atur fungsi untuk tombol kembali di toolbar + binding.toolbarMyPosts.setNavigationOnClickListener { + findNavController().navigateUp() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfilePencarianListFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfilePencarianListFragment.kt new file mode 100644 index 0000000..ae1e291 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfilePencarianListFragment.kt @@ -0,0 +1,113 @@ +package com.androidprojek.unifind.ui.profile + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.adapter.PencarianPostinganSayaAdapter // Ganti ke adapter yang sesuai jika ada +import com.androidprojek.unifind.databinding.FragmentProfilePencarianListBinding +import com.androidprojek.unifind.model.BarangModel // Ganti ke model yang sesuai jika ada +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.Query + +class ProfilePencarianListFragment : Fragment() { + + private var _binding: FragmentProfilePencarianListBinding? = null + private val binding get() = _binding!! + + // Ganti ke adapter dan model yang sesuai untuk "Pencarian" + private lateinit var pencarianAdapter: PencarianPostinganSayaAdapter + private val listPencarian = mutableListOf() + + private val db = FirebaseFirestore.getInstance() + private val auth = FirebaseAuth.getInstance() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfilePencarianListBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + Log.d("FragmentLifecycle", "ProfilePencarianListFragment - onViewCreated DIPANGGIL!") + + super.onViewCreated(view, savedInstanceState) + + setupRecyclerView() + listenToMyPosts() + } + + private fun setupRecyclerView() { + pencarianAdapter = PencarianPostinganSayaAdapter(listPencarian) // Gunakan adapter "Pencarian" + binding.rvMyPencarian.apply { + layoutManager = LinearLayoutManager(context) + adapter = pencarianAdapter + } + + // Implementasikan listener-nya + pencarianAdapter.onLihatLaporanClickListener = { barang -> + // --- PERUBAHAN DI SINI --- + // Kita gunakan isNullOrEmpty() untuk memeriksa String? + if (!barang.id.isNullOrEmpty()) { + val intent = Intent(context, LaporanMasukActivity::class.java).apply { + putExtra(LaporanMasukActivity.EXTRA_BARANG_ID, barang.id) // Kirim ID barang + } + startActivity(intent) + } else { + Toast.makeText(context, "Gagal mendapatkan ID postingan.", Toast.LENGTH_SHORT).show() + } + } + } + + private fun listenToMyPosts() { + val currentUserUid = auth.currentUser?.uid + if (currentUserUid == null) { + binding.tvEmptyMyPencarian.visibility = View.VISIBLE + return + } + + // --- NAMA KOLEKSI DISESUAIKAN DI SINI --- + db.collection("barangHilang") // Mengambil dari koleksi "barangHilang" + .whereEqualTo("pelaporUid", currentUserUid) + .orderBy("timestamp", Query.Direction.DESCENDING) + .addSnapshotListener { snapshots, error -> + if (error != null) { + Log.w("ProfilePencarianList", "Listen failed.", error) + return@addSnapshotListener + } + + if (snapshots != null) { + listPencarian.clear() + // --- PERUBAHAN LOGIKA DI SINI --- + // Kita tidak langsung .toObjects(), tapi kita loop manual + for (doc in snapshots.documents) { + // 1. Ubah dokumen menjadi objek BarangModel + val barang = doc.toObject(BarangModel::class.java) + if (barang != null) { + // 2. Ambil ID dokumen dan masukkan ke field 'id' di model + barang.id = doc.id + + // 3. Tambahkan objek yang sudah lengkap ke dalam list + listPencarian.add(barang) + } + } + // --- AKHIR PERUBAHAN --- + pencarianAdapter.notifyDataSetChanged() + binding.tvEmptyMyPencarian.visibility = if (listPencarian.isEmpty()) View.VISIBLE else View.GONE + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfilePenemuanListFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfilePenemuanListFragment.kt new file mode 100644 index 0000000..4984369 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfilePenemuanListFragment.kt @@ -0,0 +1,110 @@ +package com.androidprojek.unifind.ui.profile + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.R +import com.androidprojek.unifind.adapter.OnItemClickListener +import com.androidprojek.unifind.adapter.PenemuanAdapter +import com.androidprojek.unifind.databinding.FragmentProfilePenemuanListBinding +import com.androidprojek.unifind.model.PenemuanModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.Query + +class ProfilePenemuanListFragment : Fragment(), OnItemClickListener { + + private var _binding: FragmentProfilePenemuanListBinding? = null + private val binding get() = _binding!! + + private lateinit var penemuanAdapter: PenemuanAdapter + private val listPenemuan = mutableListOf() + private val db = FirebaseFirestore.getInstance() + private val auth = FirebaseAuth.getInstance() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfilePenemuanListBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + listenToMyPosts() + } + + private fun setupRecyclerView() { + penemuanAdapter = PenemuanAdapter(listPenemuan, this, true) + binding.rvMyPenemuan.apply { + layoutManager = LinearLayoutManager(context) + adapter = penemuanAdapter + } + } + + private fun listenToMyPosts() { + val currentUserUid = auth.currentUser?.uid + if (currentUserUid == null) { + binding.tvEmptyMyPenemuan.visibility = View.VISIBLE + return + } + + db.collection("form_penemuan") + .whereEqualTo("uid", currentUserUid) + .orderBy("timestamp", Query.Direction.DESCENDING) + .addSnapshotListener { snapshots, error -> + if (error != null) { + Log.w("ProfilePenemuanList", "Listen failed.", error) + return@addSnapshotListener + } + + if (snapshots != null) { + listPenemuan.clear() + for (doc in snapshots.documents) { + val penemuan = doc.toObject(PenemuanModel::class.java) + if (penemuan != null) { + penemuan.id = doc.id + listPenemuan.add(penemuan) + } + } + penemuanAdapter.notifyDataSetChanged() + binding.tvEmptyMyPenemuan.visibility = if (listPenemuan.isEmpty()) View.VISIBLE else View.GONE + } + } + } + + // Fungsi ini tidak akan pernah terpanggil di halaman ini, + // tapi tetap harus ada untuk memenuhi kontrak interface. + override fun onKlaimClick(postId: String) { + // Biarkan kosong + } + + // --- PERUBAHAN UTAMA DI SINI --- + // Fungsi ini yang akan berjalan saat tombol "Verifikasi" diklik. + override fun onVerifikasiClick(postId: String) { + // 1. Buat Bundle untuk menampung data postId + val bundle = Bundle().apply { + putString("postId", postId) + } + + // 2. Navigasi menggunakan ID dari action dan bundle yang sudah dibuat + try { + findNavController().navigate(R.id.action_profileMyPostsFragment_to_penemuanVerifikasiPemilikFragment, bundle) + } catch (e: Exception) { + Toast.makeText(context, "Navigasi gagal: ${e.message}", Toast.LENGTH_LONG).show() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfilePostsPagerAdapter.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfilePostsPagerAdapter.kt new file mode 100644 index 0000000..c3438c5 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/ProfilePostsPagerAdapter.kt @@ -0,0 +1,19 @@ +package com.androidprojek.unifind.ui.profile + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter + +class ProfilePostsPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { + + // Jumlah total tab + override fun getItemCount(): Int = 2 + + // Menentukan fragment mana yang akan ditampilkan untuk setiap posisi tab + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> ProfilePencarianListFragment() // Untuk tab "Pencarian" + 1 -> ProfilePenemuanListFragment() // Untuk tab "Penemuan" + else -> throw IllegalStateException("Posisi tab tidak valid: $position") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/VerifikasiLaporanMasukActivity.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/VerifikasiLaporanMasukActivity.kt new file mode 100644 index 0000000..4175c3e --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/VerifikasiLaporanMasukActivity.kt @@ -0,0 +1,124 @@ +package com.androidprojek.unifind.ui.profile + +import android.os.Build +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.androidprojek.unifind.R +import com.androidprojek.unifind.adapter.ImageSliderAdapter +import com.androidprojek.unifind.databinding.ActivityVerifikasiLaporanMasukBinding +import com.androidprojek.unifind.model.LaporanPenemuanModel +import com.google.firebase.firestore.FirebaseFirestore + +class VerifikasiLaporanMasukActivity : AppCompatActivity() { + + private lateinit var binding: ActivityVerifikasiLaporanMasukBinding + private var laporan: LaporanPenemuanModel? = null + private val db = FirebaseFirestore.getInstance() + + companion object { + const val EXTRA_LAPORAN = "extra_laporan" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityVerifikasiLaporanMasukBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Ambil data laporan LENGKAP dari intent (karena sudah dibuat Parcelable) + laporan = if (Build.VERSION.SDK_INT >= 33) { + intent.getParcelableExtra(EXTRA_LAPORAN, LaporanPenemuanModel::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(EXTRA_LAPORAN) + } + + if (laporan == null) { + Toast.makeText(this, "Gagal memuat data laporan.", Toast.LENGTH_SHORT).show() + finish() + return + } + + setupToolbar() + // Cukup panggil satu fungsi untuk menampilkan semua data yang sudah kita miliki + bindDataToViews(laporan!!) + setupActionButtons() + } + + private fun setupToolbar() { + binding.topAppBar.setNavigationOnClickListener { finish() } + } + + // --- FUNGSI INI SEKARANG MENAMPILKAN SEMUA DATA DARI OBJEK 'laporan' --- + private fun bindDataToViews(laporan: LaporanPenemuanModel) { + binding.apply { + // Mengisi data penemu + tvVerifikasiNamaPenemu.text = laporan.penemuNama + tvVerifikasiNimPenemu.text = laporan.penemuNim + + // Mengisi data detail laporan penemuan + tvVerifikasiTanggal.text = laporan.tanggalTemuan + tvVerifikasiWaktu.text = laporan.waktuTemuan + tvVerifikasiLokasi.text = laporan.lokasiTemuan + tvVerifikasiDeskripsi.text = laporan.deskripsiTambahan.ifEmpty { "Tidak ada deskripsi tambahan." } + + // Field-field ini tidak memiliki data di LaporanPenemuanModel, jadi kita sembunyikan saja + // atau Anda bisa hapus dari file XML jika mau. + tvVerifikasiNamaBarang.visibility = View.GONE + findViewById(R.id.label_nama_barang).visibility = View.GONE // Asumsikan Anda memberi ID pada labelnya + tvVerifikasiKategori.visibility = View.GONE + findViewById(R.id.label_kategori).visibility = View.GONE // Asumsikan Anda memberi ID pada labelnya + + // Logika untuk menampilkan foto bukti atau teks "kosong" + if (laporan.fotoLaporanUris.isNotEmpty()) { + viewPagerBukti.visibility = View.VISIBLE + dotsIndicatorBukti.visibility = View.VISIBLE + tvFotoBuktiKosong.visibility = View.GONE + + viewPagerBukti.adapter = ImageSliderAdapter(laporan.fotoLaporanUris) + dotsIndicatorBukti.attachTo(viewPagerBukti) + } else { + viewPagerBukti.visibility = View.GONE + dotsIndicatorBukti.visibility = View.GONE + tvFotoBuktiKosong.visibility = View.VISIBLE + } + } + } + + private fun setupActionButtons() { + binding.btnTolak.setOnClickListener { + updateStatusLaporan("Ditolak") + } + binding.btnSetujui.setOnClickListener { + updateStatusLaporan("Disetujui") + } + } + + private fun updateStatusLaporan(newStatus: String) { + if (laporan?.id.isNullOrEmpty() || laporan?.idBarangHilang.isNullOrEmpty()) { + Toast.makeText(this, "ID Laporan atau Barang tidak valid.", Toast.LENGTH_SHORT).show() + return + } + + val reportRef = db.collection("barangHilang").document(laporan!!.idBarangHilang) + .collection("laporanPenemuan").document(laporan!!.id!!) + + val batch = db.batch() + batch.update(reportRef, "statusLaporan", newStatus) + + if (newStatus == "Disetujui") { + val barangRef = db.collection("barangHilang").document(laporan!!.idBarangHilang) + batch.update(barangRef, "status", "Ditemukan") + } + + batch.commit() + .addOnSuccessListener { + Toast.makeText(this, "Laporan berhasil diubah menjadi '$newStatus'", Toast.LENGTH_LONG).show() + finish() + } + .addOnFailureListener { e -> + Toast.makeText(this, "Gagal mengupdate status: ${e.message}", Toast.LENGTH_SHORT).show() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/LacakDetailJawabanFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/LacakDetailJawabanFragment.kt new file mode 100644 index 0000000..afc341d --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/LacakDetailJawabanFragment.kt @@ -0,0 +1,76 @@ +package com.androidprojek.unifind.ui.profile.lacakformulir + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.androidprojek.unifind.databinding.FragmentLacakDetailJawabanBinding +import com.androidprojek.unifind.model.PenemuanKlaimModel +import com.bumptech.glide.Glide + +class LacakDetailJawabanFragment : Fragment() { + + private var _binding: FragmentLacakDetailJawabanBinding? = null + private val binding get() = _binding!! + + // Properti untuk menampung data klaim yang diterima + private var dataKlaim: PenemuanKlaimModel? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Ambil data Parcelable dari argumen + arguments?.let { + dataKlaim = it.getParcelable("dataKlaim") + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentLacakDetailJawabanBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Panggil fungsi untuk mengisi data ke tampilan + populateView() + + // Atur listener untuk tombol kembali + binding.toolbarDetailJawaban.setNavigationOnClickListener { + findNavController().navigateUp() + } + } + + private fun populateView() { + dataKlaim?.let { klaim -> + binding.tvLacakDetailNama.text = klaim.namaPengklaim ?: "-" + binding.tvLacakDetailNim.text = klaim.nimPengklaim ?: "-" + binding.tvLacakDetailNamaBarang.text = klaim.namaBarangKlaim ?: "-" + binding.tvLacakDetailKategori.text = klaim.kategoriKlaim ?: "-" + binding.tvLacakDetailDeskripsi.text = klaim.deskripsiKlaim ?: "-" + binding.tvLacakDetailTanggal.text = klaim.tanggalHilangKlaim ?: "-" + binding.tvLacakDetailWaktu.text = klaim.waktuHilangKlaim ?: "-" + binding.tvLacakDetailLokasi.text = klaim.lokasiHilangKlaim ?: "-" + + // Cek apakah ada URL foto bukti + if (klaim.imageUrlKlaim.isNullOrEmpty()) { + binding.ivLacakDetailFoto.visibility = View.GONE + binding.tvLacakDetailFotoKosong.visibility = View.VISIBLE + } else { + binding.ivLacakDetailFoto.visibility = View.VISIBLE + binding.tvLacakDetailFotoKosong.visibility = View.GONE + Glide.with(this).load(klaim.imageUrlKlaim).into(binding.ivLacakDetailFoto) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakFormulirFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakFormulirFragment.kt new file mode 100644 index 0000000..5b8bd40 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakFormulirFragment.kt @@ -0,0 +1,50 @@ +package com.androidprojek.unifind.ui.profile.lacakformulir + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.androidprojek.unifind.databinding.FragmentProfileLacakFormulirBinding +import com.google.android.material.tabs.TabLayoutMediator + +class ProfileLacakFormulirFragment : Fragment() { + + private var _binding: FragmentProfileLacakFormulirBinding? = null + private val binding get() = _binding!! + + private val tabTitles = arrayOf("Pencarian", "Penemuan") + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfileLacakFormulirBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Setup ViewPager dengan PagerAdapter baru + // TODO: Buat ProfileLacakPagerAdapter di langkah berikutnya + val adapter = ProfileLacakPagerAdapter(this) + binding.viewPagerLacakFormulir.adapter = adapter + + // Hubungkan TabLayout dengan ViewPager + TabLayoutMediator(binding.tabLayoutLacakFormulir, binding.viewPagerLacakFormulir) { tab, position -> + tab.text = tabTitles[position] + }.attach() + + // Atur fungsi untuk tombol kembali di toolbar + binding.toolbarLacakFormulir.setNavigationOnClickListener { + findNavController().navigateUp() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakPagerAdapter.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakPagerAdapter.kt new file mode 100644 index 0000000..005dd49 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakPagerAdapter.kt @@ -0,0 +1,27 @@ +package com.androidprojek.unifind.ui.profile.lacakformulir + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter + +class ProfileLacakPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { + + // Jumlah total tab yang akan kita tampilkan + override fun getItemCount(): Int = 2 + + /** + * Fungsi ini menentukan fragment mana yang akan ditampilkan untuk setiap posisi tab. + * Posisi 0 adalah tab pertama (kiri), Posisi 1 adalah tab kedua (kanan), dan seterusnya. + */ + override fun createFragment(position: Int): Fragment { + return when (position) { + // Untuk tab "Pencarian" (posisi 0), kita tampilkan Fragment kosong untuk sementara. + 0 -> ProfileLacakPencarianListFragment() + + // Untuk tab "Penemuan" (posisi 1), kita tampilkan fragment yang sudah kita buat. + 1 -> ProfileLacakPenemuanListFragment() + + // Pengaman jika ada posisi yang tidak valid. + else -> throw IllegalStateException("Posisi tab tidak valid: $position") + } + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakPencarianListFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakPencarianListFragment.kt new file mode 100644 index 0000000..7d4c1c0 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakPencarianListFragment.kt @@ -0,0 +1,120 @@ +package com.androidprojek.unifind.ui.profile.lacakformulir + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.adapter.PencarianLacakAdapter +import com.androidprojek.unifind.databinding.FragmentProfileLacakPencarianListBinding +import com.androidprojek.unifind.model.BarangModel +import com.androidprojek.unifind.model.LaporanPenemuanModel +import com.androidprojek.unifind.model.PencarianLacakFormulirModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ListenerRegistration +import com.google.firebase.firestore.Query +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await + +class ProfileLacakPencarianListFragment : Fragment() { + + private var _binding: FragmentProfileLacakPencarianListBinding? = null + private val binding get() = _binding!! + + private lateinit var lacakAdapter: PencarianLacakAdapter + private val db = FirebaseFirestore.getInstance() + private val auth = FirebaseAuth.getInstance() + private var firestoreListener: ListenerRegistration? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + // Ganti nama binding class sesuai nama file XML baru Anda jika berbeda + _binding = FragmentProfileLacakPencarianListBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + listenToMySubmittedReports() + } + + private fun setupRecyclerView() { + lacakAdapter = PencarianLacakAdapter(emptyList()) + // Ganti ID RecyclerView sesuai nama file XML baru Anda jika berbeda + binding.rvLacakPencarian.apply { + layoutManager = LinearLayoutManager(context) + adapter = lacakAdapter + } + } + + private fun listenToMySubmittedReports() { + val currentUserUid = auth.currentUser?.uid + if (currentUserUid == null) { + binding.tvEmptyLacak.visibility = View.VISIBLE + return + } + + binding.progressBarLacak.visibility = View.VISIBLE + binding.tvEmptyLacak.visibility = View.GONE + + val query = db.collectionGroup("laporanPenemuan") + .whereEqualTo("penemuUid", currentUserUid) + .orderBy("timestamp", Query.Direction.DESCENDING) + + firestoreListener = query.addSnapshotListener { snapshots, error -> + if (_binding == null) return@addSnapshotListener + binding.progressBarLacak.visibility = View.GONE + + if (error != null) { + Log.w("LacakPencarian", "Listen failed.", error) + return@addSnapshotListener + } + + if (snapshots != null) { + viewLifecycleOwner.lifecycleScope.launch { + val lacakItems = snapshots.documents.mapNotNull { laporanDoc -> + val postinganRef = laporanDoc.reference.parent.parent + if (postinganRef != null) { + try { + val postinganDoc = postinganRef.get().await() + if (postinganDoc.exists()) { + val postingan = postinganDoc.toObject(BarangModel::class.java) + val laporan = laporanDoc.toObject(LaporanPenemuanModel::class.java) + + if (postingan != null && laporan != null) { + // --- PERUBAHAN DI SINI --- + PencarianLacakFormulirModel( + namaBarang = postingan.namaBarang, + namaPoster = postingan.nama, + // Ambil URL gambar pertama, jika ada + imageUrlPostingan = if (postingan.fotoUris.isNotEmpty()) postingan.fotoUris[0] else null, + statusLaporan = laporan.statusLaporan + ) + } else null + } else null + } catch (e: Exception) { + Log.e("LacakPencarian", "Gagal mengambil data postingan induk", e) + null + } + } else null + } + lacakAdapter.updateData(lacakItems) + binding.tvEmptyLacak.visibility = if (lacakItems.isEmpty()) View.VISIBLE else View.GONE + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + firestoreListener?.remove() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakPenemuanListFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakPenemuanListFragment.kt new file mode 100644 index 0000000..c910b77 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/lacakformulir/ProfileLacakPenemuanListFragment.kt @@ -0,0 +1,125 @@ +package com.androidprojek.unifind.ui.profile.lacakformulir + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.adapter.LacakFormulirAdapter +import com.androidprojek.unifind.databinding.FragmentProfileLacakPenemuanListBinding +import com.androidprojek.unifind.model.PenemuanKlaimModel +import com.androidprojek.unifind.model.PenemuanLacakFormulirModel +import com.androidprojek.unifind.model.PenemuanModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ListenerRegistration +import com.google.firebase.firestore.Query +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await + +class ProfileLacakPenemuanListFragment : Fragment() { + + private var _binding: FragmentProfileLacakPenemuanListBinding? = null + private val binding get() = _binding!! + + private lateinit var lacakAdapter: LacakFormulirAdapter + private val listLacak = mutableListOf() + + private val db = FirebaseFirestore.getInstance() + private val auth = FirebaseAuth.getInstance() + private var firestoreListener: ListenerRegistration? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfileLacakPenemuanListBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + listenToMyActiveClaims() + } + + private fun setupRecyclerView() { + // Logika ini sudah benar, menggunakan lambda untuk menangani klik. + // Kita akan sesuaikan navigasinya nanti. + lacakAdapter = LacakFormulirAdapter(listLacak) + binding.rvLacakPenemuan.apply { + layoutManager = LinearLayoutManager(context) + adapter = lacakAdapter + } + } + + private fun listenToMyActiveClaims() { + val currentUserUid = auth.currentUser?.uid + if (currentUserUid == null) { + binding.tvEmptyLacak.visibility = View.VISIBLE + return + } + + binding.progressBarLacak.visibility = View.VISIBLE + binding.tvEmptyLacak.visibility = View.GONE + + // --- PERUBAHAN UTAMA DI SINI --- + // Kita hapus filter 'whereEqualTo("statusKlaim", "Menunggu Konfirmasi")' + // agar semua klaim milik pengguna (apapun statusnya) akan diambil. + val query = db.collectionGroup("klaim_barang") + .whereEqualTo("uidPengklaim", currentUserUid) + + firestoreListener = query.addSnapshotListener { snapshots, error -> + if (_binding == null) return@addSnapshotListener + binding.progressBarLacak.visibility = View.GONE + + if (error != null) { + Log.w("LacakFragment", "Listen failed.", error) + return@addSnapshotListener + } + + if (snapshots != null) { + viewLifecycleOwner.lifecycleScope.launch { + val lacakItems = snapshots.documents.mapNotNull { claimDoc -> + val postinganRef = claimDoc.reference.parent.parent + if (postinganRef != null) { + try { + val postinganDoc = postinganRef.get().await() + if (postinganDoc.exists()) { + val postingan = postinganDoc.toObject(PenemuanModel::class.java) + val klaim = claimDoc.toObject(PenemuanKlaimModel::class.java) + + if (postingan != null && klaim != null) { + PenemuanLacakFormulirModel( + postId = postinganDoc.id, + namaBarangPostingan = postingan.namaBarang, + imageUrlPostingan = postingan.imageUrl, + namaPenemu = postingan.namaPelapor, + klaimId = claimDoc.id, + statusKlaim = klaim.statusKlaim, + detailKlaim = klaim + ) + } else null + } else null + } catch (e: Exception) { + Log.e("LacakFragment", "Gagal mengambil data postingan induk", e) + null + } + } else null + } + lacakAdapter.updateData(lacakItems) + binding.tvEmptyLacak.visibility = if (lacakItems.isEmpty()) View.VISIBLE else View.GONE + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + firestoreListener?.remove() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/verifikasi/PenemuanVerifikasiDetailJawabanFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/verifikasi/PenemuanVerifikasiDetailJawabanFragment.kt new file mode 100644 index 0000000..d1e7953 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/verifikasi/PenemuanVerifikasiDetailJawabanFragment.kt @@ -0,0 +1,127 @@ +package com.androidprojek.unifind.ui.profile.verifikasi + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.androidprojek.unifind.databinding.PenemuanVerifikasiDetailJawabanBinding +import com.androidprojek.unifind.model.PenemuanKlaimModel +import com.bumptech.glide.Glide + +class PenemuanVerifikasiDetailJawabanFragment : Fragment() { + + private var _binding: PenemuanVerifikasiDetailJawabanBinding? = null + private val binding get() = _binding!! + + // --- 1. INISIALISASI VIEWMODEL --- + private val viewModel: VerifikasiViewModel by viewModels() + + private var dataKlaim: PenemuanKlaimModel? = null + // Tambahkan properti untuk postId, kita akan butuh ini + private var postId: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Ambil data yang dikirim dari halaman sebelumnya + arguments?.let { + postId = it.getString("postId") + dataKlaim = it.getParcelable("dataKlaim") + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = PenemuanVerifikasiDetailJawabanBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + populateView() + setupClickListeners() + observeViewModel() // Mulai mengamati perubahan dari ViewModel + } + + // --- 2. FUNGSI BARU UNTUK MENGAMATI HASIL PROSES --- + private fun observeViewModel() { + viewModel.prosesSelesai.observe(viewLifecycleOwner) { selesai -> + if (selesai) { + Toast.makeText(context, "Verifikasi berhasil!", Toast.LENGTH_SHORT).show() + // Kembali ke halaman daftar pengklaim + findNavController().popBackStack() + } + } + viewModel.errorMessage.observe(viewLifecycleOwner) { error -> + if(error.isNotEmpty()){ + Toast.makeText(context, error, Toast.LENGTH_LONG).show() + } + } + } + + private fun populateView() { + dataKlaim?.let { klaim -> + binding.toolbarDetailJawaban.title = "Jawaban dari ${klaim.namaPengklaim}" + binding.tvDetailNamaPengklaim.text = klaim.namaPengklaim ?: "-" + binding.tvDetailNimPengklaim.text = klaim.nimPengklaim ?: "-" + binding.chipDetailStatusVerifikasi.text = klaim.statusKlaim ?: "Status Tidak Ada" + + binding.tvDetailNamaBarang.text = klaim.namaBarangKlaim ?: "-" + binding.tvDetailKategori.text = klaim.kategoriKlaim ?: "-" + binding.tvDetailDeskripsi.text = klaim.deskripsiKlaim ?: "-" + binding.tvDetailTanggal.text = klaim.tanggalHilangKlaim ?: "-" + binding.tvDetailWaktu.text = klaim.waktuHilangKlaim ?: "-" + binding.tvDetailLokasi.text = klaim.lokasiHilangKlaim ?: "-" + + if (klaim.imageUrlKlaim.isNullOrEmpty()) { + binding.ivDetailFoto.visibility = View.GONE + binding.tvDetailFotoKosong.visibility = View.VISIBLE + } else { + binding.ivDetailFoto.visibility = View.VISIBLE + binding.tvDetailFotoKosong.visibility = View.GONE + Glide.with(this).load(klaim.imageUrlKlaim).into(binding.ivDetailFoto) + } + } + } + + private fun setupClickListeners() { + binding.toolbarDetailJawaban.setNavigationOnClickListener { + findNavController().navigateUp() + } + + // --- 3. UBAH LOGIKA TOMBOL TERIMA --- + binding.btnTerimaKlaim.setOnClickListener { + showConfirmationDialog() + } + } + + private fun showConfirmationDialog() { + val currentPostId = postId + val currentKlaim = dataKlaim + if (currentPostId != null && currentKlaim?.id != null) { + AlertDialog.Builder(requireContext()) + .setTitle("Konfirmasi Tindakan") + .setMessage("Apakah Anda yakin ingin menerima klaim dari ${currentKlaim.namaPengklaim}? Tindakan ini akan menolak klaim lainnya dan tidak dapat diubah.") + .setPositiveButton("Ya, Terima") { dialog, _ -> + // Panggil fungsi di ViewModel untuk memulai proses + viewModel.terimaKlaim(currentPostId, currentKlaim.id!!) + dialog.dismiss() + } + .setNegativeButton("Batal", null) + .show() + } else { + Toast.makeText(context, "Error: Data tidak lengkap untuk verifikasi.", Toast.LENGTH_SHORT).show() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/verifikasi/PenemuanVerifikasiPemilikFragment.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/verifikasi/PenemuanVerifikasiPemilikFragment.kt new file mode 100644 index 0000000..af184df --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/verifikasi/PenemuanVerifikasiPemilikFragment.kt @@ -0,0 +1,135 @@ +package com.androidprojek.unifind.ui.profile.verifikasi + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.androidprojek.unifind.R +import com.androidprojek.unifind.adapter.PenemuanPengklaimAdapter +import com.androidprojek.unifind.databinding.ProfilePenemuanVerifikasiPemilikBinding +import com.androidprojek.unifind.model.PenemuanKlaimModel +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ListenerRegistration +import com.google.firebase.firestore.Query + +// --- HAPUS IMPLEMENTASI INTERFACE DARI SINI --- +class PenemuanVerifikasiPemilikFragment : Fragment() { + + private var _binding: ProfilePenemuanVerifikasiPemilikBinding? = null + private val binding get() = _binding!! + + private var postId: String? = null + + private lateinit var pengklaimAdapter: PenemuanPengklaimAdapter + private val listKlaim = mutableListOf() + + private val db = FirebaseFirestore.getInstance() + private var firestoreListener: ListenerRegistration? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + postId = it.getString("postId") + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = ProfilePenemuanVerifikasiPemilikBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupRecyclerView() + listenToClaims() + + binding.toolbarVerifikasi.setNavigationOnClickListener { + findNavController().navigateUp() + } + } + + private fun setupRecyclerView() { + // --- PERUBAHAN UTAMA SAAT MEMBUAT ADAPTER --- + pengklaimAdapter = PenemuanPengklaimAdapter(listKlaim, + // Lambda untuk "Lihat Jawaban" + onLihatJawaban = { klaim -> + navigateToDetailJawaban(klaim) + }, + // Lambda untuk "Kontak" + onKontak = { klaim -> + Toast.makeText(requireContext(), "Kontak ${klaim.namaPengklaim}", Toast.LENGTH_SHORT).show() + } + ) + binding.rvPengklaim.apply { + layoutManager = LinearLayoutManager(context) + adapter = pengklaimAdapter + } + } + + private fun listenToClaims() { + if (postId == null) { + Log.e("VerifikasiFragment", "Error: Post ID tidak ditemukan.") + binding.tvEmptyVerifikasi.visibility = View.VISIBLE + binding.tvEmptyVerifikasi.text = "Error: ID Postingan tidak valid." + return + } + + binding.progressBarVerifikasi.visibility = View.VISIBLE + binding.tvEmptyVerifikasi.visibility = View.GONE + + val query = db.collection("form_penemuan").document(postId!!) + .collection("klaim_barang") + .orderBy("timestampKlaim", Query.Direction.ASCENDING) + + firestoreListener = query.addSnapshotListener { snapshots, error -> + if (_binding == null) return@addSnapshotListener + + binding.progressBarVerifikasi.visibility = View.GONE + + if (error != null) { + Log.w("VerifikasiFragment", "Listen failed.", error) + return@addSnapshotListener + } + + if (snapshots != null) { + listKlaim.clear() + for (doc in snapshots.documents) { + val klaim = doc.toObject(PenemuanKlaimModel::class.java) + if (klaim != null) { + klaim.id = doc.id + listKlaim.add(klaim) + } + } + pengklaimAdapter.notifyDataSetChanged() + + binding.tvEmptyVerifikasi.visibility = if (listKlaim.isEmpty()) View.VISIBLE else View.GONE + } + } + } + + private fun navigateToDetailJawaban(klaim: PenemuanKlaimModel) { + val bundle = Bundle().apply { + // Kita juga perlu mengirim postId agar di halaman detail kita tahu postingan mana yang di-update + putString("postId", postId) + putParcelable("dataKlaim", klaim) + } + findNavController().navigate(R.id.action_verifikasiPemilik_to_detailJawaban, bundle) + } + + // Fungsi onLihatJawabanClicked dan onKontakClicked tidak lagi diperlukan di sini + + override fun onDestroyView() { + super.onDestroyView() + firestoreListener?.remove() + _binding = null + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/ui/profile/verifikasi/VerifikasiViewModel.kt b/app/src/main/java/com/androidprojek/unifind/ui/profile/verifikasi/VerifikasiViewModel.kt new file mode 100644 index 0000000..812d376 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/ui/profile/verifikasi/VerifikasiViewModel.kt @@ -0,0 +1,63 @@ +package com.androidprojek.unifind.ui.profile.verifikasi + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await + +class VerifikasiViewModel : ViewModel() { + + private val db = FirebaseFirestore.getInstance() + + // LiveData untuk memberi tahu Fragment jika proses selesai + private val _prosesSelesai = MutableLiveData() + val prosesSelesai: LiveData = _prosesSelesai + + // LiveData untuk menampilkan pesan error + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData = _errorMessage + + fun terimaKlaim(postId: String, klaimDiterimaId: String) { + viewModelScope.launch { + try { + val klaimRef = db.collection("form_penemuan").document(postId) + .collection("klaim_barang") + + // 1. Dapatkan semua klaim untuk postingan ini + val semuaKlaimSnapshot = klaimRef.get().await() + + // Mulai operasi batch (semua berhasil atau semua gagal) + val batch = db.batch() + + // 2. Loop melalui semua klaim + for (dokumenKlaim in semuaKlaimSnapshot.documents) { + if (dokumenKlaim.id == klaimDiterimaId) { + // Jika ini adalah klaim yang diterima, update statusnya + batch.update(dokumenKlaim.reference, "statusKlaim", "Diterima") + } else { + // Jika bukan, tolak semua klaim lainnya + batch.update(dokumenKlaim.reference, "statusKlaim", "Ditolak") + } + } + + // 3. Update status postingan utama menjadi "Selesai" + val postinganUtamaRef = db.collection("form_penemuan").document(postId) + batch.update(postinganUtamaRef, "status", "Selesai") + + // 4. Jalankan semua operasi sekaligus + batch.commit().await() + + // Beri tahu Fragment bahwa proses sudah selesai + _prosesSelesai.value = true + + } catch (e: Exception) { + Log.e("VerifikasiViewModel", "Gagal menerima klaim", e) + _errorMessage.value = "Terjadi kesalahan: ${e.message}" + } + } + } +} diff --git a/app/src/main/java/com/androidprojek/unifind/viewmodel/NotificationMainViewModel.kt b/app/src/main/java/com/androidprojek/unifind/viewmodel/NotificationMainViewModel.kt new file mode 100644 index 0000000..fd65422 --- /dev/null +++ b/app/src/main/java/com/androidprojek/unifind/viewmodel/NotificationMainViewModel.kt @@ -0,0 +1,45 @@ +package com.androidprojek.unifind.viewmodel + +import androidx.lifecycle.ViewModel +import com.androidprojek.unifind.R +// Pastikan path ke Repository Anda benar +import com.androidprojek.unifind.datanotification.NotificationRepository +import com.androidprojek.unifind.model.NotificationModel +import java.util.Date + +class NotificationMainViewModel : ViewModel() { + + // ViewModel tidak lagi menyimpan data. + // Dia hanya menunjuk langsung ke data yang ada di Repository. + val notificationList = NotificationRepository.notificationList + + /** + * Fungsi ini hanya membuat model notifikasi lalu meneruskannya + * ke Repository untuk ditambahkan. + */ + fun addPostSuccessNotification() { + val newNotification = NotificationModel( + id = Date().time, + iconResId = R.drawable.form, + message = "Form penemuan barang kamu sudah berhasil terkirim!", + timestamp = "baru saja" + ) + // Meneruskan perintah ke Repository + NotificationRepository.addNotification(newNotification) + } + + /** + * Fungsi ini juga hanya membuat model notifikasi lalu meneruskannya + * ke Repository untuk ditambahkan. + */ + fun addSearchSuccessNotification() { + val newNotification = NotificationModel( + id = Date().time, + iconResId = R.drawable.form, + message = "form pencarian barang kamu sudah berhasil terkirim.", + timestamp = "baru saja" + ) + // Meneruskan perintah ke Repository + NotificationRepository.addNotification(newNotification) + } +} \ No newline at end of file diff --git a/app/src/main/res/color/chip_filter_selector.xml b/app/src/main/res/color/chip_filter_selector.xml new file mode 100644 index 0000000..d125193 --- /dev/null +++ b/app/src/main/res/color/chip_filter_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/abdul.png b/app/src/main/res/drawable/abdul.png new file mode 100644 index 0000000..45a88a6 Binary files /dev/null and b/app/src/main/res/drawable/abdul.png differ diff --git a/app/src/main/res/drawable/abdulgami.png b/app/src/main/res/drawable/abdulgami.png new file mode 100644 index 0000000..25f0c7e Binary files /dev/null and b/app/src/main/res/drawable/abdulgami.png differ diff --git a/app/src/main/res/drawable/add_icon.xml b/app/src/main/res/drawable/add_icon.xml new file mode 100644 index 0000000..2ae27b8 --- /dev/null +++ b/app/src/main/res/drawable/add_icon.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/arrow_down.png b/app/src/main/res/drawable/arrow_down.png new file mode 100644 index 0000000..c81c8eb Binary files /dev/null and b/app/src/main/res/drawable/arrow_down.png differ diff --git a/app/src/main/res/drawable/arrow_up.png b/app/src/main/res/drawable/arrow_up.png new file mode 100644 index 0000000..8a1dbc7 Binary files /dev/null and b/app/src/main/res/drawable/arrow_up.png differ diff --git a/app/src/main/res/drawable/back_icon.png b/app/src/main/res/drawable/back_icon.png new file mode 100644 index 0000000..2f1ce3f Binary files /dev/null and b/app/src/main/res/drawable/back_icon.png differ diff --git a/app/src/main/res/drawable/barang_contoh.png b/app/src/main/res/drawable/barang_contoh.png new file mode 100644 index 0000000..8fb962f Binary files /dev/null and b/app/src/main/res/drawable/barang_contoh.png differ diff --git a/app/src/main/res/drawable/baseline_add_24.xml b/app/src/main/res/drawable/baseline_add_24.xml new file mode 100644 index 0000000..fda2f8e --- /dev/null +++ b/app/src/main/res/drawable/baseline_add_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_image_24.xml b/app/src/main/res/drawable/baseline_image_24.xml new file mode 100644 index 0000000..51981e2 --- /dev/null +++ b/app/src/main/res/drawable/baseline_image_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_person_outline_24.xml b/app/src/main/res/drawable/baseline_person_outline_24.xml new file mode 100644 index 0000000..a3be1b8 --- /dev/null +++ b/app/src/main/res/drawable/baseline_person_outline_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_edittext_outline.xml b/app/src/main/res/drawable/bg_edittext_outline.xml new file mode 100644 index 0000000..b021207 --- /dev/null +++ b/app/src/main/res/drawable/bg_edittext_outline.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_form_input_field.xml b/app/src/main/res/drawable/bg_form_input_field.xml new file mode 100644 index 0000000..0e4904d --- /dev/null +++ b/app/src/main/res/drawable/bg_form_input_field.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/bg_form_upload_area.xml b/app/src/main/res/drawable/bg_form_upload_area.xml new file mode 100644 index 0000000..2bcb164 --- /dev/null +++ b/app/src/main/res/drawable/bg_form_upload_area.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_textview.xml b/app/src/main/res/drawable/bg_textview.xml new file mode 100644 index 0000000..146e4ca --- /dev/null +++ b/app/src/main/res/drawable/bg_textview.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_bg.xml b/app/src/main/res/drawable/circle_bg.xml new file mode 100644 index 0000000..b32ddd5 --- /dev/null +++ b/app/src/main/res/drawable/circle_bg.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/circle_mask.xml b/app/src/main/res/drawable/circle_mask.xml new file mode 100644 index 0000000..bb5d5e2 --- /dev/null +++ b/app/src/main/res/drawable/circle_mask.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/contact_item_background.xml b/app/src/main/res/drawable/contact_item_background.xml new file mode 100644 index 0000000..22ad753 --- /dev/null +++ b/app/src/main/res/drawable/contact_item_background.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_background.xml b/app/src/main/res/drawable/dialog_background.xml new file mode 100644 index 0000000..f3e0979 --- /dev/null +++ b/app/src/main/res/drawable/dialog_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/edit_text_border.xml b/app/src/main/res/drawable/edit_text_border.xml new file mode 100644 index 0000000..a46efd3 --- /dev/null +++ b/app/src/main/res/drawable/edit_text_border.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/empty_pelacakan.png b/app/src/main/res/drawable/empty_pelacakan.png new file mode 100644 index 0000000..eda4d37 Binary files /dev/null and b/app/src/main/res/drawable/empty_pelacakan.png differ diff --git a/app/src/main/res/drawable/enjun.jpg b/app/src/main/res/drawable/enjun.jpg new file mode 100644 index 0000000..57fa5a6 Binary files /dev/null and b/app/src/main/res/drawable/enjun.jpg differ diff --git a/app/src/main/res/drawable/eye_icon.png b/app/src/main/res/drawable/eye_icon.png new file mode 100644 index 0000000..310379f Binary files /dev/null and b/app/src/main/res/drawable/eye_icon.png differ diff --git a/app/src/main/res/drawable/filter_icon.png b/app/src/main/res/drawable/filter_icon.png new file mode 100644 index 0000000..a56b183 Binary files /dev/null and b/app/src/main/res/drawable/filter_icon.png differ diff --git a/app/src/main/res/drawable/form.png b/app/src/main/res/drawable/form.png new file mode 100644 index 0000000..16471bf Binary files /dev/null and b/app/src/main/res/drawable/form.png differ diff --git a/app/src/main/res/drawable/frame_10.xml b/app/src/main/res/drawable/frame_10.xml new file mode 100644 index 0000000..4a02f99 --- /dev/null +++ b/app/src/main/res/drawable/frame_10.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 0000000..f2e36f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_up.xml b/app/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 0000000..672b3d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back_arrow.png b/app/src/main/res/drawable/ic_back_arrow.png new file mode 100644 index 0000000..2f1ce3f Binary files /dev/null and b/app/src/main/res/drawable/ic_back_arrow.png differ diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..9565504 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_contact_person.xml b/app/src/main/res/drawable/ic_contact_person.xml new file mode 100644 index 0000000..49a52af --- /dev/null +++ b/app/src/main/res/drawable/ic_contact_person.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml new file mode 100644 index 0000000..46fc8de --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit.png b/app/src/main/res/drawable/ic_edit.png new file mode 100644 index 0000000..bdb4be5 Binary files /dev/null and b/app/src/main/res/drawable/ic_edit.png differ diff --git a/app/src/main/res/drawable/ic_eye.png b/app/src/main/res/drawable/ic_eye.png new file mode 100644 index 0000000..233fa4d Binary files /dev/null and b/app/src/main/res/drawable/ic_eye.png differ diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 0000000..a7e1ff2 --- /dev/null +++ b/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml new file mode 100644 index 0000000..f8bb0b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_instagram.jpg b/app/src/main/res/drawable/ic_instagram.jpg new file mode 100644 index 0000000..129f6fa Binary files /dev/null and b/app/src/main/res/drawable/ic_instagram.jpg differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..e009ebe --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_line.png b/app/src/main/res/drawable/ic_line.png new file mode 100644 index 0000000..d474be4 Binary files /dev/null and b/app/src/main/res/drawable/ic_line.png differ diff --git a/app/src/main/res/drawable/ic_location.png b/app/src/main/res/drawable/ic_location.png new file mode 100644 index 0000000..a5c1670 Binary files /dev/null and b/app/src/main/res/drawable/ic_location.png differ diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml new file mode 100644 index 0000000..78b75c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_time.png b/app/src/main/res/drawable/ic_time.png new file mode 100644 index 0000000..7f4e259 Binary files /dev/null and b/app/src/main/res/drawable/ic_time.png differ diff --git a/app/src/main/res/drawable/ic_whatsapp.png b/app/src/main/res/drawable/ic_whatsapp.png new file mode 100644 index 0000000..08beef1 Binary files /dev/null and b/app/src/main/res/drawable/ic_whatsapp.png differ diff --git a/app/src/main/res/drawable/input_rectangle.xml b/app/src/main/res/drawable/input_rectangle.xml new file mode 100644 index 0000000..3f4e3ea --- /dev/null +++ b/app/src/main/res/drawable/input_rectangle.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/input_rectangle_description.xml b/app/src/main/res/drawable/input_rectangle_description.xml new file mode 100644 index 0000000..da9d670 --- /dev/null +++ b/app/src/main/res/drawable/input_rectangle_description.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/input_rectangle_image.xml b/app/src/main/res/drawable/input_rectangle_image.xml new file mode 100644 index 0000000..1ef57be --- /dev/null +++ b/app/src/main/res/drawable/input_rectangle_image.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/item_notification_background.xml b/app/src/main/res/drawable/item_notification_background.xml new file mode 100644 index 0000000..3571bb4 --- /dev/null +++ b/app/src/main/res/drawable/item_notification_background.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/jeki.png b/app/src/main/res/drawable/jeki.png new file mode 100644 index 0000000..f03a898 Binary files /dev/null and b/app/src/main/res/drawable/jeki.png differ diff --git a/app/src/main/res/drawable/lacak_button.xml b/app/src/main/res/drawable/lacak_button.xml new file mode 100644 index 0000000..10b1faa --- /dev/null +++ b/app/src/main/res/drawable/lacak_button.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/login_button.xml b/app/src/main/res/drawable/login_button.xml new file mode 100644 index 0000000..323a21a --- /dev/null +++ b/app/src/main/res/drawable/login_button.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/login_warning.xml b/app/src/main/res/drawable/login_warning.xml new file mode 100644 index 0000000..c3d97dd --- /dev/null +++ b/app/src/main/res/drawable/login_warning.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000..b7e4032 Binary files /dev/null and b/app/src/main/res/drawable/logo.png differ diff --git a/app/src/main/res/drawable/logo_bg.png b/app/src/main/res/drawable/logo_bg.png new file mode 100644 index 0000000..7388fd5 Binary files /dev/null and b/app/src/main/res/drawable/logo_bg.png differ diff --git a/app/src/main/res/drawable/logo_blue.png b/app/src/main/res/drawable/logo_blue.png new file mode 100644 index 0000000..1b94b56 Binary files /dev/null and b/app/src/main/res/drawable/logo_blue.png differ diff --git a/app/src/main/res/drawable/logo_splashscreen_1.png b/app/src/main/res/drawable/logo_splashscreen_1.png new file mode 100644 index 0000000..edfd62a Binary files /dev/null and b/app/src/main/res/drawable/logo_splashscreen_1.png differ diff --git a/app/src/main/res/drawable/logo_splashscreen_2.png b/app/src/main/res/drawable/logo_splashscreen_2.png new file mode 100644 index 0000000..eefd31d Binary files /dev/null and b/app/src/main/res/drawable/logo_splashscreen_2.png differ diff --git a/app/src/main/res/drawable/logo_splashscreen_3.png b/app/src/main/res/drawable/logo_splashscreen_3.png new file mode 100644 index 0000000..f2d0739 Binary files /dev/null and b/app/src/main/res/drawable/logo_splashscreen_3.png differ diff --git a/app/src/main/res/drawable/logo_u_biru.png b/app/src/main/res/drawable/logo_u_biru.png new file mode 100644 index 0000000..374799d Binary files /dev/null and b/app/src/main/res/drawable/logo_u_biru.png differ diff --git a/app/src/main/res/drawable/mamad.png b/app/src/main/res/drawable/mamad.png new file mode 100644 index 0000000..5ae5db4 Binary files /dev/null and b/app/src/main/res/drawable/mamad.png differ diff --git a/app/src/main/res/drawable/my_icon.png b/app/src/main/res/drawable/my_icon.png new file mode 100644 index 0000000..eefd31d Binary files /dev/null and b/app/src/main/res/drawable/my_icon.png differ diff --git a/app/src/main/res/drawable/nim_inactive.png b/app/src/main/res/drawable/nim_inactive.png new file mode 100644 index 0000000..94a3763 Binary files /dev/null and b/app/src/main/res/drawable/nim_inactive.png differ diff --git a/app/src/main/res/drawable/notification.png b/app/src/main/res/drawable/notification.png new file mode 100644 index 0000000..eebec3a Binary files /dev/null and b/app/src/main/res/drawable/notification.png differ diff --git a/app/src/main/res/drawable/rectangle_blue.png b/app/src/main/res/drawable/rectangle_blue.png new file mode 100644 index 0000000..53fb54b Binary files /dev/null and b/app/src/main/res/drawable/rectangle_blue.png differ diff --git a/app/src/main/res/drawable/rounded_background.xml b/app/src/main/res/drawable/rounded_background.xml new file mode 100644 index 0000000..4ae1c6b --- /dev/null +++ b/app/src/main/res/drawable/rounded_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/search_background.xml b/app/src/main/res/drawable/search_background.xml new file mode 100644 index 0000000..a865253 --- /dev/null +++ b/app/src/main/res/drawable/search_background.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_notification_default.xml b/app/src/main/res/drawable/shape_notification_default.xml new file mode 100644 index 0000000..60baf61 --- /dev/null +++ b/app/src/main/res/drawable/shape_notification_default.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_notification_pressed.xml b/app/src/main/res/drawable/shape_notification_pressed.xml new file mode 100644 index 0000000..3fa97fc --- /dev/null +++ b/app/src/main/res/drawable/shape_notification_pressed.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/status_background.xml b/app/src/main/res/drawable/status_background.xml new file mode 100644 index 0000000..0ea2759 --- /dev/null +++ b/app/src/main/res/drawable/status_background.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/subang.jpg b/app/src/main/res/drawable/subang.jpg new file mode 100644 index 0000000..f470dcb Binary files /dev/null and b/app/src/main/res/drawable/subang.jpg differ diff --git a/app/src/main/res/drawable/unggah_foto.png b/app/src/main/res/drawable/unggah_foto.png new file mode 100644 index 0000000..f5d511d Binary files /dev/null and b/app/src/main/res/drawable/unggah_foto.png differ diff --git a/app/src/main/res/drawable/warning_vector.png b/app/src/main/res/drawable/warning_vector.png new file mode 100644 index 0000000..ac34c90 Binary files /dev/null and b/app/src/main/res/drawable/warning_vector.png differ diff --git a/app/src/main/res/font/plusjakartasans_bold.ttf b/app/src/main/res/font/plusjakartasans_bold.ttf new file mode 100644 index 0000000..386d3a6 Binary files /dev/null and b/app/src/main/res/font/plusjakartasans_bold.ttf differ diff --git a/app/src/main/res/font/plusjakartasans_regular.ttf b/app/src/main/res/font/plusjakartasans_regular.ttf new file mode 100644 index 0000000..1e77059 Binary files /dev/null and b/app/src/main/res/font/plusjakartasans_regular.ttf differ diff --git a/app/src/main/res/layout/activity_detail_barang.xml b/app/src/main/res/layout/activity_detail_barang.xml new file mode 100644 index 0000000..c296c19 --- /dev/null +++ b/app/src/main/res/layout/activity_detail_barang.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_form_barang.xml b/app/src/main/res/layout/activity_form_barang.xml new file mode 100644 index 0000000..bae4573 --- /dev/null +++ b/app/src/main/res/layout/activity_form_barang.xml @@ -0,0 +1,326 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +