deploy unifind
15
.gitignore
vendored
Normal 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
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
||||
unifind
|
||||
6
.idea/compiler.xml
generated
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1 @@
|
||||
/build
|
||||
87
app/build.gradle.kts
Normal 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
@ -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
@ -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
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
67
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
120
app/src/main/java/com/androidprojek/unifind/LoginActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/src/main/java/com/androidprojek/unifind/MainActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
118
app/src/main/java/com/androidprojek/unifind/RegisterActivity.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -0,0 +1,3 @@
|
||||
package com.androidprojek.unifind.model // Pastikan package-nya benar
|
||||
|
||||
data class KategoriModel(val nama: String, var isSelected: Boolean = false)
|
||||
@ -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
|
||||
@ -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
|
||||
)
|
||||
@ -0,0 +1,8 @@
|
||||
package com.androidprojek.unifind.model
|
||||
|
||||
data class PencarianLacakFormulirModel(
|
||||
val namaBarang: String?,
|
||||
val namaPoster: String?,
|
||||
val imageUrlPostingan: String?,
|
||||
val statusLaporan: String?
|
||||
)
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
)
|
||||
@ -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 = ""
|
||||
)
|
||||
@ -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)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
5
app/src/main/res/color/chip_filter_selector.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable/abdul.png
Normal file
|
After Width: | Height: | Size: 638 KiB |
BIN
app/src/main/res/drawable/abdulgami.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
5
app/src/main/res/drawable/add_icon.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable/arrow_down.png
Normal file
|
After Width: | Height: | Size: 260 B |
BIN
app/src/main/res/drawable/arrow_up.png
Normal file
|
After Width: | Height: | Size: 252 B |
BIN
app/src/main/res/drawable/back_icon.png
Normal file
|
After Width: | Height: | Size: 300 B |
BIN
app/src/main/res/drawable/barang_contoh.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
5
app/src/main/res/drawable/baseline_add_24.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/baseline_image_24.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/baseline_person_outline_24.xml
Normal 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>
|
||||
8
app/src/main/res/drawable/bg_edittext_outline.xml
Normal 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>
|
||||
8
app/src/main/res/drawable/bg_form_input_field.xml
Normal 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>
|
||||
15
app/src/main/res/drawable/bg_form_upload_area.xml
Normal 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>
|
||||