deploy unifind

This commit is contained in:
BenedictoGeraldo
2025-09-08 14:13:18 +07:00
commit 8a0b995500
247 changed files with 12831 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -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

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
unifind

6
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

18
.idea/deploymentTargetSelector.xml generated Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-06-26T06:57:39.596367200Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\bened\.android\avd\Pixel_6a_API_24.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

20
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

6
.idea/kotlinc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.0" />
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

7
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

10
README.md Normal file
View File

@ -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

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

87
app/build.gradle.kts Normal file
View File

@ -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")
}

30
app/google-services.json Normal file
View File

@ -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"
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -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)
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Unifind"
tools:targetApi="31">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCdNFa8UKWFB6Q-Jx5nocfG7XrWdwPh2_o"/>
<activity
android:name=".ui.profile.VerifikasiLaporanMasukActivity"
android:exported="false" />
<activity
android:name=".ui.profile.LaporanMasukActivity"
android:exported="false" />
<activity
android:name=".ui.LaporPenemuanActivity"
android:exported="false" />
<activity
android:name=".ui.KontakPelaporActivity"
android:exported="false" />
<activity
android:name=".RegisterActivity"
android:exported="false" />
<activity
android:name=".ui.DetailBarangActivity"
android:exported="false" />
<activity
android:name=".ui.FormBarangActivity"
android:exported="false" />
<activity
android:name=".SplashScreenActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".LoginActivity"
android:exported="false">
</activity>
<activity
android:name=".MainActivity"
android:exported="false"
android:label="@string/app_name" />
<activity android:name=".ui.profile.KontakActivity" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -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<ImageView>(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)
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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<BarangModel>) :
RecyclerView.Adapter<BarangAdapter.BarangViewHolder>() {
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
}

View File

@ -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<String>) : RecyclerView.Adapter<ImageSliderAdapter.ImageViewHolder>() {
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
}

View File

@ -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<KategoriModel>,
private val onCategoryClick: (KategoriModel) -> Unit
) : RecyclerView.Adapter<KategoriFilterAdapter.ViewHolder>() {
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
}

View File

@ -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<PenemuanLacakFormulirModel>
) : RecyclerView.Adapter<LacakFormulirAdapter.ViewHolder>() {
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<PenemuanLacakFormulirModel>) {
listLacak = newList
notifyDataSetChanged()
}
}

View File

@ -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<LaporanPenemuanModel>) :
RecyclerView.Adapter<LaporanMasukAdapter.LaporanViewHolder>() {
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
}

View File

@ -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<NotificationModel>) :
RecyclerView.Adapter<NotificationAdapter.NotificationViewHolder>() {
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<NotificationModel>) {
this.notificationList = newNotificationList
notifyDataSetChanged() // Perintahkan RecyclerView untuk menggambar ulang dirinya
}
}

View File

@ -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<PencarianLacakFormulirModel>) :
RecyclerView.Adapter<PencarianLacakAdapter.LacakViewHolder>() {
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<PencarianLacakFormulirModel>) {
listLacak = newList
notifyDataSetChanged()
}
}

View File

@ -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<BarangModel>) :
RecyclerView.Adapter<PencarianPostinganSayaAdapter.PencarianViewHolder>() {
// 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
}

View File

@ -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<PenemuanModel>,
private val listener: OnItemClickListener,
private val isMyPostsPage: Boolean = false // Defaultnya false (untuk halaman Home)
) : RecyclerView.Adapter<PenemuanAdapter.ViewHolder>() {
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<PenemuanModel>) {
listPenemuan.clear()
listPenemuan.addAll(newList)
notifyDataSetChanged()
}
}

View File

@ -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<PenemuanKlaimModel>,
private val onLihatJawaban: (PenemuanKlaimModel) -> Unit,
private val onKontak: (PenemuanKlaimModel) -> Unit
) : RecyclerView.Adapter<PenemuanPengklaimAdapter.ViewHolder>() {
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
}

View File

@ -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<Tracking>,
private val onLacakClick: (Tracking) -> Unit
) : RecyclerView.Adapter<TrackingAdapter.TrackingViewHolder>() {
private val expandedPositions = mutableSetOf<Int>()
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)
}
}

View File

@ -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<MutableList<NotificationModel>>().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
}
}

View File

@ -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<String> = emptyList(),
val status: String = "Dalam Pencarian",
@ServerTimestamp val timestamp: Date? = null
) : Parcelable

View File

@ -0,0 +1,3 @@
package com.androidprojek.unifind.model // Pastikan package-nya benar
data class KategoriModel(val nama: String, var isSelected: Boolean = false)

View File

@ -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<String> = emptyList(), // Foto dari si penemu
// Status
val statusLaporan: String = "Menunggu Verifikasi",
@ServerTimestamp val timestamp: Date? = null
) : Parcelable

View File

@ -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
)

View File

@ -0,0 +1,8 @@
package com.androidprojek.unifind.model
data class PencarianLacakFormulirModel(
val namaBarang: String?,
val namaPoster: String?,
val imageUrlPostingan: String?,
val statusLaporan: String?
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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 = ""
)

View File

@ -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<BarangModel>("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)
// }
// }
// }
//}

View File

@ -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<Uri>()
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<String>) {
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<String>()
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)
}
}

View File

@ -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()
}
}
}
}
}

View File

@ -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<Uri>()
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<String>()
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<String>) {
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
}
}

View File

@ -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<String>(
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
}
}

View File

@ -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<Tracking>()
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
}
}

View File

@ -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<String>().apply {
value = "Pelacakan"
}
val text: LiveData<String> = _text
}

View File

@ -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<TextView>(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<ImageView>(R.id.back)
btnBack.setOnClickListener {
requireActivity().onBackPressedDispatcher.onBackPressed()
// Atau bisa juga:
// findNavController().navigateUp()
}
val tvKategori = view.findViewById<TextView>(R.id.item_kategori)
val tvNama = view.findViewById<TextView>(R.id.item_namaBarang)
val imageView = view.findViewById<ImageView>(R.id.item_image)
val tvKoordinat = view.findViewById<TextView>(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<TextView>(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()
}
})
}
}

View File

@ -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<LoggedInUser> {
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
}
}

View File

@ -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<LoggedInUser> {
// 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
}
}

View File

@ -0,0 +1,18 @@
package com.androidprojek.unifind.ui.data
/**
* A generic class that holds a value with its loading status.
* @param <T>
*/
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
}
}
}

View File

@ -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
)

View File

@ -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<KategoriModel>()
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
}
}

View File

@ -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<KategoriModel>()
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
}
}

View File

@ -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
}
}

View File

@ -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<String?>()
val userProfileImageUrl: LiveData<String?> = _userProfileImageUrl
// DATA SUMBER (INPUTS)
private val _originalList = MutableLiveData<List<PenemuanModel>>()
private val _searchQuery = MutableLiveData<String>("")
private val _activeCategories = MutableLiveData<List<String>>(emptyList())
// DATA HASIL (OUTPUT)
val filteredPenemuanList = MediatorLiveData<List<PenemuanModel>>()
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<PenemuanModel>()
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<String>) {
_activeCategories.value = categories
}
}

View File

@ -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<BarangModel>()
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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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")
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<String>().apply {
value = "This is notifications Fragment"
}
val text: LiveData<String> = _text
}

View File

@ -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
}
}

View File

@ -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<LaporanPenemuanModel>()
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<ImageView>(R.id.btn_close_dialog)
val layoutInstagram = dialogView.findViewById<LinearLayout>(R.id.layout_instagram)
val tvInstagram = dialogView.findViewById<TextView>(R.id.tv_instagram)
val layoutLine = dialogView.findViewById<LinearLayout>(R.id.layout_line)
val tvLine = dialogView.findViewById<TextView>(R.id.tv_line)
val layoutWhatsapp = dialogView.findViewById<LinearLayout>(R.id.layout_whatsapp)
val tvWhatsapp = dialogView.findViewById<TextView>(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
}
}
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<BarangModel>()
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
}
}

View File

@ -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<PenemuanModel>()
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
}
}

View File

@ -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")
}
}
}

View File

@ -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<View>(R.id.label_nama_barang).visibility = View.GONE // Asumsikan Anda memberi ID pada labelnya
tvVerifikasiKategori.visibility = View.GONE
findViewById<View>(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()
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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")
}
}
}

View File

@ -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
}
}

View File

@ -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<PenemuanLacakFormulirModel>()
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
}
}

View File

@ -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
}
}

View File

@ -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<PenemuanKlaimModel>()
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
}
}

View File

@ -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<Boolean>()
val prosesSelesai: LiveData<Boolean> = _prosesSelesai
// LiveData untuk menampilkan pesan error
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _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}"
}
}
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#49888D" android:state_checked="true" />
<item android:color="@android:color/transparent" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#1218E5" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#757575"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,5.9c1.16,0 2.1,0.94 2.1,2.1s-0.94,2.1 -2.1,2.1S9.9,9.16 9.9,8s0.94,-2.1 2.1,-2.1m0,9c2.97,0 6.1,1.46 6.1,2.1v1.1L5.9,18.1L5.9,17c0,-0.64 3.13,-2.1 6.1,-2.1M12,4C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,13c-2.67,0 -8,1.34 -8,4v3h16v-3c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<stroke
android:width="1dp"
android:color="#005B92" />
<corners android:radius="6dp" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Mengatur warna isi menjadi abu-abu sesuai permintaan -->
<solid android:color="#E4E4E5" />
<!-- Mengatur sudutnya agar membulat -->
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Warna isi latar belakang -->
<solid android:color="#F0F4F7"/>
<!-- Sudut yang membulat -->
<corners android:radius="8dp"/>
<!-- Garis tepi putus-putus -->
<stroke
android:width="2dp"
android:color="#D0DDE7"
android:dashWidth="10px"
android:dashGap="10px"/>
</shape>

Some files were not shown because too many files have changed in this diff Show More